Files
RenewCMS/docs/design/screens/image-variants.md
T

26 KiB
Raw Blame History

Image variants

  • Routes: /settings/media/variants and /settings/media/cache (admins and owners), plus additions to /media and /article/:id/edit
  • API (proposed by the design, to be fixed by the feature spec): GET/POST /v1/image-variants · PUT/PATCH/DELETE /v1/image-variants/{name} · POST /v1/image-variants/{name}/purge · GET /v1/image-variants/cache · POST /v1/image-variants/cache/purge · GET /v1/media/{id}/variants · POST/DELETE /v1/media/{id}/variants/{name}
  • Public contract: <public image URL>?v=<name>, see section 7
  • Visual reference: source/RenewCMS Screens Image variants.dc.html, drawn by source/VariantsFrame.dc.html inside the AppFrame.dc.html back-office frame (open it in a browser; each state is drawn at desktop and mobile, with a light/dark toggle). Every frame is numbered; the numbers below match.

Named variants of an original image. The original is never changed. An admin defines variants (a name, a width and/or height, a fit, a format, a quality). A request for an image with ?v=<name> generates the variant on first use, caches it, and serves the cached file afterwards. Only variants that exist and are enabled are served: anything else is a 400. There is no arbitrary-size mode and no raw width, height, format or quality parameter.

Vocabulary: "variant", "generated", "purge". Delete removes a variant definition; purge removes only its generated files. Never "thumbnail preset", "version" or "rendition".

Variant fields: name (slug, 2 to 32 lowercase letters, numbers and dashes, unique, immutable after creation), width and height (1 to 8000 px, at least one required, images are never enlarged), fit (cover, contain, inside), format (original, JPEG, PNG, WebP; no AVIF), quality (1 to 100, hidden for PNG), generate on upload, enabled.

1. Settings → Media → Variants

  • Route: /settings/media/variants
  • API: GET /v1/image-variants · PATCH /v1/image-variants/{name} · DELETE /v1/image-variants/{name} · POST /v1/image-variants/{name}/purge (proposed)

Admin-only page under Settings → Media, with two tabs: Variants and Cache. Desktop shows a table; mobile stacks each variant as a card. The status switch always pairs with an icon and the words Enabled or Disabled. The reference card "How requests are handled" sits under the table (section 7 of the brief).

State Name Trigger
1a Loaded Six variants, one of them disabled (print).
1b Loading Skeleton rows appear after 150 ms.
1c Empty No variants. Two templates pre-fill the Add variant form.
1d Error loading A 500 or network failure on the list request.
1e Forbidden A signed-in user who is not an admin opens the URL.
1f Row menu The ellipsis button on a row.
1g Row action in progress Purge cache was confirmed on hero. The row is dimmed and its controls are disabled.
1h Purge confirm Purge cache in the row menu.
1i Delete confirm Delete in the row menu.
1j Delete in progress Delete was confirmed. The button shows a spinner.
1k Request reference Mobile scrolled to the reference card. On desktop it is visible below the table.

Copy: Variants list

State Where Copy
Header Title, description, button Image variants · Named sizes and formats generated from your original images. Originals are never changed. · Add variant
Table columns Header row Name · Dimensions · Fit · Format · Quality · Generated · Cache size · Status
Dimensions Cell 400×300 · 800 × auto · auto × 2400
Quality Cell 80 · — (PNG has no quality)
Status Switch + text Enabled · Disabled (aria-label: "Enable variant hero")
Row menu Menu items Edit · Purge cache · Delete
Footer summary Under the table 6 variants · 1,284 generated files · 212 MB
Footer summary (singular) One variant 1 variant · 1 generated file · 18 KB
Loading Skeleton No text. Region announces "Loading variants".
Empty Title · text No variants yet · A variant is a named size that an image can be requested in, like ?v=thumb. Start with one of these.
Empty Template cards thumb · 400×300 · cover · WebP — hero · 1200×630 · cover · WebP — Use template
Error Alert Couldn't load variants · Check your connection and try again. · Try again
Forbidden Title · text · button You don't have access · Only admins can manage image variants. Ask an admin if you need one changed. · Back to Home
Row in progress Status cell Purging…
Purge success Toast Purged 244 files from “hero”
Purge dialog Title · text · buttons Purge cache for “hero”? · This removes 244 generated files (84 MB). Files are generated again on the next request. · Cancel · Purge 244 files
Delete dialog Title · text · buttons Delete variant “hero”? · This removes the variant and its 244 generated files (84 MB). Articles that use ?v=hero will start returning 400. This can't be undone. · Cancel · Delete variant
Delete success Toast Variant “hero” deleted
Request reference Card How requests are handled · Body of a 400 response · unsupported_parameter replaces unknown_variant when a raw parameter is sent. There is no arbitrary-size mode.

