Astro 5 content collections as an editorial layer in a programmatic site A developer integrated Astro 5 content collections into the programmatic site Open Alternative To, enabling editorial takes for 3 of 18 indexed pages while leaving the rest unchanged. The pattern uses typed content collections with Zod validation, allowing conditional rendering of editorial sections only for pages that have them, with no runtime errors for missing entries. Each editorial take requires 3-4 hours of writing and verification, covering license analysis and data accuracy checks. The 18 indexed pages on Open Alternative To https://ossfind.com are structurally identical — same template, same GitHub API data sources, same Claude Haiku-generated intro. That uniformity is useful at build time and a liability at review time. Pages that don't differ in any content requiring editorial judgment are indistinguishable from scraped mirrors. The fix I reached for is an Astro 5 content collection for per-entry editorial takes. Here's how the pattern works and where it earns its overhead. Astro 5 content collections are typed collections of Markdown or data files living in src/content/ . You define a Zod schema in content.config.ts , and at build time Astro validates every file and gives you typed APIs — getCollection , getEntry — that don't compile if a file is malformed or missing an expected field. The critical property for this use case: getEntry returns undefined for missing entries rather than throwing. You can conditionally render editorial content only for pages that have it, with no try/catch, no file-existence check, no runtime error. The 15 pages without editorial takes render exactly as before; the 3 pages with takes get the extra section automatically at build time. src/content/content.config.ts : js import { defineCollection, z } from "astro:content"; const perAlternativeTakes = defineCollection { type: "content", schema: z.object { saas slug: z.string , author: z.string , last reviewed: z.string , summary: z.string .max 200 , } , } ; export const collections = { "per-alternative-takes": perAlternativeTakes, }; Files live at src/content/per-alternative-takes/{slug}.md . The {slug} matches the saas slug in the comparison page's Turso data row — so auth0.md , datadog.md , airtable.md . The summary field is the 200-char intro line shown before the full editorial body. Everything after the frontmatter renders as standard Markdown via