Work · 2026
This site: a trilingual portfolio whose tests run on the built output
Astro 5, three languages with localised URLs, and a test suite that reads dist/ rather than the source. Including the bug that suite could not catch, which shipped lorem ipsum to production.
The site you’re reading. It’s an Astro 5 site, prerendered to static HTML, published in English, Italian and French. I’m describing it here mostly for one decision and one failure.
URLs that belong to their language
English sits at the root with no prefix; Italian lives under /it/, French
under /fr/. The part that costs real work is that the path segments are
translated too, not just the content:
| English | Italian | French |
|---|---|---|
/projects/ | /it/progetti/ | /fr/projets/ |
/about/ | /it/chi-sono/ | /fr/a-propos/ |
/hobbies/ | /it/hobby/ | /fr/loisirs/ |
A project is one Markdown file per language sharing a slug, which is what lets
the three versions point at each other. A page only has to exist in the
languages it has content for: hreflang is emitted towards the translations
that actually exist, never towards a URL that would 404. Every page also
references itself, which the spec requires and which is the rule people most
often miss.
The tests read the build, not the source
The suite — fifteen files, 64 tests on the Node test runner — runs npm run build first and then reads dist/. Nothing is mocked, because the thing worth
checking is the HTML a browser will actually receive.
It checks that no internal link is broken, that every page has a non-empty
title and description and that the top-level ones are distinct, that every
locally referenced image exists on disk, that hreflang sets are complete and
self-referencing, that drafts never reach the build, and that the theme is
applied before first paint so the page doesn’t flash white.
Asserting against the build catches a whole class of bug that source-level tests miss: a link that was correct in a component and wrong after routing, an image path that resolved in dev and not in the output.
The bug the tests could not see
Here’s the part worth telling. For weeks this site served five projects called things like “Lorem ipsum ingestion pipeline”, and the footer of every single page read “Lorem ipsum dolor sit amet”. The test suite was green the whole time.
It was green because every assertion was structural. The fixtures the tests used to verify behaviour — a published project, one not featured, one draft — were the same files that shipped as content. The suite asked is a project listed on this page, and a project was listed. It had no opinion on what the project said.
That’s the trap in using your fixtures as your content. The tests keep passing, precisely and honestly, while the site says nothing true. A green suite tells you the structure holds; it cannot tell you the structure is holding up something real.
What fixed it wasn’t a cleverer assertion, it was deleting the fixtures and pointing the tests at projects that exist. The behavioural coverage is unchanged — something still has to be published-but-not-featured for that test to mean anything, and that something is now this page.
What /projects/ served for weeks, rebuilt from the commit before the clean-up. Every test was green against this page.
Deploying on purpose
The build rsyncs to its host, and npm run deploy runs in --dry-run by
default. Publishing for real takes an explicit -- --live, and then the script
asks the operator to type the domain name before it proceeds.
That friction is deliberate, because rsync runs with --delete: anything on the
server that isn’t in dist/ goes away. A flag that destructive shouldn’t be one
typo from a tired evening.
Placeholder art without a network call
Every project, post and page renders a cover. With no cover: field set, it
falls back to a flat SVG generated deterministically from the slug by a script —
same slug, same artwork, every time, with no fonts or images fetched from
anywhere. A test fails the build if a page references a placeholder that doesn’t
exist, which is how a missing cover becomes a red test instead of a broken
image.