2. Add / edit variant

  • Route: Slide-over on desktop · full page on mobile
  • API: POST /v1/image-variants · PUT /v1/image-variants/{name}

Form on the left, live preview on the right (stacked on mobile). The preview uses a sample image with a before/after divider. The divider is a slider: arrow keys move it by 1%, Shift+arrow by 10%, Home and End jump to the ends. Name is locked in edit mode. The edit warning is always visible in edit mode. Preview images are placeholders.

State Name Trigger
2a Create: pristine Add variant just opened. Save is disabled.
2b Create: dirty Valid values. The divider has keyboard focus. Mobile is scrolled to the preview.
2c Field errors On blur and on submit. Name format, width above 8000, quality above 100.
2d No dimension Width and height are both empty on submit.
2e Name already taken POST returns 409.
2f Saving Request in flight. Fields and buttons are disabled.
2g Save error A 500 or network failure. Values are kept.
2h Edit: pristine Opened from Edit. Name is locked. Save is disabled.
2i Edit: dirty Quality changed from 82 to 70.
2j Unsaved changes Close, Cancel or Escape while dirty.
2k PNG: no quality Quality is hidden when the format is PNG.

Copy: Add / edit variant

State Where Copy
Titles Slide-over header Add variant · Edit variant: hero
Name Label · placeholder · help Name · e.g. hero · Lowercase letters, numbers and dashes. Can't be changed later.
Name (edit) Help The name can't be changed.
Name error: format Inline Use lowercase letters, numbers and dashes, 2 to 32 characters.
Name error: taken Inline A variant named “thumb” already exists. Choose another name.
Width · Height Label · suffix · placeholder · help Width · Height · px · auto · Set a width, a height, or both. 1–8000 px. Images are never enlarged.
Dimension error Inline Set a width, a height, or both.
Dimension range error Inline Enter a value from 1 to 8000.
Fit Options · help Cover: Fills the box and crops what overflows. · Contain: Whole image inside the box, padded to the box size. · Inside: Whole image inside the box, no padding.
Format Options · help Original · JPEG · PNG · WebP · Keeps the format of each source image.
Quality Label · help · error Quality · 1–100. Higher is sharper and larger. · Enter a value from 1 to 100.
Toggles Label · help Generate on upload: Creates this variant when an image is uploaded. · Enabled: Disabled variants return 400 on the public site.
Edit warning Alert (edit only) Changing dimensions, fit, format or quality purges 244 generated files; they will be regenerated on next request.
Preview Heading · labels · hint Preview · Sample: north-wall.jpg · Original — 4032 × 3024 · 3.9 MB · hero — 1200×630 · about 96 KB · Drag the divider, or focus it and use the arrow keys. The estimate comes from a sample; output is never larger than the source.
Preview (no size) Over the image Set a width or height to see a preview.
Slider aria-label · aria-valuetext Compare original and hero · 50% hero
URL Label · button URL · /media/north-wall.jpg?v=hero · Copy · Copied
Footer Status · buttons Unsaved changes · Cancel · Add variant · Save changes · Saving…
Save error Alert Couldn't save the variant · Check your connection and try again. · Try again
Save success Toast Variant “hero” added · Variant “hero” saved. 244 files purged.
Unsaved dialog Title · text · buttons Discard changes? · You have unsaved changes to “hero”. If you close now, they're lost. · Keep editing · Discard changes

3. Media library

  • Route: /media
  • API: GET /v1/media?variants=with|without&sort=total_size (proposed)

The existing grid with three additions: a "N variants" badge on cards that have cached files, an All / With variants / Without filter, and the sort option "Largest total size (original + variants)". When sorted by total size, the meta line shows the total.

State Name Trigger
3a Grid with badges Default view. Images with no cached variants have no badge.
3b Sort menu The sort select is open. The new option is last.
3c Filter: With variants, sorted by total size The meta line becomes total · original + variants.
3d Filter: Without variants Images no variant has been generated for.
3e Filter: nothing to show With variants, and the cache is empty.
3f Delete confirm Delete on an image that has cached variants.

Copy: Media library additions

