---
title: Manage media and images
description: Upload once, let Quilium produce the sizes, render them with the right dimensions and a real alt text.
updated: 2026-08-19
---

Images are the one thing every page on every site uses, and the one place where a small habit, uploading the original and declaring the versions, saves you from re-cropping a library by hand later.

<div class="q-callout q-callout-note"><i class="fa-solid fa-flask lead"></i>
  <div><b>Live example: Lisière.</b> The snippets come from <b>Lisière</b>, whose articles carry a cover image declared in two versions, a card and a full-width one.</div></div>

## 1. Declare the versions on the field, not in the upload

A version is a named size Quilium generates for you. Declare the ones the design actually uses:

```yaml
        image:
          type: image
          label: Image de couverture
          config:
            versions:
              card:
                label: Carte
                width: 800
                height: 600
              cover:
                label: Couverture
                width: 1600
                height: 900
```

<div class="q-callout q-callout-warn"><i class="fa-solid fa-triangle-exclamation lead"></i>
  <div><b>Upload the original, never a pre-resized file.</b> Cropping before upload throws away the pixels a future version would need. Quilium derives every size from the original, so a new breakpoint is a config change, not a re-upload.</div></div>

## 2. Know the stored shape

A media reference is **always an array of objects**, never a bare UUID and never a URL:

```json
{ "image": [{ "id": "…" }] }
```

That single rule accounts for a large share of 422s on writes.

## 3. Render it

Guard the array, read the version you declared, and always set `width` and `height` so the browser reserves the space:

```liquid
{% if art.image and art.image.size > 0 %}
  <img src="{{ art.image[0].versions.card.url }}"
       alt="{% if art.image[0].alt != blank %}{{ art.image[0].alt }}{% else %}{{ art.title }}{% endif %}"
       width="800" height="600" loading="lazy">
{% endif %}
```

Falling back to the entry's title is better than an empty attribute, and better than repeating the file name.

The alt lives on the **media item**, not on the field that points at it: it is a dedicated `alt`, alongside `name`, `legend`, `description` and `credits`. Reach it through the reference, `image[0].alt` — never `image[0].name`, which is the librarian's label ("Couverture — Écorce") and reads as noise in a screen reader.

Writing it right through the library means every page that uses the image inherits it. One call per asset, flat at the root, no `data:` wrapper:

```
update-media(media_id: "<uuid>", alt: "Gros plan d'écorce d'arbre, texture rugueuse en lumière froide")
```

It is translatable like any content: pass the default-language alt at the root and mirror it per locale in `translations`, in the same call.

Two things follow. **A decorative image takes `alt=""`** — an empty string, deliberately, not a missing attribute and not a description of the decoration; the media record carries an `altDisabled` flag for exactly that intent. And **the same image used twice on a page needs two different alts**, or the second one empty, since repeating it makes a screen reader read the same sentence twice.

## 4. Organise the library

Media live in folders, and the folder tree is its own set of operations: create, rename, move, and move media between folders. A site whose media all sit at the root becomes unusable for an editor around the two hundredth file.

![The media library: a grid of image cards, each with its name, format, weight and a usage counter. The left rail holds Upload, Recent, My files and Trash; this site has no folders, so everything sits at the root.](/thumbs/5755e320-aa3f-11f1-90ac-5d2cff37b90b.png)

The library above is a small site — eight images, no folders yet, everything at the root. That is the shape every site starts in, and the one to leave before it costs anything.

Two things on each card are worth knowing about. The **weight** is the original you uploaded, not what a visitor downloads: the versions from section 1 are generated from it and served instead. And the **usage counter** — `4x`, `2x`, `1x` — is how many places reference that media. It is the answer to "can I delete this?", and it is available to an agent as well, on the media record's `usage` field.

<div class="q-callout q-callout-warn"><i class="fa-solid fa-triangle-exclamation lead"></i>
  <div><b>A counter above 1 means an image is shared.</b> Replacing the file updates every one of those places at once, which is the feature — and deleting it empties them all just as quietly. Check the count before doing either.</div></div>

## 5. Let an agent do the tedious part

Describing a batch of photos and filling their alt texts is the archetypal task to hand over:

```prompt
List the media in the "itineraires" folder whose alt is empty,
write one for each in French, factual, under 120 characters, then apply them.
Show me the list before writing.
```

The agent looks at each image to write its alt, so ask to see the list first: a generated alt is plausible far more often than it is right.

## Pitfalls

- **A version you did not declare does not exist.** Reading `versions.thumb.url` when the field declares only `card` renders an empty `src`.
- **Replacing a media keeps its references.** Use it deliberately: swapping a file updates every page that points at it.
- **Deleting a media does not clean the references.** Check what points at it first.

## Related

<div class="q-cards">
  <a class="q-card reco" href="/en/docs/custom-fields"><span class="k">Recommended</span><span class="t">Custom fields</span><span class="d">The <code>image</code> field and its <code>versions</code> config.</span></a>
  <a class="q-card" href="/en/docs/mcp-tools"><span class="t">MCP tools</span><span class="d">Upload, folders and the array-of-objects rule.</span></a>
  <a class="q-card" href="/en/docs/blog"><span class="t">Build a blog</span><span class="d">Where these images end up.</span></a>
</div>
