BB Media: design#
Status: phases 1–4 built (0.4.0, 2026-11-08); phase 5 (R2 offload) not started, as planned: only if disk or sync remain a problem.
BB Media is the media layer under BB Cache and BB Migrate: one place that knows every image file, which attachment and size it belongs to, how big it is, what it looks like on disk in other formats, and where the site uses it. It replaces guesses that the other two plugins make today, and later the one job Smush Pro does here.
It is not a Smush or WP Offload Media clone. Compression in the cloud, a CDN and lazy loading are left to the tools that already do them (the server's Imagick, Cloudflare, BB Cache).
1. Where things stand (local mirror of live, 2026-11-08)#
| Item | State |
|---|---|
| Media | 3,895 attachments, about 42,600 image files, 3.6 GB in uploads; 6 registered sizes (thumbnail, medium, medium_large, large, 1536x1536, 2048x2048) |
| Smush Pro 4.3.3 | Active. Only automatic lossy compression on upload and EXIF stripping (3,114 attachments done). WebP, AVIF, CDN, lazy load, resize and backups are off; originals are not compressed; no smush-webp folder |
| WebP | Comes from Cloudflare Polish on live only, at the edge. Staging and local serve JPEG/PNG |
| Image libraries | Staging: Imagick with WebP and AVIF, GD with WebP, PHP 8.1. Local Docker: no Imagick, GD without WebP. Live: not checked (Cloudways usually ships Imagick) |
| Staging disk | 9.7 GB volume, 87% used (1.3 GB free), uploads 2.9 GB; t2.micro with gp2 burst credits |
2. Pain points this is for#
| # | What happened | Today | With BB Media |
|---|---|---|---|
| 1 | BB Cache's srcset vanished on staging: it guesses an attachment from an image URL (attachment_id()), and an http/https mismatch plus cached misses broke it (fixed 2026-11-06 with a scheme-less compare and a hits-only transient). |
Each render guesses from URLs; misses are retried per request. | An index of every file (path → attachment, size, width, height, bytes, hash), kept current on upload, edit and delete. BB Cache looks a URL up instead of guessing. |
| 2 | BB Cache measures images with getimagesize() while rendering and keeps the results in an ever-growing option (bb_cache_img_dims) to add width/height. |
Disk reads on cache misses; a stale entry when a file is replaced. | Dimensions come from the index. |
| 3 | BB Cache's "serve WebP" looks for name.webp, name.jpg.webp and Smush's smush-webp tree; none exist, so it never fires. Staging scores trail live partly because only live gets Polish. |
No WebP outside Cloudflare Polish. | Phase 4: WebP (optionally AVIF) siblings written next to each size as name.jpg.webp, which BB Cache already finds. |
| 4 | The staging sync pushed every size of every image; the disk filled and the gp2 burst balance hit 0. Switching to originals only made it worse, because regenerating sizes on the t2.micro was slower than copying them. | Uploads pushes compare path, size and mtime; regeneration is all or nothing. | File hashes in the index: a push sends only files whose content differs, and the receiver can say which it already has. Regeneration, when chosen, runs in small background batches with a CPU budget instead of inside the import request. |
| 5 | The same image uploaded on two sites gets two paths (x.jpg, x-1.jpg) and two attachments, so packages bring duplicates. |
Attachments are matched by path (plus md5 of that path's file). | Matched by content hash first, then path. |
| 6 | Cleaning up unused media (old Lottie files, superseded uploads) had no safe basis: nothing records what is used where. A wp post delete from a filter WP-CLI ignored once emptied the media library. |
Manual, risky. | A usage index (post content, post meta and ACF fields, ACF options, snippets, theme CSS/SCSS and twig, BB Snippets theme files) and a read-only "unused" report. Deleting stays a person's decision, by listed IDs. |
| 7 | Smush Pro is a paid subscription and a WPMU DEV API dependency for one feature (lossy compression on upload). | Compression happens on WPMU DEV's servers. | Phase 4: compression with the server's Imagick, at a quality matched to Smush's on a sample; then Smush can be retired. |
3. Data model#
{prefix}bb_media_files(custom table; post meta would mean 42,000 rows of serialized arrays to search):path(relative to uploads, unique),attachment_id,size(fullor a size name, ororphan),width,height,bytes,hash(sha1 of the content),mime,variants(JSON:webp/avifsibling bytes),optimized(JSON: bytes before/after, method, quality),checked(time). Index onattachment_idandhash.{prefix}bb_media_usage:attachment_id,object_type(post, option, snippet, theme-file),object_idor path,where(content, a meta key or ACF field name, a CSS file),how(id, url). Rebuilt incrementally on save and fully by WP-CLI.- Attachment meta stays WordPress's own (
_wp_attachment_metadata,_wp_attached_file): nothing is moved or renamed, so turning BB Media off leaves a normal media library. Smush's meta is read once for the report and otherwise left alone.
4. Components#
- Index: backfill by WP-CLI (
wp bb-media index, resumable, throttled), then kept current fromwp_generate_attachment_metadata,wp_update_attachment_metadata,delete_attachmentand file replacements. Files under uploads that no attachment owns are indexed asorphan. - Lookup API for the other plugins:
bb_media_lookup( $url_or_path )→ attachment, size, width, height, hash, variants;bb_media_srcset( $id ). BB Cache uses it when present and keeps its current code as the fallback. - Usage scanner: incremental on post/option/snippet saves; full scan by WP-CLI; reports in the admin
(Media → Usage) and
wp bb-media unused/wp bb-media where <id|url>. Read-only. - Migrate bridge: hash lists for the uploads scope, hash-first attachment matching in packages, and throttled background regeneration as an option. Lives behind BB Migrate's existing signed endpoint (no new remote surface without the same review BB Migrate 0.2.2 had).
- Optimizer (phase 4): on upload, after sizes exist: recompress each size with Imagick (originals untouched, as
now), write
.webp(and.avifif chosen) siblings, record before/after bytes. Bulk by WP-CLI in batches. Never on a page request.
5. Phases and gates#
- Index, lookup, BB Cache integration (no image is written). Gate: BB Cache output (srcset, sizes, width/height,
LCP preload) identical on the 323 public pages with and without BB Media; index backfill time and table size on
local and staging.
Done (0.1.0). Local: 42,879 files of 3,895 attachments indexed in 15 s, unattached scan 3 s (8,860 files, about
970 MB, mostly year folders),
verifyfinds no difference from the disk. Gate: 262 pages identical; 61 pages gain a srcset on 13 images whosesrcis root-relative (/wp-content/uploads/…), which BB Cache's own lookup never recognised. Rendered boxes of all 162 such images at 1440 and 390 px: 160 identical, 2 differ by 1 px of height (rounding of the smaller candidate). One file is named by two attachments (15145, 15168); the lower ID owns it, as WordPress's lookups return. Staging not run yet. - Usage index and the unused report. Gate: every attachment the 323 pages render is reported as used; the unused list reviewed by Matt before anything is deleted. Done (0.2.0). 7,462 references found in 1 s. None of the 5,017 files the pages render is reported unused (two referenced files are missing from local's disk). The first run showed three gaps, now closed: Yoast's options (default social image, organisation logo), and a file named by two attachments, where the one the posts use by ID is not the one that owns the path (both count as used). Unused on local: 1,154 attachments (1,003 MB), 209 more used only by drafts or trash, 8,846 unattached files (963 MB). Not reviewed or deleted.
- BB Migrate bridge. Gate: a staging push of uploads after a local change sends only the changed files; a page
package with an image that already exists under another path reuses it.
Done (0.3.0), on the local test sites (A → B). Uploads push of a folder whose 852 copies on B were identical but
older: 852 files (93 MB) to send without hashes, 0 with; a one-byte change on A: 1. Page push of a post whose featured
image exists on B only under another name: reused, no new attachment ("0 created"). Regeneration queued through
BB Migrate's filter, then only the missing sizes made. Hashing both libraries: 29 s for A's 3 GB, 13 s for B's 2 GB on
local SSDs; on staging's gp2 volume
--originals(or--sleep) keeps the burst credits. Staging not run yet. - Optimizer and WebP/AVIF; retire Smush Pro. Gate: on a sample of 50 images, bytes within a few percent of Smush's
and no visible loss (SSIM check plus a look); PSI on staging before/after.
Built (0.4.0), gate passed on local. 48 sizes of 16 Smush-compressed originals through WordPress's own upload:
JPEG 4.6% smaller than Smush at the same SSIM; WebP copies 29.9% smaller at a higher SSIM; worst file identical to
the eye at 2x. The finding that made it work: WordPress keeps a 4:4:4 source's chroma sampling, and most uploads here
are 4:4:4; forcing 4:2:0 is most of Smush's advantage. Progressive JPEG is not honoured by this ImageMagick (6.9.10)
and was not needed. Retiring Smush, when Matt decides: switch Smush's automatic compression off (BB Media's
compression then takes over new uploads), check a few uploads, then deactivate Smush; existing Smush-compressed
files stay as they are. PSI on staging still to do. Local needed the imagick PHP extension (
pecl install imagickin the running container: lost when the container is recreated; staging has it). The local Apache has no.webptype, so copies are sent as image/jpeg there; staging needs checking (the status screen does it). - Offload to Cloudflare R2 (optional, later): only if disk or sync remain a problem after phase 3. 3.6 GB fits R2's free 10 GB tier; moving uploads and rewriting URLs is the riskiest step, so it is last.
6. Decisions needed#
- Cloudflare Polish on live: which plan, and is Polish on? If yes, WebP generation mainly serves staging and local parity, and AVIF may not be worth the disk.
- Smush Pro: its cost on its own, and whether the WPMU DEV membership is needed for anything else.
- Local Docker: Imagick was added to the running container for phase 4 (
pecl install imagick-3.7.0); making it permanent means adding it to the image (a change to the dev setup). - Live Imagick: whoever has Cloudways access checks
php -mfor imagick and its WebP support. - Image sizes: 1536x1536 and 2048x2048 add files to every upload; worth checking whether any page uses them before phase 4 multiplies them by formats.
- Cleanup policy: report only (proposed), or a quarantine folder before deletion.