State Where Copy
Badge Card 4 variants · 1 variant
Filter Segmented control All · With variants · Without
Sort Select, new option Largest total size (original + variants) · mobile label: Largest total size
Meta line (sorted by total) Card 4.2 MB total · 3.9 MB + 260 KB
Empty filter Title · text · button No images with variants · Variants are generated when an image is requested with ?v=, or from its Variants tab. · Show all images
Delete dialog Title · text · note · buttons Delete this image? · "north-wall.jpg" is used in 2 articles. If you delete it, they will show a broken image. This can't be undone. · Also removes 4 generated variants (260 KB) · Cancel · Delete image
Bulk delete Dialog line Also removes 11 generated variants (1.1 MB)

4. Image detail: Variants tab

  • Route: /media (detail panel)
  • API: GET /v1/media/{id}/variants · POST /v1/media/{id}/variants/{name} · DELETE /v1/media/{id}/variants/{name} (proposed)

The detail panel gets a Variants tab with one row per enabled variant. On desktop the row actions are icon buttons with tooltips and accessible names; on mobile they are labeled buttons on their own line. Only actions that apply are shown: generated rows offer Copy URL, Preview and Purge; others offer Copy URL and Generate now. The tab shows output dimensions as they will be generated, capped at the source size.

State Name Trigger
4a Partly generated Four of five generated. Saving is shown against the original.
4b Nothing generated yet No cached files for this image.
4c Generating Generate all or Generate now was pressed. Rows show a spinner.
4d Generation failed The row shows the cause and Retry. Both causes are shown together for reference.
4e No variants defined There are no enabled variants.
4f Source is an SVG Variants aren't generated for vector images.
4g Source is an animated GIF Same rule as SVG.
4h Preview dialog Preview on a generated row. Both at actual size.

Copy: Variants tab

State Where Copy
Tabs Panel Details · Variants
Original Header block Original · kept untouched · north-wall.jpg · JPEG · 4032 × 3024 · 3.9 MB
Summary Above the list 4 of 5 generated · Nothing generated yet. · Generating variants…
Generate all Button Generate all · Generating…
Row status Badge Generated · Not generated · Generating · Failed
Row meta (generated) Under the name 400×300 · WebP · 18 KB · -99% vs original
Row meta (not generated) Under the name 400×300 · WebP
Row actions Tooltip · aria-label Copy URL · Preview · Generate now · Purge · Retry
Failed: cause Row line Unsupported source format · Image too large
No variants defined Title · text · link No variants defined · Define a variant in Settings to get resized copies of this image. · Settings → Media → Variants
SVG / GIF Alert Variants aren't generated for this format. · SVG images are served as they are. · Animated GIFs are served as they are.
Preview dialog Title · panes · hint Preview: thumb · thumb · 400×300 · WebP — 18 KB · saves 99% · Original · 4032×3024 · JPEG — 3.9 MB · Both images are shown at actual size. Scroll to pan. · Close
Toasts After actions Generated 5 variants · Generated thumb · Purged 4 files (260 KB) · Couldn't generate hero. Image too large.

5. Settings → Media → Cache

  • Route: /settings/media/cache
  • API: GET /v1/image-variants/cache · POST /v1/image-variants/cache/purge {scope: all|unused|variant|image} (proposed)

Stat tiles, a per-variant bar list (bar length is the disk size relative to the largest variant) and the ten images with the most variant disk usage. Purge all needs the word "purge" typed. Purge unused removes files of enabled variants not requested in 30 days.

State Name Trigger
5a Loaded Default view.
5b Loading Tiles and lists show skeletons.
5c Empty cache Nothing generated. Purge buttons are disabled.
5d Purging Progress for a purge in progress. Content is dimmed.
5e Purge done The cache is empty and a toast reports the result.
5f Purge failed Part of the purge did not complete.
5g Error loading A 500 or network failure.
5h Purge all: confirm The word is not typed yet. The button is disabled.
5i Purge all: confirm, typed The input matches "purge".
5j Purge unused: confirm Variants of enabled variants not requested for 30 days.

Copy: Cache panel

