The image library
Upload, describe and optimise images, one commit each: why alt text is not a property of a file, and what the AVIF conversion measures first.
/admin/images/ is a view of src/assets/library/, read fresh on every request. Every action on it is a commit on your branch — none of them is a push.
The listing
A request-time directory read, not the build’s glob. That distinction is the difference between a working library and a confusing one: in a production bundle the glob is frozen at build time, so an upload would be invisible and a delete a ghost until the next rebuild.
A file the build has never seen is marked pending with a badge on its tile, and its dimensions are read directly from the file. Once your site rebuilds, the badge goes.
Bytes are served through the CMS’s own asset endpoint, which revalidates on modified-time and size — necessary because Optimise rewrites a file’s bytes under an unchanged name.
Upload
Drop files or use the button. The stored extension comes from the content type, never from the filename, so a .jpg that is really a PNG is stored as what it is. A filename that sanitises to nothing is refused; a collision suffixes from -2.
The filename is the security boundary here, and it is a pure function with its own tests — there is no path in an upload, only a name.
Alt text is not a property of a file
This is the one modelling decision on the screen worth understanding, because it explains why the alt field behaves the way it does.
The same photograph means different things in different entries. So a description is written per use, not once per file — and the modal’s alt field writes both: it sets a default in src/assets/library/_descriptions.json and rewrites every existing use of that file, in one commit.
Precedence: the first described use wins, and the default fills in behind it. Empty removes the default and writes , which is the markdown spelling of decorative — the audit keeps flagging it, which is correct, because decorative is a claim somebody has to make on purpose.
Describe is the one verb that is not batched. One sentence across different photographs would be wrong.
The sidecar file starts with an underscore, which keeps it out of every listing by construction — the directory read, the glob pattern and the reference scan all skip it.
Optimise
Converts to AVIF when the encode actually wins. The saving is measured first; a bigger result is thrown away, and an over-wide image falls back to resizing in place. A file earns a row in the Optimise tab when the saving is at least a tenth of the file and at least 4 KB.
A conversion renames the file to .avif and rewrites every reference to it in the same commit, moving its default description with it. That is why every optimise button arms before it fires: the conversion touches other people’s entries.
Quality is images.avifQuality in src/admin.config.ts, defaulting to 50 — roughly visually lossless for photography on AVIF’s scale, which is not JPEG’s.
The reference scan
The library reports which files are used and which are not, by searching your tracked files for the guarded assets/library/<file> path form — fixed-string, so a dot in a filename is a dot rather than a wildcard.
A hit inside the library directory itself, or anywhere under src/admin/, does not count as a use. For one day it did, and the scan cheerfully read the CMS’s own test fixtures as content and rendered their alt strings as an author’s.
On the demo template, the library honestly reports every file unreferenced and missing alt text, because nothing outside the CMS’s own source references those files. Your real repository gets real numbers.
Deleting
Deletes the file and drops its default description, in its own commit. Like every other verb here, it is in your history afterwards.
Selecting
Selection is keyed by filename, because the grid renders every panel and one file can therefore have up to three tiles on screen at once. Selecting it in one place selects it everywhere.
Upload, delete and optimise all reload the screen, because every figure on it — the counts, the tab totals, the savings — is derived from the listing.
The picker
The same library, opened inline from the editor’s Image block and from the social image field. The picker pre-fills the file’s default description, so the common case is one click.
Troubleshooting
An uploaded image shows a “pending” badge. Expected. The build has not seen it yet; the badge clears at the next build of your site.
Optimise offers nothing for a large file. The measured AVIF encode did not beat the original by a tenth of the file and 4 KB. That is the check working — a conversion that makes a file bigger is not an optimisation.
Every file says unreferenced. On the demo content, that is true. In your own repository, check that your entries reference images through the assets/library/<file> path form; that is the one spelling the scan matches.
A description I wrote does not appear on an entry. Precedence is first-described-use wins, default behind. An entry with its own alt text keeps it.
An image 404s after Optimise. The file was renamed to .avif and every reference was rewritten in the same commit — but only the guarded path form. A reference built by string concatenation in a component will not have been found.