> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stage.systems/llms.txt
> Use this file to discover all available pages before exploring further.

# Media

> Uploads go through the API to object storage and get WebP variants at four widths. Renderers address them by convention, and a slot can hold an image or a video.

## Upload

```
admin ─▶ POST /api/image-upload ─▶ original to storage ({org}/{hash}.{ext})
                                ─▶ WebP variants ({org}/{hash}-{640,1200,1920,2880}.webp)
                                ─▶ media row (hash, width, height, alt, …)
```

The API holds the storage credentials; sites hold none. Files are content-addressed, so the same image uploaded twice is one object. Alt text is generated at upload if none is supplied and is editable in the library.

## Render

An image field stores a URL, not dimensions: one of ours (a CDN URL) or an external one. The SDK helpers apply to CDN URLs; `isCdnImage` tells them apart, and an external URL renders as a plain `src`:

| Helper                | Gives                                |
| --------------------- | ------------------------------------ |
| `isCdnImage(value)`   | Whether a value is one of ours       |
| `imageDefault(value)` | The URL of a sensible single variant |
| `imageSrcset(value)`  | A `srcset` across the four widths    |

Because URLs are derived, moving the CDN host or adding a width is a platform change, not a content migration.

## The Media slot

`<Media image video path alt class sizes />` is the one way to render a slot. It replaces the hand-written `<img>` idiom, keeps the identical CDN `srcset` and focal-point output, and adds the video side:

* video set: `<video autoplay muted loop playsinline preload="metadata">`, poster resolved from the video host
* else: `<img>`, or a transparent placeholder when empty so the slot stays a picker in the admin

It emits the combined-slot anchors (`data-e-path`, `data-e-media`, `data-e-video-path`), which is what opens the combined library instead of the image-only picker.

## Converting an existing slot

<Steps>
  <Step title="Swap the markup">
    Replace the `<img>` block with `<Media image={image} video={imageVideo} path="props.image" … />`. The image case renders identically.
  </Step>

  <Step title="Add the video prop">
    A `${imageKey}Video` prop next to the image one. Absent on live rows reads as empty. No backfill.
  </Step>

  <Step title="Update the Add form, if the slot is in a list">
    `{ key: "image", type: "media", videoKey: "imageVideo" }`.
  </Step>
</Steps>

Zero data migration: live rows are never touched, and the video side stays empty until an editor picks a clip.

## Video

Video uploads go to the video host through the API's upload and status routes and are stored as `mux:{id}`. Transcoding can take minutes; the library tile tracks the phase. Static builds localise posters.

## Storage is public by URL

Media objects are reachable at their hashed URLs even on a private site. Signed URLs are a later phase.
