---
title: Images
description: Upload the pictures of your content once, converted to WebP when they arrive, kept in R2 and served from your Hollware.
section: guides
order: 8
---

# Images

An image is a file of a site. It is sent once, converted once when it arrives, kept in an R2 bucket of your account, and named in content by a reference that never changes: `asset:ast_…`. Agents upload it over MCP, people in the admin under **Images**.

A project needs two bindings in `wrangler.jsonc`, which projects made with 1.0 have:

```jsonc
"r2_buckets": [{ "binding": "MEDIA", "bucket_name": "my-hollware-media" }],
"images": { "binding": "IMAGES" },
```

`hollware deploy` creates the bucket the first time, as it creates the database. Without `MEDIA`, `upload_asset` answers `unavailable`. Without `IMAGES`, every file is kept as it came, unconverted. `hollware doctor` says which one is missing.

## Upload an image

An agent calls `upload_asset` with the file in base64, its name and its `alt`, what the image shows for whoever cannot see it:

```json
{ "filename": "continuum.png", "alt": "The Continuum logo on a dark background", "data": "iVBORw0KGgo…" }
```

It answers with the asset: its `reference`, its `url`, the format and size it is served at, and the format and size it was sent at. The same file sent again answers `exists` with the same asset, so an agent can upload without checking first. The same file in another site of the deployment is another asset, with an id of its own. `list_assets` lists the images of the site, newest first, and `get_asset` gets one by its id.

What a file is comes from its bytes, never from its name: JPEG, PNG, WebP, AVIF, GIF and SVG, at most 10 MB. Anything else is refused with what to do.

## Show it

Write the reference wherever the image goes, in the frontmatter or in the Markdown:

```markdown
---
title: Moving a monorepo to a new toolchain
img: asset:ast_e51afa61663c33d06276f786da
---

![The Continuum logo on a dark background](asset:ast_e51afa61663c33d06276f786da)
```

A Markdown image with no alt of its own takes the asset's when a site reads it, so the contract `images-have-alt` lets it through. What an image shows can change later: `update_asset` with its id and a new `alt`, or **Edit what it shows** on its card under **Images**. Every entry that shows it without an alt of its own reads the new one; the file never changes.

Sites read the URL, never the reference: `getEntry` over RPC and the read API turn every `asset:` into the image's URL, in the frontmatter and in the rendered document. Over MCP an agent reads what it wrote, references included.

The URL is `<your Hollware>/assets/<site>/<id>`, public and cached for a year: an asset never changes, so a new picture is a new asset.

## Responsive images

When an image arrives, Hollware also makes its narrower copies, so a phone loads a small one: one for each of the site's `media.widths` narrower than the image, 640 and 1280 pixels by default. Each is served at `<url>/<width>`.

A site gets them without asking. In the HTML of an entry, and in the document a site reads, each image carries `srcset`, `sizes` and its `width` and `height`, so the page keeps its room while it loads:

```html
<img src="https://cms.example.com/assets/blog/ast_e51a…" srcset="https://cms.example.com/assets/blog/ast_e51a…/640 640w, https://cms.example.com/assets/blog/ast_e51a…/1280 1280w, https://cms.example.com/assets/blog/ast_e51a… 1600w" sizes="(min-width: 1600px) 1600px, 100vw" width="1600" height="900" alt="…">
```

A size the author wrote is kept. For the images of the frontmatter, a cover, a summary a site reads (`getEntry`, `listEntries`) carries `images`: by URL, the `width`, `height` and `srcset` of each.

```ts
const cover = entry.images?.[entry.frontmatter.img]
// <img src={entry.frontmatter.img} srcset={cover?.srcset ?? undefined} width={cover?.width} …>
```

`upload_asset`, `get_asset` and `list_assets` answer `widths` and `srcset` too. Images uploaded before 1.0.0-rc.3 have no copies.

## How images are converted

Each site says how in its `media` setting, part of its structure (read it with `get_structure`, change it with `update_site`):

```json
"media": { "format": "webp", "quality": 85, "maxWidth": 2400, "widths": [640, 1280], "keepOriginals": true }
```

Those are the defaults, which a site that says nothing gets. In the admin, they are under **Site**, in **Images**.

| Sent as | Kept as |
| --- | --- |
| JPEG | `format` at `quality`, lossy |
| PNG | `format`, lossless: screenshots, diagrams and logos keep their edges, and transparency |
| GIF | `format` with every frame, animated |
| WebP, AVIF | as they came |
| SVG | as it came, never converted |

Wider than `maxWidth` (320 to 8192 pixels), an image is scaled down, never up. `format` takes `webp`, `avif` (smaller still, a little slower to encode) or `original`, which keeps every file as it came, without copies. `widths` takes up to four widths from 320 to 8192, smallest first, or `[]` for no copies. A change applies to the images uploaded after it.

Hollware keeps the original of each file too, never served, at `/v1/assets/<id>/original` with a personal token, so a later change of format has the originals to start from. A site that would rather take less storage sets `keepOriginals` to `false` (**Keep the original of each image** in the admin): it keeps only the file it serves and its copies, and the download of an original, and `hollware export`, give the file served instead. The images uploaded before keep theirs.

## SVG

An SVG stays a vector and is never rewritten. Hollware accepts one only when nothing in it can run or reach out: no `<script>`, `<foreignObject>`, `<iframe>`, `<embed>` or `<object>`, no `on…` attribute, no `javascript:` or `data:` URL, no `<!DOCTYPE>` or `<!ENTITY>`, and no link outside the file. Character references and entities other than the five of XML (`&amp;`, `&lt;`, `&gt;`, `&quot;`, `&apos;`), backslashes and animations of a link (`<set>` or `<animate>` of `href`) are refused too, since they can spell what the other rules look for. Anything else is refused with the part to remove. An SVG optimizer cleans most files that come from a design tool.

It is served with a security policy that runs nothing even if a browser opens it on its own.

## What it costs

The conversion is Cloudflare Images on R2 storage. Each new image is one transformation, two when it is wider than `maxWidth`, and one more for each copy; an image sent again costs nothing. When the allowance runs out while making the copies, the image keeps those already made. The free plan includes 5,000 transformations a month: past them, an upload answers `unavailable` until the next month, and nothing is charged. R2 keeps 10 GB free.

## Your images on your site

The URLs of the assets are on the domain of your Hollware, which tells search engines not to index it. A site that wants its pictures indexed on its own domain serves them itself:

```sh
npx hollware export ./content --published --assets public/images/cms --assets-url /images/cms/ --url https://cms.example.com --token-file export.token
```

The token is a personal one with `content:read`: `hollware token create --label export --scopes content:read --remote`.

`--assets` writes each image the published entries show into that directory, as it is served, and every `asset:` reference in the export points at `--assets-url` instead. A static site builds from that export.

Without `--assets`, an export keeps the references as they are and writes the images, as they were uploaded, to `_assets/` with an `assets.json` beside them. `hollware import` uploads them again before the entries that show them, so an export is a backup of your content and of its pictures.
