Mono Gallery releases
Minimal monochrome photography gallery plugin for WordPress — EXIF metadata, smart albums, GPS map, image protection, watermarking, Google Photos & Apple Photos import. Optimised for Cloudways.
by Mike Richardson · github.com/mike-richardson-uk/mono-gallery · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/mike-richardson-uk/mono-gallery/archive/refs/heads/main.zipReadme
Mono Gallery
A minimal monochrome photography gallery plugin for WordPress, built for Cloudways hosting. Designed for abstract and fine-art photography — clean grid/masonry layouts, full EXIF display, a world map view, smart albums, and multi-layer image protection.
Requirements
- WordPress 6.0+
- PHP 8.1+ with GD extension (standard on Cloudways)
- MySQL 5.7+ / MariaDB 10.3+
- EXIF support in PHP (
exif_read_data— enabled by default on Cloudways)
Installation
- Upload the
mono-galleryfolder towp-content/plugins/. - Activate via Plugins → Installed Plugins.
- On activation the plugin:
- Creates five database tables (
wp_mono_photos,wp_mono_albums,wp_mono_photo_albums,wp_mono_tags,wp_mono_photo_tags) - Creates the upload directory structure under
wp-content/uploads/mono-gallery/ - Writes
.htaccessrules blocking direct access to image files - Generates a Cloudways Nginx config snippet at
mono-gallery/cloudways-nginx.conf
- Creates five database tables (
- Follow the Cloudways setup steps under Mono Gallery → Settings → Setup & Help.
Shortcodes
| Shortcode | Description |
|---|---|
[mono_gallery] |
All photos, paginated grid |
[mono_gallery album="slug"] |
Photos from a named album |
[mono_gallery tag="architecture"] |
Photos with a specific tag |
[mono_gallery layout="masonry" columns="4"] |
Masonry layout, 4 columns |
[mono_gallery per_page="36"] |
Custom page size |
[mono_gallery show_map="true"] |
Gallery + GPS map below |
[mono_album slug="japan-2024"] |
Album by slug (alias for album="") |
[mono_map] |
Standalone world map of all geotagged photos |
[mono_map album="japan-2024" height="600px"] |
Map for one album |
Features
Gallery display
- CSS grid and CSS masonry layouts (no JavaScript layout library)
- Lightbox with swipe support and keyboard navigation (← → Esc)
- EXIF panel in lightbox: camera, lens, aperture, shutter, ISO, focal length, date
- Leaflet.js world map (OpenStreetMap tiles, no API key required) — grayscale filtered to match the monochrome aesthetic
- Lazy loading on all images
- Search and album/tag filters on the front end
Photo management
- Drag-and-drop bulk uploader in the admin (JPEG, PNG, WebP)
- EXIF extraction on upload: camera make/model, lens, aperture, shutter speed, ISO, focal length, GPS coordinates, date taken
- Tags (free-form, comma-separated)
- Manual albums — assign photos individually
- Smart albums — auto-populated by rules (camera model, lens, ISO, aperture, date year/month, GPS presence, tag, import source). All rules are AND-combined.
Import
- Google Photos — full OAuth2 integration. Imports media items from any album or your entire library. Skips duplicates and videos automatically.
- Apple Photos — iCloud Shared Album import. Create a shared album in Photos.app, copy the iCloud link, paste it into the import page. No Apple developer account required.
Image protection
| Layer | How it works |
|---|---|
| Signed proxy URLs | Every image URL contains an HMAC-SHA256 token. Cannot be guessed or bulk-enumerated. |
| Referrer check | Server-side: requests without a matching referrer are rejected with 403. |
.htaccess block |
Direct HTTP access to the originals, thumbs, and watermarked directories is denied. |
| Nginx rules | Provided config snippet blocks direct access at the Nginx layer (faster than .htaccess). |
| Transparent overlay | A <div> covers every image, intercepting right-click, drag, and iOS touch-hold. |
| CSS/JS | pointer-events: none, user-select: none, -webkit-user-drag: none on all <img> elements. |
| Cache headers | Cache-Control: private, no-store — bypasses Varnish, Breeze, and browser cache. |
| Keyboard shortcuts | Ctrl+S, Ctrl+U, Ctrl+Shift+I, PrintScreen blocked on gallery pages (opt-in). |
Watermark
- Server-side GD watermarking — burned into served copies, originals untouched
- Configurable text (supports
{year}placeholder), position (9 positions), opacity, and size - Drop a
mono.ttfTrueType font intoassets/fonts/for crisp text rendering; falls back to GD's built-in bitmap font - Watermark cache invalidated automatically when settings change
Performance (Cloudways-optimised)
- Redis object cache — photo queries, EXIF lookups, and thumbnail existence checks are cached via
wp_cache_*. When the Redis Object Cache plugin is active on Cloudways, all cache calls hit Redis automatically. - WebP conversion — opt-in. JPEG/PNG thumbnails are converted to WebP on first request and cached. Browsers that send
Accept: image/webpreceive WebP automatically (~30% smaller). - 304 Not Modified — ETag headers on all image responses. Browsers skip re-downloading images they've seen before.
- On-demand thumbnails — thumbnails are generated on first request, not on upload. Large imports don't block.
- X-Accel-Buffering: no — tells Nginx not to buffer the full image in memory before sending.
- X-Cache-Bypass: 1 — tells Cloudways Breeze to skip its cache for image proxy requests.
Cloudways Setup
1. Enable Redis
- Cloudways Platform → Application → Packages → ensure Redis is installed.
- In WordPress, install the Redis Object Cache plugin.
- Settings → Redis → Enable Object Cache.
2. Apply Nginx Rules
- Cloudways Platform → Application → Settings & Packages → Nginx Settings → Custom Vhost.
- Copy the contents of
mono-gallery/cloudways-nginx.confand paste them in. - Save and restart Nginx.
The snippet:
- Blocks direct access to gallery image directories at the Nginx layer
- Bypasses Varnish/Breeze for the image proxy and REST API endpoints
- Sets generous FastCGI timeouts for large image handling
3. Breeze Cache Exclusions
Settings → Breeze → CDN / Advanced — add these to the exclusion list:
/mono-gallery/img/
/wp-json/mono-gallery/
4. Cloudflare (if used)
Add a Cache Rule in Cloudflare to bypass cache for */mono-gallery/img/*. Signed image URLs are per-user and must never be served from a shared CDN cache.
5. PHP Settings
In Cloudways → Application → Settings & Packages → PHP FPM Settings:
| Setting | Recommended |
|---|---|
memory_limit |
256M |
max_execution_time |
120 |
upload_max_filesize |
50M |
post_max_size |
50M |
max_file_uploads |
50 |
REST API
All endpoints are under /wp-json/mono-gallery/v1/.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /photos |
Public | List photos. Params: album_id, tag, search, has_gps, order, orderby, per_page, page |
| GET | /photos/{id} |
Public | Single photo with full EXIF |
| PATCH | /photos/{id} |
Admin | Update title, description, tags, location name |
| DELETE | /photos/{id} |
Admin | Delete photo and all cached files |
| POST | /upload |
Admin | Upload a photo file. Params: file, title, tags, album_id |
| GET | /albums |
Public | List all albums |
| POST | /albums |
Admin | Create album |
| PATCH | /albums/{id} |
Admin | Update album |
| DELETE | /albums/{id} |
Admin | Delete album |
| POST | /albums/{id}/photos |
Admin | Set album photo membership |
| GET | /tags |
Public | List all tags with counts |
| GET | /google/auth |
Admin | Get Google OAuth URL |
| POST | /google/import |
Admin | Import from Google Photos |
| POST | /google/disconnect |
Admin | Disconnect Google account |
| GET | /apple/links |
Admin | List iCloud shared album links |
| POST | /apple/links |
Admin | Add an iCloud link |
| DELETE | /apple/links/{index} |
Admin | Remove a link |
| POST | /apple/import |
Admin | Import from an iCloud link |
| POST | /smart-albums/rebuild |
Admin | Rebuild all smart album memberships |
File Structure
mono-gallery/
├── mono-gallery.php Main plugin file
├── cloudways-nginx.conf Generated Nginx config snippet (copy to Cloudways)
├── assets/
│ └── fonts/
│ └── mono.ttf (drop your .ttf here for watermark text)
├── includes/
│ ├── class-activator.php DB setup, upload dirs, .htaccess, Nginx config
│ ├── class-photo.php Photo CRUD and tag management
│ ├── class-album.php Album CRUD and photo membership
│ ├── class-exif.php EXIF extraction (GD/exif_read_data)
│ ├── class-image-server.php Protected image proxy, watermarking, WebP serving
│ ├── class-cache.php Redis/object cache helpers
│ ├── class-webp.php WebP conversion and stats
│ ├── class-google-photos.php Google Photos OAuth2 and import
│ ├── class-apple-photos.php iCloud Shared Albums import
│ ├── class-smart-albums.php Smart album rule engine
│ └── class-rest-api.php REST API endpoints
├── admin/
│ ├── class-admin.php Admin menus and settings pages
│ ├── css/admin.css
│ └── js/admin.js Library grid, uploader, album builder
└── public/
├── class-shortcode.php Shortcode registration
├── css/gallery.css Minimal monochrome front-end CSS
└── js/gallery.js Grid, masonry, lightbox, map, protection
Changelog
1.0.1 — Security patch
- [Critical] Fixed SSRF in Apple Photos import: strict regex allowlist now enforces that the iCloud API host matches
p{N}-sharedstreams.icloud.comexactly — subdomain confusion attacks (e.g.icloud.com.evil.com) are rejected - [Critical] Fixed SSRF via unvalidated download URLs in Apple Photos import: download URLs must be
https://and resolve to*.icloud.comor*.apple.combeforedownload_url()is called - [High] Fixed broken Google Photos OAuth state verification:
wp_verify_nonce()arguments were swapped, meaning the incoming state was never actually compared — corrected argument order - [High] Added
require_adminpermission check to the Google OAuth callback REST endpoint, which was previously publicly accessible - [High] Upload MIME type validation now uses
wp_check_filetype_and_ext()(validates file bytes, not just extension) plusgetimagesize()confirmation — prevents extension-spoofed uploads - [High] Tab labels in admin settings page escaped with
esc_html()(XSS hardening) - [Medium] Image proxy tokens are now time-windowed (24-hour expiry, previous window also accepted) — leaked URLs from logs/referrers eventually expire
- [Medium] Watermark cache file written atomically via temp-file-then-rename, eliminating TOCTOU race condition under concurrent requests
- [Medium] Watermark cache key now derived from watermark-specific settings only, not the full options array (which included Google OAuth secrets)
- [Medium] Google Photos OAuth tokens stored in a non-autoloaded
wp_optionsrow, reducing exposure in serialized dumps and per-request memory - [Medium]
sort_orderalbum field validated against an explicit allowlist before storage - [Low] Plugin directory protected by
.htaccessblocking web access to.conf,.md,.txtfiles (prevents the generated Nginx config from being read publicly) - [Low] Leaflet map popup built via DOM API instead of
innerHTMLtemplate interpolation (eliminates XSS surface onphoto.urls.thumb) - [Low] Leaflet 1.9.4 loaded from unpkg with verified SHA-256 Subresource Integrity hashes and
crossorigin="anonymous"— compromised CDN cannot inject code - [Low] Apple Photos import error messages sanitised — raw
WP_Errormessages (which can contain filesystem paths from cURL/TLS errors) are no longer returned to the browser
1.0.0
- Initial release
- Grid and masonry layouts
- EXIF extraction and display
- Leaflet.js GPS map
- Manual and smart albums
- Google Photos OAuth2 import
- Apple Photos iCloud Shared Albums import
- Multi-layer image protection (signed URLs, overlays, .htaccess, Nginx rules)
- Server-side GD watermarking
- WebP conversion
- Redis object caching
- Cloudways Nginx config generator
- Tabbed settings page with inline setup guide
Licence
GPL-2.0+
Read the full README on GitHub →
Releases
These releases are tags only. The author does not attach a packaged zip, so there are no download counts to report.