Appearance
Managing media (admin)
In the admin: Media — the screen this page documents (opens in the running app).
What/why: Content & media — what this screen manages and why it exists.
The Media library screen under /admin/media and the Media panel on every entity editor are where data editors attach images and videos to the product knowledge base (#182): systems, attributes, an enum attribute's allowed values, selection factors and their options, tracks, and right-sizing rules. All media management requires the data editor capability (edit_data) — including media on right-sizing rules, whose rule editing is separately gated by edit_rules. See admin-access.md for how sign-in and roles work.
Everything here is enforced server-side: the UI reflects your capability, but every endpoint re-checks it, so a hand-crafted request can't bypass the guard. Every change (upload, add-video, attach, detach, reorder, set-primary, set-usage, update, delete, set-display-mode) is recorded in the append-only audit trail under the media entity type.
The model: one library, many attachments
Media lives in a reusable library. An asset is uploaded once and can be attached to any number of entities — the same hero image can sit on a system and a track. Detaching removes only that attachment; the asset stays in the library. Two types:
- Images — uploaded files (JPEG, PNG, or WebP, max 10 MB, both edges at least 200 px). Originals are stored privately with EXIF/location metadata stripped; pages always render resized, modern-format renditions (AVIF/WebP with JPEG fallback) served from
/media/<id>/<variant>.<format>. - Videos — remote embeds by URL (YouTube, Vimeo, or Wistia). Paste any normal watch/share link; it is normalized and embedded with the provider's privacy-friendly player. Nothing is uploaded, and only these three providers are accepted. (Uploading video files via Cloudflare Stream was considered and explicitly deferred — see #182, decision 4.)
Alt text is required
Attaching or editing an image requires alt text — what a screen reader announces. The one exception is the explicit Decorative checkbox, which records the image as deliberately unlabeled (rendered with empty alt so assistive tech skips it). Pick one or the other; an upload with neither is refused.
The Media panel on entity editors
Each editor (system, attribute, factor, right-sizing rule, track) ends with a Media panel; on the factor editor each option also has its own collapsible media panel — option imagery is often the visually meaningful choice aid in the guided flow. Likewise, on a multiple-choice attribute's editor each allowed value has its own collapsible media panel (an education point for that choice — see Managing attributes); renaming a value carries its attachments along, and removing one detaches them while the assets stay in the library. The panel shows the entity's gallery in display order:
- Upload image / Add video URL / Pick from library — the shared media chooser (#1697): the same three-button cluster is the way media is picked everywhere in admin — entity galleries, the factor education-media card, and page-chrome slots all render it identically. Upload and Add-video create the asset (one shared ingestion path, same audit records and rights defaults on every surface); Pick from library reuses an existing asset instead of re-uploading. On a gallery panel the chosen asset is attached in one step.
- ↑ / ↓ — reorder the gallery (position is saved immediately).
- Set primary — mark the entity's hero media. The first attachment becomes primary automatically, and detaching the primary promotes the next item, so a non-empty gallery always has exactly one.
- Use as thumbnail — designate the attachment a theme should use where it renders the entity small (a step thumbnail, distinct from the step's hero) — see Media roles. Like primary, the designation is single-holder: picking a new thumbnail moves it. Clear thumbnail removes the designation; surfaces then fall back to the primary, so designating is always optional.
- Edit — alt text / decorative flag, title, caption, credit.
- Detach — remove from this entity only (the asset stays in the library).
- Display — per-gallery Grid, Slideshow, or Single image rendering (default Grid). The slideshow is keyboard-navigable and never auto-advances (safe under reduced-motion preferences). Single image shows only the gallery's primary image (set one with Make primary; the first attachment is used when none is flagged) — handy for an entity whose media is one hero shot rather than a set.
Galleries hold at most 12 items; the panel shows the count and disables adds when full.
Media roles: primary, thumbnail, education media, illustration
An entity's media can carry four distinct roles, each with its own home and its own purpose — don't conflate them (#1696):
| Role | Where it lives | What it's for | Fallback when unset |
|---|---|---|---|
| Primary (main image) | The gallery attachment's primary flag | The entity's hero / background image — buyer cards, headers, and the step's full-bleed backdrop, independent of the education unit | First image in the gallery |
| Thumbnail | A gallery attachment usage designation | The entity rendered small — e.g. a guided-flow step's thumbnail, distinct from its hero | The primary |
| Education media | The factor's own field (Overview tab, #1679) | The teaching visual (image or video) paired with the step's Education text | None — the unit renders text-only (the main image is never pulled in) |
| Illustration | Results page role in the panel (factor options / attribute values) | Which image illustrates the value in the recommendation page's contextual slots | No illustration renders |
Usage designations (#1696) are the extensible part of this model: v1 defines thumbnail, and future roles (designated per attachment, resolved with the same primary fallback) need only a vocabulary entry and an admin control — no schema change. Unknown usage values are rejected loudly at the admin boundary. Only rights-cleared assets reach buyer surfaces regardless of role; a designation on an un-cleared asset simply doesn't render publicly (the panel badges this).
The library screen (/admin/media)
The library lists every asset with its type, dimensions, and usage count. Search (filename / alt / title) and the source (App / Drupal), type, and usage filters all run server-side, one page at a time, so the screen scales to the tens of thousands of assets the Drupal ingest can bring in — see the media library query contract. A Sort control (newest / oldest / name A–Z / name Z–A) also runs server-side, so it holds across pagination (#473). Click any image thumbnail to enlarge it in a lightbox (with its alt/title, dismissible by Escape or a click outside) — so you can actually see an asset before attaching it. The same sort + click-to-enlarge are available in the Pick from library picker on entity editors.
On collapse-enabled surfaces (the factor/attribute illustration picker, #1276) a shot group shows only its primary frame — flagged primary with a shot group · N badge — so you don't scroll past five near-identical frames of one subject. Show N more expands the group in place to reveal its sibling frames without leaving the picker, and Make primary on any sibling swaps which frame represents the group in results (the change is audited and takes effect immediately). The main /admin/media library is not collapsed — it lists every asset for management.
The Unused only usage filter shows orphans — assets attached to nothing — which is the cleanup surface: retire-and-restore of entities never touches their media, so a gallery survives retirement, but an asset you detached everywhere lingers as unused until deliberately deleted.
Delete permanently removes the asset and its stored file. If the asset is still attached anywhere, the delete is refused with a confirmation that names the usage count — confirming detaches it everywhere first. This is deliberate friction: deletion is the one destructive media operation.
The per-asset edit page (/admin/media/[id])
Each library tile has an Open button that goes to the asset's own editor at /admin/media/<id> — one place to see and correct everything about a single media item (#1239). The inline Edit panel on the library remains for a quick metadata tweak; Open is the full editor. It is organised into tabs:
- Details — preview, filename, type, dimensions, source (App / Drupal, with the Drupal ref), and the editable metadata (title, alt text, decorative, caption, credit). EXIF/orientation is stripped on ingest, so it is not stored per asset.
- Classification — the system / project / attribute / factor / application tag axes (#1112). Add or remove a tag on any axis; an admin edit stamps provenance human, and each tag shows a badge for where it came from (human / dump).
- Shot group — group near-identical frames of the same subject (open/closed, day/night, a detail crop) so they share one classification and differ only by a per-frame variation. Shows membership, cover frame, variation and order for a grouped asset; finds and confirms suggested groups (filename stem or near-dup) for an ungrouped one. Each suggested frame renders a thumbnail (a video shows its poster) so you can see a candidate before confirming, and near-dup clusters show a tightness % — the weakest similarity in the cluster — so a mis-clustered frame is catchable at a glance. Tick the frames you want (a select-all toggle sets the whole cluster); only checked frames are grouped, and the asset you are editing is always included. Create stays disabled until at least two frames are selected. Use each frame's Open ↗ link to inspect that asset in a new tab without losing your current selection — handy for seeding a different group from the same frames (each asset belongs to exactly one group, so confirming a subset consumes those frames). At create-time you can also pick the cover frame (the cover radio on any checked frame; defaults to the asset you're editing) and tag each frame's variation state (open/closed/day/night/…) — so a group is born correct instead of needing post-creation edits (the per-frame cover / variation edits stay available on a grouped asset). See the shot-group model (#1240, #1272, #1274).
- AI suggestions — pending AI-proposed tags with Accept / Reject, the same review path as the library's visual-tag chips (#1370; see media tagging). Accepting promotes a tag to a live classification; rejecting is remembered so the analysis pass won't re-propose it.
- Rights — the usage-rights posture (only
clearedpublishes to buyer surfaces), photographer, credit, license, and credit-required (#1115). - Where used — every entity that references the asset, deep-linked to its admin editor where one exists, or
Nonewhen the asset is unused.
App vs Drupal media, and importing (#384)
The App / Drupal tabs split the library by source. App media is uploaded (or imported) and stored in the app's R2. Drupal media is referenced — it displays straight from images.nanawall.com and the app stores no copy, so a Drupal-scale sync stays cheap. A referenced row shows a referenced marker.
Import to app on a referenced row downloads the source bytes into the app's storage and switches that asset to the App side — use it when you want the app to own a specific image (it stops depending on the source origin). Import is idempotent: importing an already-imported asset does nothing. See the referenced-media contract for the model and the display resolver.
Serving details (for developers)
- Renditions:
/media/<assetId>/<spec>where spec is a named variant (thumb/card/hero/full) or ladder width (w160…w2000) plus a format (avif/webp/jpg), e.g./media/abc/card.webp. Off-ladder widths 404. - Responses are immutable-cached for a year; a changed image means a new asset id, never an overwrite.
- Components:
ResponsiveImage(AVIF → WebP → JPEG<picture>),VideoEmbed,MediaFrame(the shared framing convention), andMediaGallery(grid/slideshow/single) insrc/lib/media/. How these present media on the public site is documented in media presentation. - Tagging: assets can be tagged in-app (system-first) so the library is findable by product — see media tagging.
- Buyer-facing surfaces (#245): the comparison view header shows each system's primary image; the guided-flow results show the lead recommendation's full gallery (your grid/slideshow choice applies there) and a primary-image thumbnail per alternative. The primary attachment you pick in the manager is what buyers see first — if the primary is a video, buyer stills fall back to the first image in position order.