State Where Copy
Header Title · text · buttons Cache · Generated variant files. Purged files are generated again on the next request. · Purge unused · Purge all variants
Tiles Label · sub text Generated files · Across 6 variants — Disk used · Originals not included — Variants defined · 5 enabled, 1 disabled — Rejected requests · Last 7 days
Bar list Heading · row By variant · hero 244 files · 84 MB · Disabled (badge on disabled variants)
Top 10 Heading · row · button Top 10 images by variant disk usage · north-wall.jpg · 6 variants · 5.8 MB · Purge
Empty cache Title · text Nothing generated yet · Variants are generated on the first request with ?v=, or from an image's Variants tab.
Purging Banner Purging 1,284 files · 640 of 1,284 · 106 MB freed
Purge done Toast Purged 1,284 files, freed 212 MB
Purge failed Toast Purge didn't finish · 640 of 1,284 files were removed. Try again. · Try again
Error Alert Couldn't load cache stats · Check your connection and try again. · Try again
Purge all dialog Title · text · field · buttons Purge all variants? · This removes 1,284 generated files (212 MB) for every variant. Originals aren't touched. Files are generated again on request. · Type purge to confirm · Cancel · Purge all variants
Purge unused dialog Title · text · buttons Purge unused variants? · This removes 312 files (41 MB) from enabled variants that haven't been requested for 30 days. Files are generated again on request. · Cancel · Purge 312 files
Purge image (row) Toast Purged 6 files (5.8 MB) for north-wall.jpg

6. Editor image picker

  • Route: /articles/{id}/edit
  • API: Inserted URL: /media/{file}?v={name}

The Library tab of the picker gets a Variant select. Options are Original plus the enabled variants, each with its dimensions and format. Once inserted, a block whose variant was deleted or disabled shows an inline warning with a way to fix it. Disabled variants do not appear in the select.

State Name Trigger
6a Variant select An image is selected. The URL line shows what will be inserted.
6b Select open Original plus five enabled variants.
6c No variants defined Only Original is offered.
6d Variant deleted The block keeps its URL. Pick another variant opens the select.
6e Variant disabled Same block, different cause. Proposed copy.

Copy: Editor picker

State Where Copy
Select Label Variant
Options Option label Original · 4032×3024 · JPEG — Thumb · 400×300 · WebP — Card · 800 × auto · WebP — Hero · 1200×630 · WebP — Square · 600×600 · JPEG — Og · 1200×630 · JPEG
URL line Under the select /media/north-wall.jpg?v=hero
No variants Help No variants defined. Add one in Settings → Media → Variants.
Block warning: deleted Inline warning Variant 'hero' no longer exists. This image returns an error on the public site. · Pick another variant
Block warning: disabled Inline warning (proposed) Variant 'hero' is disabled. This image returns an error on the public site. · Pick another variant
Block label Chip on the image hero · 1200×630

7. Public request contract

Shown as the "How requests are handled" card in section 1. Only variants that exist and are enabled are served. There is no arbitrary-size mode.

