Andrea Grossi
← Back to projects

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:

EnglishItalianFrench
/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.

The projects page while the fixtures were still live 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.