Bundle extension (upload hook + API endpoint + admin module) that warms Directus image transform cache in-process with an explicit acceptFormat, so the generated file matches the browser's cache key byte-for-byte. - resumable, window-aware backfill with progress + cancel - system + custom presets, effective-format reporting, shadow warnings - storage breakdown (previews vs originals), human-readable API fields - diagnostics: resolve-plan prediction, per-file generate, ops ring buffer - isolated dev/staging compose stack + deploy/rollback scripts Verified on Directus 11.17.4: 40/40 tests, hash-match gate passed, idempotent backfill, custom fixed-format presets, error surfacing. |
||
|---|---|---|
| .opencode | ||
| dev | ||
| src | ||
| .gitignore | ||
| AGENTS.md | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
directus-extension-generate-image-previews
Pre-generate Directus image transform ("preview") cache files so the admin UI and API never pay the first-request sharp cost.
A Directus bundle:
- upload hook — warm previews right after an image upload.
- new-preset trigger —
Run nowwarms every preset (system + custom) for existing images. - backfill job — resumable, window-aware sweep over the existing library.
- admin module — progress, storage breakdown, configuration, diagnostics, and API reference.
Target: self-hosted Directus 11.17.4 (PM2 cluster, local disk storage).
Why this approach (read AGENTS.md too)
The cache file is <filename_disk>__<object-hash(resolved transforms)>.<ext>. The hash depends on the
RESOLVED transforms, and every system-* preset is format:'auto' — which resolves differently
server-side (jpg/png) than in a browser (avif). We therefore generate in-process with an explicit
acceptFormat (avif + webp), reproducing the browser's cache file byte-for-byte without any HTTP
request. Naive warming (or the popular community extension) silently misses.
Install / build / publish
npm install
npm run build # emits dist/app.js + dist/api.js
npm run typecheck # 0 errors
npm test # unit tests
npm run dev # watch build
npm run publish # scp dist + package.json to the (staging) deploy target
Settings
Defaults are applied automatically; everything is editable at runtime from the admin module — no rebuild.
| setting | default | meaning |
|---|---|---|
enabled |
true |
master switch (off = hooks/backfill inert) |
onUpload |
true |
generate previews immediately after an upload |
windowStart / windowEnd |
01:00 / 04:00 |
scheduled runs only start inside this window (local time) |
concurrency |
1 |
parallel generations |
formats |
avif,webp |
output variants for format:'auto' presets only (all system presets) |
presetKeys |
null (all) |
restrict to specific preset keys |
debug |
false |
verbose generation logs in the Directus server log |
IMAGE_PREVIEWS_STATE_DIR |
<STORAGE_LOCAL_ROOT>/.image-previews |
where settings + progress state live |
Presets and formats
- System presets (
system-small|medium|large×cover|contain) always useformat:'auto', so they generate one file per entry informats(default avif + webp), at their built-in sizes (64 / 300 / 800 px). - Custom presets from Settings → File Storage are added automatically. A custom preset with a fixed
format(e.g.png) ignoresformatsand generates exactly that format. A custom preset withformat:'auto'followsformats. - A custom preset whose
keyequals a system key is shadowed (system wins, same as Directus) and is flagged in the admin module.
Storage reporting
GET /image-previews/disk-usage (and the admin panel) reports bytes + file counts separately for
preview assets and original assets, plus the total — in both raw bytes and human-readable form.
API endpoints
All require an admin session or admin token. Human-readable fields accompany machine-readable ones.
GET /image-previews/status— run progress, rate, ETA, window state, ISO + human datesGET /image-previews/config,PUT /image-previews/configGET /image-previews/presets— system + custom presets, effective formats, shadowed keysPOST /image-previews/run— start backfill ({"ignoreWindow":true}for an immediate run)POST /image-previews/run/cancelGET /image-previews/disk-usage— previews vs originals vs totalPOST /image-previews/generate— one file ({fileId, keys?, formats?, dryRun?})GET /image-previews/resolve-plan?fileId=&key=— predicted cache filename, no writeGET /image-previews/ops?limit=— recent operations with timing + error messages
The admin module includes a full API reference with curl examples.
Safe testing & deployment
Never test against the prod instance.
- Local / staging (
dev/compose.dev.yml): Directus 11.17.4 + its own postgres + volumes, on separate ports. Shares nothing with prod. Seedev/README.md. - Fail-open: entrypoints never throw into Directus core.
- Rollout: build → verify on staging → back up previous
dist/→ deploy → healthcheck → enable. Rollback: restore previousdist/. - Scripts:
dev/deploy-staging.sh,dev/rollback-staging.sh, templatedev/compose.staging.yml.
Caveats
- Transform dimension guard, not a file-size limit. Directus rejects transformations of images whose
width or height exceeds
ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION(default 6000 px). This is a Directus env var, not something this extension imposes. To process larger images, raise it on the Directus instance (and watch memory). Upload size limits (FILES_MAX_UPLOAD_SIZE,MAX_PAYLOAD_SIZE) are separate. - Warm only after uploads are committed (local writes are non-atomic).
- In-process generation bypasses the HTTP rate limiter; bound it with
concurrency+ the window. - Unsupported custom preset formats (e.g.
bmp) fail at generation time; the error appears in Recent operations //ops.
See .opencode/goal.md for the full contract and .opencode/feature_list.json for the task list.