Request Status Body or result
/media/photo.png?v=thumb 200 The cached image. Generated on first request.
/media/photo.png?v=unknown 400 Unknown variant
/media/photo.png?v=hero (disabled) 400 Unknown variant
/media/photo.png?width=400 400 Unsupported parameter
height, format or quality parameters 400 Unsupported parameter
/media/missing.png?v=thumb 404 Not found
400 body shape { "status": 400, "error": "unknown_variant"

8. Activity log entries

Row copy for the existing activity log. The actor name is bold, the quoted name is the variant name, and the detail line is secondary text.

Event Row copy Detail line
Variant created Marta Alves created variant “hero” 1200×630 · cover · WebP · quality 82
Variant updated Marta Alves updated variant “hero” Quality 82 → 70 · purged 244 files
Variant deleted Marta Alves deleted variant “hero” Removed 244 generated files (84 MB)
Variant enabled Marta Alves enabled variant “hero” Requests for ?v=hero are served
Variant disabled Marta Alves disabled variant “hero” Requests for ?v=hero return 400
Cache purged: all Marta Alves purged all variants Removed 1,284 files (212 MB)
Cache purged: unused Marta Alves purged unused variants Removed 312 files (41 MB) not requested for 30 days
Cache purged: by variant Marta Alves purged the cache of variant “hero” Removed 244 files (84 MB)
Cache purged: by image Marta Alves purged the variants of north-wall.jpg Removed 4 files (260 KB)
Generate all Marta Alves generated all variants of north-wall.jpg 5 variants generated
Generate all, with a failure Marta Alves generated all variants of north-wall.jpg 4 generated · 1 failed (Image too large)
Image deleted Marta Alves deleted north-wall.jpg Also removed 4 generated variants (260 KB)

States matrix

Frame: a numbered state above. Copy: covered by the copy tables, with no separate frame. Existing: unchanged behavior of the existing screen.

Screen Loaded Empty Loading Error Forbidden In progress Confirm Validation Success
1 Variants list Frame 1a Frame 1c Frame 1b Frame 1d Frame 1e Frame 1g, 1j Frame 1h, 1i – Copy
2 Add / edit Frame 2a, 2h – – Frame 2g Copy Frame 2f Frame 2j Frame 2c, 2d, 2e Copy
3 Media library Frame 3a Frame 3e Existing Existing – – Frame 3f – –
4 Image detail Frame 4a Frame 4b, 4e, 4f, 4g Existing Frame 4d – Frame 4c – – Copy
5 Cache Frame 5a Frame 5c Frame 5b Frame 5g, 5f Copy Frame 5d Frame 5h, 5i, 5j Frame 5h, 5i Frame 5e
6 Editor picker Frame 6a, 6b Frame 6c Existing Frame 6d, 6e – – – – –

Back-office API errors

Shown as a toast, or inline where a field is named. The first line is the title, the second the text. The public media URL does not use these; see section 7.

Status Title Text Where it shows
400 Check the request A value in the request isn't valid. Reload the page and try again. Toast
401 Session expired Sign in again to continue. Redirect to sign in, then a toast
403 You don't have access Only admins can manage image variants. Toast, or the forbidden state on the list
404 Not found This variant or image no longer exists. Refresh the page. Toast
409 Name already taken A variant named “thumb” already exists. Choose another name. Inline under Name
413 Image too large The source image is too large to generate this variant. Row cause in the Variants tab
422 Check the fields Width and height must be between 1 and 8000, and one of them is required. Inline under each field
500 Something went wrong The server couldn't finish. Try again. If it keeps happening, check the server log. Alert or toast with Try again

Component inventory

Existing components are reused as they are. New components are built on Nuxt UI parts and use only existing tokens. Every icon-only button needs an aria-label and a UTooltip.

Component Status Nuxt UI mapping Notes
Variants table New UTable Cell slots for the status switch and actions. Below 640 px it renders as a list of UCard.
Variant row (settings, mobile) New UCard + USwitch + UDropdownMenu Name, spec line, counts, status switch and menu.
Variant row (image detail) New UBadge + UButton + UTooltip Status badge, meta line, contextual actions, error line. Spinner uses UIcon.
Variant form New UForm, UFormField, UInput, USelect, USlider, USwitch, URadioGroup Fit is a URadioGroup, horizontal, 3 options. Quality hides when the format is PNG.
Slide-over / full page New USlideover (desktop) · routed page (mobile) Close with unsaved changes opens the discard UModal.
Split comparison slider New No match. Custom ImageCompare on a native range input Two stacked images, clip-path on the top one. role="slider", arrow keys 1%, Shift 10%, Home and End. Handle ≥ 44 px on touch.
Stat tile New UCard (default slot) Icon, label, value, sub text. Skeleton uses USkeleton.
Bar list New UProgress per row Row with name, stats and a bar. Value is disk size over the largest variant.
Top-N list New UCard + list rows + UButton Thumbnail, name, count, size, Purge.
Type-to-confirm dialog New UModal + UInput Confirm button disabled until the input equals "purge".
Variant select New USelectMenu Item slot shows name, dimensions, format. Only enabled variants.
Variants badge New UBadge (outline, icon layers) Shown on media cards when count > 0.
Filter New UTabs (variant link) or UButtonGroup All, With variants, Without.
Request reference card New UCard + code block Table of responses and a JSON body.
Alerts, toasts, dialogs, empty states, skeletons Existing UAlert, useToast, UModal, UEmpty pattern, USkeleton Same copy patterns as the Media and API keys screens.

Vocabulary and accessibility

Use "variant", "generated" and "purge" everywhere. Never "thumbnail preset", "version" or "rendition". Delete removes a variant definition; purge removes only its generated files. Every status is an icon plus text: Generated (circle-check), Not generated (circle-dashed), Generating (loader), Failed (circle-x), Enabled (circle-check), Disabled (circle-off). The comparison slider has a visible focus ring and works from the keyboard. Dialogs trap focus and return it to the opener. Progress is announced with aria-live="polite". Touch targets on mobile are at least 44 px. The only token not already in the system is --rc-on-sand (dark ink on the sand selection color), used for the active nav item in both themes.

Implementation notes

The board predates two decisions. Where the board and these notes differ, these notes win for behaviour and the board stays the reference for layout and copy structure.

  • The public URL is /static/uploadedImages/<storedName>?v=<name> (image.PublicPathPrefix), not /media/<file>. Every /media/...?v=... string in the copy tables above (URL field, Copy URL, URL line in the picker, request contract rows) shows that URL instead. /media stays the SPA library route.
  • WebP output depends on a CGO-free lossy encoder. If none is viable, the Format select offers Original, JPEG and PNG only.
  • The padding colour of contain is not drawn. The feature spec decides it.