# 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 `__.`. 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 ```bash 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` | `/.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.