Media sources

One library, every forge

The Dashboard's Media section and the editor's image picker show the same media, because both read it through a Media source: one interface, with an implementation for git forges and one for the server forge. An image uploaded in the Media section is waiting in the picker's Choose existing list, and the other way round.

interface MediaSource {
	capabilities: { metadata: boolean; usage: boolean; delete: boolean; search: boolean };
	list(query?: { search?: string; contentType?: string }): Promise<MediaItem[]>;
	upload(file: File, opts?: { title?: string }): Promise<MediaItem>;
	delete(item: MediaItem): Promise<void>;
}

A MediaItem always has an id, a url, a filename and a contentType. Depending on the source it may also carry a size, width and height, a title, uploadedAt and uploadedBy, and usage: the pages that use it. The url is what a Content document stores as an image's src.

Capabilities

A source's capabilities say what it can do, and the Media section shows only that:

  • metadata: items carry a title, size and dimensions, which the grid shows.

  • usage: items list the Content documents that use them.

  • delete: items can be deleted.

  • search: the list can be searched, so the Media section shows a search box.

The two sources differ in exactly these flags.

Git forges: the media dir

On the github and local forges, media is the files in the site's mediaDir, committed like any other file. The source lists, uploads and deletes them through the same forge as pages, so a site that has mediaDir in its site options needs nothing more.

  • Upload shrinks a large photo to WebP until it fits the forge's 1 MB limit, then commits it under a name made from a hash of its bytes. Uploading the same image twice reuses the file already there.

  • Delete is a commit that removes the file.

  • No metadata. A git source keeps no titles and no sidecar files. Alt text belongs on the image block that uses the file, where it can say what the image means on that page.

  • No usage index. Before a delete, the Media section reads the site's Content documents once, looking for the file's served URL. If any page uses it, the confirmation lists those pages and its button reads Delete anyway.

Its capabilities are { metadata: false, usage: false, delete: true, search: false }.

The server forge: the Media library

On the server forge, media lives in the server's Media library, behind the mediaApiBase endpoint the site declares with defineServerSite. The source wraps those endpoints and has every capability. Each item shows its title, filename, dimensions and size, and the pages that use it, and the list can be searched. The server decides what may be deleted. Deleting an item that Content documents still use is refused with MediaInUseError, and the Media section shows the server's message in an alert rather than deleting anything.

Permissions

The Media section asks the Dashboard's can before it offers Upload, with can('media-upload'), and before each Delete, with can('media-delete', item). Without can, both are offered. See Permissions.

Outside the Dashboard

mediaSourceFor(config, session, { staticDir }), from uncial-cms, picks the source for a site config. The server forge needs only the config. A git forge reaches its media through a signed-in editing session, so it also needs that session's adapter and author. The MediaLibrary component from uncial-cms/svelte is the Media section's grid on its own. Give it a source, <MediaLibrary {source} />, and it shows what that source's capabilities allow. It also takes can, confirmDelete and base. The editor's image picker needs no wiring: EditorPage, mountEditorPage and the Astro EditorSurface back every input: 'image' field with cmsImageSource, which reads through the same Media source on every forge.