koo.dev

September 9, 2026 · Christian

Why adding a product to koo.dev costs one file

When koo.dev started, the plan was clear: BStrong is the first entry in the portfolio, but it won’t be the last. If adding the next one meant touching a route file, an array of product slugs, or the nav — the architecture would have failed before it started.

One collection, two rails

/work is driven by a single Astro content collection with a kind field: "product" for things built end-to-end, "client" for work done for someone else. The page groups entries by kind and renders both rails from one getCollection() call — nothing else changes when a new entry identifies as either one.

The schema is the contract

Every entry — title, summary, cover, coverAlt, and so on — is validated by a Zod schema at build time. Forget a required field, or point cover at a file that isn’t there, and astro build fails with the exact field and reason. There’s no separate test suite for this: the build is the test, on every pull request.

This blog works the same way, down to an optional product field that can reference a product by its collection ID — write one that doesn’t exist, and the build fails just as loudly.

Try it

Drop a new .mdx file in src/content/products/, fill in the frontmatter, and it appears on /work, at its own /work/<slug> page, and in the sitemap. No code change, no deploy step to remember.