Pre-generate Directus image transform ("preview") cache files so the admin UI and API never pay the first-request sharp cost.
Find a file
Rawman Grey c8f8bbc5ac feat: pre-generate Directus image previews
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.
2026-10-25 11:19:54 +03:00
.opencode feat: pre-generate Directus image previews 2026-10-25 11:19:54 +03:00
dev feat: pre-generate Directus image previews 2026-10-25 11:19:54 +03:00
src feat: pre-generate Directus image previews 2026-10-25 11:19:54 +03:00
.gitignore feat: pre-generate Directus image previews 2026-10-25 11:19:54 +03:00
AGENTS.md feat: pre-generate Directus image previews 2026-10-25 11:19:54 +03:00
package-lock.json feat: pre-generate Directus image previews 2026-10-25 11:19:54 +03:00
package.json feat: pre-generate Directus image previews 2026-10-25 11:19:54 +03:00
README.md feat: pre-generate Directus image previews 2026-10-25 11:19:54 +03:00
tsconfig.json feat: pre-generate Directus image previews 2026-10-25 11:19:54 +03:00

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 now warms 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 use format:'auto', so they generate one file per entry in formats (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) ignores formats and generates exactly that format. A custom preset with format:'auto' follows formats.
  • A custom preset whose key equals 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 dates
  • GET /image-previews/config, PUT /image-previews/config
  • GET /image-previews/presets — system + custom presets, effective formats, shadowed keys
  • POST /image-previews/run — start backfill ({"ignoreWindow":true} for an immediate run)
  • POST /image-previews/run/cancel
  • GET /image-previews/disk-usage — previews vs originals vs total
  • POST /image-previews/generate — one file ({fileId, keys?, formats?, dryRun?})
  • GET /image-previews/resolve-plan?fileId=&key= — predicted cache filename, no write
  • GET /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. See dev/README.md.
  • Fail-open: entrypoints never throw into Directus core.
  • Rollout: build → verify on staging → back up previous dist/ → deploy → healthcheck → enable. Rollback: restore previous dist/.
  • Scripts: dev/deploy-staging.sh, dev/rollback-staging.sh, template dev/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.