# Media - **Route:** `/media` (verified users) - **API:** GET /v1/media/ · POST /v1/media/ · PATCH /v1/media/{id} · DELETE /v1/media/{id} · POST /v1/media/merge · GET /v1/media/{id}/usage - **Visual reference:** `source/RenewCMS Screens Back-office.dc.html`, group "Media", drawn by `source/MediaFrame.dc.html` (open it in a browser; each state is drawn at desktop and mobile, with a light/dark toggle). The library of every uploaded image. It replaces "images belong to one article": an image belongs to the library and can be used in any number of articles. Deleting an article never deletes images. The sidebar gains **Media** (`images` icon) between Articles and Users, breadcrumb "Media". New route for verified users, in the sidebar between Articles and Users with the images icon. Images belong to the library, not to an article: one image can be used in many articles, and deleting an article never deletes images. Uploads from this page are not cropped or resized, and the file is stored as chosen. "Used in" is read from article bodies each time the list loads. Duplicates are images with identical content, whatever their filenames. Alt text is not stored on the image; it is set when inserting into an article. Search, filter, sort and view are stored in the query string. Keyboard: grid items are buttons in one tab stop each; Enter opens the detail panel, Space toggles selection, and Escape closes the panel or clears the selection. Selection changes are announced ("north-wall.jpg selected, 1 selected"). ## Layout Header: title "Media", subtitle "Every image you've uploaded, ready to use in any article.", primary "Upload" button (`upload` icon) on the right. On mobile the button is a full-width 48 px button pinned to the bottom (it turns into "Try again in 0:42" while rate limited, and is replaced by the bulk bar while items are selected). Toolbar: tabs **All**, **Unused** and **Duplicates** (count badge of duplicate groups, only while groups exist), then search ("Search by filename"), sort and the grid/list toggle. Sort options: Newest (default), Oldest, Name, Largest. Below md the toolbar stacks: search, tabs, then sort and toggle. Grid (default): 5 columns on desktop, 2 on mobile, 16 px gap (10 px on mobile). Each card is one button: 4:3 thumbnail (checkerboard behind transparent PNG and SVG), filename (medium, ellipsis), `size · dimensions` (SVG dimensions read "Vector"), then badges. The accessible name reads "north-wall.jpg, 3.9 MB, used in 2 articles" or "…, unused". A "Select all" row with the image count sits above the grid. | Badge | Look | |---|---| | Unused | warning-soft fill, `unlink` icon | | Duplicate | info-soft fill, `copy` icon | | Used in N | outline, toned text | List view: table from md up, columns Filename (with 40 px thumbnail and Duplicate badge), Dimensions, Size, Type, Used in (underlined count, opens the detail panel), Uploaded by, Uploaded (sorted, `arrow-down`); 56 px rows, 36 px header, first column is the checkbox. Below md it is a card list with a 44 px checkbox column, a 56 px thumbnail, filename, `size · dimensions · type`, badges plus `uploader · date`, and a trailing `chevron-right`. Selection: selected items take the sand fill (`--rc-sand`) with a primary outline. The checkbox shows on hover, on keyboard focus, on mobile, and whenever anything is selected. The bulk bar is sticky at the bottom (400 px wide on desktop, full width above the bottom edge on mobile): inverted fill, "3 selected", a danger "Delete" button (`trash-2`) and an inverted "Clear". Detail panel: right-hand slideover 460 px wide on desktop (`x` to close), full screen on mobile with an `arrow-left`. Title is the filename. Body: 16:10 preview (4:3 on mobile), actions Copy URL (`link`), Rename (`pencil`), Delete (`trash-2`, error text), an info alert for duplicates, a metadata list (Type, Size, Dimensions, Uploaded, Uploaded by, URL in mono), then the "Used in" section. Detail panel copy: | Where | Copy | |---|---| | Duplicate alert | "Identical to 2 other images" / "floorboards-copy.jpg and IMG_2208.jpg have the same content." / action "Review duplicates" (`arrow-right`) | | Used | "Used in 2 articles", then one row per article: title (underlined link to the editor), badge "Online" (`globe`, inverted) or "Draft" (`pencil-line`, outline), `arrow-up-right`. Footnote: "Counted from the text of your articles each time you open this page." | | Not used | "Not used in any article" / "This image is safe to delete. No article shows it." | ## Dialogs All dialogs use the existing ConfirmDialog look (`UModal`, `sm:max-w-md`, bottom sheet on mobile, cancel first). While a request runs the confirm button shows a `loader-circle`, both buttons are disabled and the dialog cannot be dismissed. A failure shows an error alert at the top of the dialog body and the dialog stays open. | Dialog | Title | Description | Body | Cancel / Confirm (pending) | |---|---|---|---|---| | Rename | "Rename image" | "Only the filename changes. The URL stays the same, so articles that use this image keep working." | Field "Filename" with the extension fixed as a mono suffix (`.jpg`); help "Letters, numbers, dashes and underscores. The .jpg ending stays."; error "Enter a filename." on empty or only symbols | "Cancel" / "Rename" ("Renaming…") | | Delete, unused | "Delete this image?" | ""window-frame.jpg" will be deleted from your library. No article uses it. This can't be undone." | none | "Cancel" / "Delete image" ("Deleting…"), danger | | Delete, in use | "Delete an image that's in use?" | ""north-wall.jpg" is used in 2 articles. If you delete it, they will show a broken image. This can't be undone." | list of affected articles with Online or Draft badges | "Cancel" / "Delete image" ("Deleting…"), danger | | Bulk delete | "Delete 3 images?" | "2 of them are used in 4 articles, which will show a broken image. The other one isn't used anywhere. This can't be undone." | one row per image: filename and "Used in N" or "Unused" | "Cancel" / "Delete 3 images" ("Deleting…"), danger | | Merge | "Merge into floorboards.jpg?" | "Articles that use the other 2 copies will be switched to floorboards.jpg, and the other 2 copies will be deleted. 5 articles will be updated. This can't be undone." | the kept image: thumbnail, filename, "Kept · used in 4 articles now" | "Cancel" / "Merge 2 copies" ("Merging…"), danger | Dialog errors: rename "Couldn't rename the image. Your text is still here. Try again in a moment."; delete "Couldn't delete the image, so it's still there. Try again in a moment."; merge (500) "Couldn't merge the copies. Nothing was changed. Try again in a moment."; merge (409) "One of these images changed, so nothing was merged. The list is up to date now. Check it and try again." ## Upload panel A panel above the images while files are in flight and until dismissed. Header: "Uploading 6 images" with "2 of 6 done" and "Cancel all" (after it settles: "Upload finished" / "5 images" and "Dismiss"; if nothing was added: "Nothing was uploaded"). Files upload two at a time, one row each: 40 px thumbnail, filename, size, then per status: | Status | Row | |---|---| | Waiting | "Waiting" | | Uploading | "Uploading 1.1 of 2.6 MB" with a 4 px progress bar and a cancel `x` | | Added | `circle-check` "Added to your library" | | Duplicate | "Already in your library" / `Same as "floorboards.jpg", uploaded 14 Jun 2026 by Inès Duarte.` / "Use existing" and "Upload anyway" | | Rejected | `file-x` title, then "Images can be up to 10 MB. Use a PNG, JPEG, WEBP or SVG image." Titles: `scan.tif is 14.2 MB` (too large), `notes.pdf isn't an image` (type), `broken.png couldn't be read` with "The server couldn't read this image. Save it again as a PNG or JPEG, then try again." (400) | | Failed | `circle-x` "Upload failed" / "The server didn't accept the file. Nothing was added to your library." / "Try again" (`rotate-cw`) | Summary line when settled: "2 uploaded · 1 already in your library · 2 need attention". Type and size are checked in the browser before anything is sent (PNG, JPEG, WEBP, SVG; 10 MB), by MIME type and by extension, exactly as the editor's uploader does. Dropping files onto the page shows a dashed primary-bordered area "Drop images to upload" / "PNG, JPEG, WEBP, SVG · max 10 MB each". ## Duplicates view Groups stacked with 16 px gap. Each group is a bordered card: header "Group 1" and "3 identical images · 2.4 MB each · 3600 × 2400"; one column per copy (stacked on mobile) with thumbnail, filename, "Uploaded 14 Jun 2026 by Inès Duarte", underlined "Used in 4 articles", and a "Merge into this one" button (`merge` icon). The suggested copy has the sand fill, a primary outline, a "Suggested" tag and a primary button; the others are outline. Footer: "Suggested: the most used copy, then the oldest. Choosing another copy switches articles to it." Empty: `copy` icon, "No duplicates" / "Every image in your library is different." / "Back to all images". ## Other copy | Where | Copy | |---|---| | Empty library | `images` icon, "No images yet" / "Upload your first image. You can use it in any article." / primary "Upload an image"; under it "PNG, JPEG, WEBP, SVG · max 10 MB each" | | No results | `search` icon, `No images match "wall"` / "Nothing matches in Unused. Check the spelling, or clear the filters." / "Clear filters" | | List error | error alert "Couldn't load your images" / "Check your connection and try again." / "Try again" | | Rate limited | info alert `timer` "Too many uploads" / "For your security, wait a moment before uploading again."; the Upload button reads "Try again in 0:42" and is disabled | | Offline banner | `wifi-off` "You're offline. You can look through the images that are loaded, but uploading, renaming and deleting need a connection." | Toasts (success 5 s unless noted): | Trigger | Icon, title, description | |---|---| | Copy URL | `circle-check` "URL copied" / "Paste it wherever you need the image address." | | Rename | `circle-check` "Image renamed" / `It's now "north-wall-01.jpg". The URL is the same.` | | Delete | `circle-check` "Image deleted" / `"window-frame.jpg" is gone from your library.` | | Bulk delete | `circle-check` "3 images deleted" / "They're gone from your library." | | Bulk delete, partial | error, stays until retried or dismissed: `circle-x` "Couldn't delete 1 image" / "2 images were deleted. window-frame.jpg is still in your library. Try again." / "Retry" | | Merge | `circle-check` "Copies merged" / "2 copies merged. 5 articles updated." | | 404 | info `info` "That image no longer exists" / "It was deleted somewhere else, so we removed it from the library." | ## Status to copy | Status | Where | Copy | |---|---|---| | 200 / 201 | Upload, rename, merge | Upload: row reads "Added to your library". Rename and merge: toasts above. | | 204 | Delete, bulk delete | Toasts above. | | 400 | Upload row | "broken.png couldn't be read" / "The server couldn't read this image. Save it again as a PNG or JPEG, then try again." In a dialog: "Something in the form isn't valid. Check the highlighted fields." | | 401 | any | Global: /login?redirect=/media&reason=expired. No local copy. | | 403 | any, unverified | Global: /register/pending. | | 404 | rename, delete, merge, panel | The info toast above; the panel or dialog closes. | | 409 | merge | Dialog alert above. | | 413 | upload row | "scan.tif is 14.2 MB" / "Images can be up to 10 MB. Use a PNG, JPEG, WEBP or SVG image." | | 415 | upload row | "notes.pdf isn't an image" / same description. | | 429 | upload | Rate-limited alert above, counting down Retry-After. | | 500 / network | list, upload row, rename, delete, merge | The copy for each place above. | | Offline | whole page | Banner above. | ## Keyboard and accessibility Grid items are buttons, one tab stop each. Enter opens the detail panel, Space toggles selection, arrow keys move focus across the grid, Escape closes the panel or clears the selection. Focus returns to the item when the panel closes, and moves to the next item after a delete. Selection changes are announced ("north-wall.jpg selected, 1 selected"), as are upload completion and the merge result. Search, filter, sort and view live in the query string. ## States | State | Trigger and behaviour | |---|---| | Grid, many images | Default view. 12 images, newest first. Each item is a button labelled like "north-wall.jpg, 3.9 MB, used in 2 articles". Badges: "Unused" (warm), "Duplicate" (info), "Used in N" (outline). The Duplicates tab carries a count of groups only while duplicates exist. | | Grid, few images | Three images. Cards keep their size instead of stretching. Checkerboard shows behind the SVG. | | List view | The list icon in the toggle. A table from md up (Filename, Dimensions, Size, Type, Used in, Uploaded by, Uploaded) and a card list below md. The "Used in" count opens the article list in the detail panel. | | Loading | The first load, a retry, or a change of search or filter. Skeleton cards appear after 150 ms. | | Empty library, first run | Zero images. Toolbar and tabs are hidden; one call to action. Copy: "No images yet" / "Upload your first image. You can use it in any article." / "Upload an image". | | No results | Search or filter matched nothing. "Clear filters" resets the search and goes back to All. | | Hover, focus and keyboard | Hover (second item) darkens the border, adds the pop shadow and reveals the checkbox. Keyboard focus (third item) shows the 2 px primary ring with a 2 px offset and reveals the checkbox. Tab moves between items, Enter opens the detail panel, Space toggles selection, and arrow keys move focus across the grid. | | Selection and bulk bar | Checkbox on any item, or Space. Selected items take the sand fill with a primary outline, and Select all shows a partial state. The bar is sticky at the bottom (above the bottom edge on mobile, replacing the Upload button). Announced: "3 selected". Escape or Clear empties it. | | Bulk delete: confirm | Delete in the bulk bar. The dialog says how many of the selection are in use and lists them. The confirm button carries the count. | | Bulk delete: in progress | Requests in flight. The dialog can't be dismissed. | | Bulk delete: partly failed | Some requests failed. Deleted images leave the grid, failed ones stay selected, and the toast stays until retried or dismissed. | | Bulk delete: done | 204 for all. Focus moves to the next item. | | Detail panel: in use | Click or Enter on an item. Right-hand slideover on desktop, full screen on mobile with a back arrow. Actions: Copy URL, Rename, Delete. "Used in 2 articles" lists title, Online or Draft, and links to the editor. Focus returns to the item on close. | | Detail panel: unused | Used in nothing. The section reads "Not used in any article" and says the image is safe to delete. | | Detail panel: duplicate | The image has identical copies. An info alert links to the duplicates view. | | Copy URL | Copy URL. Clipboard write succeeded. Success toasts last 5 s. | | Rename: dialog | Rename in the panel. The field holds the name without its extension, and the .jpg ending is fixed. The description says the URL stays the same. | | Rename: saving | PATCH in flight. Field and buttons disabled, dialog can't be dismissed. | | Rename: validation | Empty or only symbols, on submit or blur. Nothing is sent. | | Rename: failed | 500 or network failure. The error shows inside the dialog. | | Rename: done | 200: the panel title and list update in place. | | Delete: unused image | Delete in the panel. Plain confirm. Says what happens and that it can't be undone. | | Delete: image in use | Stronger confirm. Lists the affected articles and states that they will show a broken image. | | Delete: in progress | DELETE in flight. The dialog can't be dismissed. | | Delete: failed | 500 or network failure. The error shows inside the dialog, as in the article delete. | | Delete: done | 204: the panel closes, the item leaves the grid and focus moves to the next item. | | Image already gone (404) | Any action on an image that was deleted elsewhere. The panel closes and the item leaves the list. | | Upload: drag over the page | Files dragged onto the page. A dashed drop area covers the content. Dropping starts the upload straight away; there is no crop step on this page. | | Upload: several files, mixed | Upload button or drop, several files. Each file has its own row and progress. Files upload two at a time. A row can end as added, "Already in your library", rejected in the browser, or failed. Rejected and failed rows stay until dismissed. | | Upload: summary | All rows settled. The summary counts each outcome. "Use existing" selects the existing image and drops the row; "Upload anyway" stores a second copy, which then appears under Duplicates. "Try again" retries one file. Announced when it finishes. | | Upload: rejected files (413, 400) | Type and size are checked in the browser first, so nothing is sent. A server 413 or 400 for a file that got through shows the same row with its own message. | | Upload: too many requests (429) | Rate limited. Retry-After is counted down in the button. It re-enables at zero. Already-added files are kept. | | Duplicates: groups | The Duplicates tab. Groups of identical images side by side, each with its uploader, upload date and "Used in N articles". The suggested copy to keep is the most used, then the oldest. | | Duplicates: empty | No groups. The Duplicates tab loses its count badge. | | Merge: confirm | "Merge into this one" on floorboards.jpg. The dialog says articles using the other copies will switch to this image and the other copies will be deleted, with the article count. | | Merge: in progress | Request in flight. The dialog can't be dismissed. | | Merge: failed | 500 or network failure. Nothing is switched or deleted. | | Merge: conflict (409) | A copy was deleted, or a new copy appeared, since the page loaded. | | Merge: done | 200: the group leaves the list. When it was the last one, "No duplicates" shows. | | List failed to load | A 500 or network failure on the list request. The alert replaces the images; Try again repeats the request. | | Offline | navigator.onLine is false. Loaded images stay browsable. Upload, Rename, Delete and Merge are disabled, and the banner says why. |