posts and avif
Some checks failed
Deploy / build-deploy (push) Failing after 5m49s

This commit is contained in:
Jorijn van der Graaf 2026-08-10 01:37:26 +02:00
commit 6841623e23
17 changed files with 2306 additions and 148 deletions

View file

@ -128,6 +128,62 @@ build. Neither fails the build on a network error — a fediverse outage leaves
previous `posts.json` in place, and a single failed download leaves that one entry
pointing at its original URL rather than losing the post.
### The full body, and its inline media
Each post is hosted whole at `/posts/<slug>`, with the card on `/posts` linking
to it — the writing is what the site is about, and a page this site can name as
canonical is the only version a search engine can be pointed at. The comments
are *not* mirrored: every post page links out to the thread, which is where the
discussion belongs.
The body stays Markdown in `posts.json` and is rendered by
`Catcrafts.Shared:Markdown` at page-render time, never converted to HTML by the
shell. That is deliberate: the renderer is inside the escaping guarantee, and
text fetched from someone else's server must not be able to become markup
anywhere else. Raw HTML in a body is always shown as text.
`fetch-media.sh` mirrors what the body embeds as well as the headline file, and
rewrites the URLs **inside the Markdown**, so a post page loads nothing
third-party either. It also writes a `body_media` list per post — dimensions,
poster frame and format renditions for each inline file, which Markdown syntax
has nowhere to carry. Slugs come from the title; a duplicate title takes the
post's numeric id as a suffix, so an old post's URL is never renumbered by a new
one. Re-running the script is a no-op: local paths are adopted from the mount
rather than re-fetched.
### The image format ladder
Every mirrored still image is transcoded to two siblings named after its content
hash, and `Catcrafts.Shared:Media` serves all three as one `<picture>` — so the
browser fetches **exactly one**:
| tier | file | size vs. WebP | who gets it |
|---|---|---|---|
| `<source type="image/avif">` | `<hash>.avif` | **76%** | almost everyone |
| `<source type="image/webp">` | `<hash>.webp` (the mirrored original) | 100% | Safari 1416 |
| `<img src>` | `<hash>.png` | **875%** | neither of the above |
The middle tier is why the PNG being ~9× the WebP does not matter: it is free
(the mirror already downloaded that file) and it is what the small number of
non-AVIF browsers actually land on. The PNG is the floor nothing can refuse.
AVIF is encoded at `crf 26, cpu-used 6, yuv444p` — measured at SSIM 0.997
against the source and still smaller than it. Full chroma is deliberate: these
are screenshots of text, and re-subsampling chroma that pict-rs already
subsampled once fringes coloured text visibly, for about 3% more bytes.
Encoding is skipped when the sibling is already on the mount, so only genuinely
new images cost encoder time (~0.5 s each). Animated sources are left alone
entirely — one moving GIF beats three copies of its first frame. Video posters
are skipped too: `poster` takes exactly one URL, so a `<video>` cannot negotiate
a format the way `<picture>` can and the renditions would be unreachable.
Both encodes pin `-c:v` and then **verify the codec that actually came out**.
That check earned its place immediately: `-f image2 out.png` without an explicit
codec makes ffmpeg fall back to the muxer default, which is MJPEG — it silently
produced a full set of lossy JPEGs under `.png` names, served to browsers as
`image/png`. A rendition that fails the check is discarded and its tier dropped.
### Publish the media first, then post it
**The recommended flow is to put a recording on catcrafts.net before writing the