# Upload and render

> The whole loop — bytes in, responsive URLs out — with the two decisions that cannot be undone later.

import { Code } from '@astrojs/starlight/components';
import uploadImage from '../../../../examples/01-upload-an-image.ts?raw';
import imageUrls from '../../../../examples/02-image-urls.ts?raw';

## Upload

<Code code={uploadImage} lang="ts" title="upload-an-image.ts" />

`upload()` does the whole loop in one call: compress → sha256 → presign → PUT
straight to storage → register → poll until ready. It returns ids and the
canonical URL, **not** a full asset — build the other URLs from the sha.

## Render

<Code code={imageUrls} lang="ts" title="image-urls.ts" />

## The two decisions you cannot undo

### 1. The presets you ask for at the FIRST ingest

Ask for what you need here. A variant you skip is **not** lost — cleanup keeps
`raw/` for as long as the asset row exists (verified on a
2-month-old thumb-only asset), so `regenerate({ presets })` can add it later.
What you get meanwhile is an incoherence: `getAssetUrl(asset,"md")` **404**s
while `/t/…width=1280/<sha>.webp` serves the same image on demand.

The real trap is upstream of that: a backup run once archived **97 files
"successfully"** after copying a delivery-oriented `presets: ["thumb"]` from a
migration example, and the operator concluded the bytes were gone without
checking `raw`. **A preset list copied from an example is a decision you did not
make**, and *"N uploaded, 0 failed"* measures the upload, not the outcome.

If the bytes matter, `"original"` is not optional.

### 2. The filename you upload under

The stored extension for `original` comes from the **uploaded filename**, not
from the MIME type. For JPEG those disagree: `image/jpeg` implies `jpeg` while
every camera writes `.jpg`. Measured: `…-o.jpeg` **404**, `…-o.jpg` **200**.

If you plan to fetch the original back by URL, `HEAD` it once and store what
answered rather than deriving it twice in two places.

## What you get back

| | |
|---|---|
| `assetId` | the row id — poll this, bind slots to it |
| `sha256` | content address — dedup key, and what every URL builder takes |
| `cdnUrl` | the canonical URL for the asset |

Two uploads of the same bytes return the **same** asset. That is a feature, and
it is also why a retried upload can hide a bug in the non-deduped path — see
[For agents](/start/for-agents/).