Anatomy of a fast static site: how a post becomes HTML
A technical walk through the render pipeline of this site — how one MDX file becomes cached, SEO-complete HTML with zero client JavaScript, and the tradeoffs that buys.

Marketing sites are judged in the first 500 milliseconds. A hero that paints
instantly and a Lighthouse score of 100 aren’t vanity — they’re conversion. This
post follows a single blog post through this site’s build to show exactly how that
speed is produced, and what it costs. We’ll keep one example in view the whole
way: the file you’re reading right now, anatomy-of-a-fast-astro-site.md.
The shape of the pipeline
At its core the build is a pure function: content in, static HTML out. Nothing runs on a server at request time.
content/blog/*.md ──▶ content collection ──▶ getStaticPaths ──▶ render()
(MDX) (schema-checked) (one route/post) │
▼
src/assets/*.jpg ──▶ astro:assets ──▶ optimized .webp static HTML + sitemap + RSS
Every arrow is build-time. The browser receives the last box and nothing that produced it. Let’s walk each stage.
Stage 1 — content is data, and data has a schema
The post isn’t loaded by a glob at runtime; it’s a typed entry in a content collection. The schema is enforced when the site builds, so a malformed post fails CI instead of shipping broken:
const blog = defineCollection({
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
draft: z.boolean().default(false),
}),
});
The payoff isn’t just safety — it’s that pubDate arrives in a template as a real
Date, and heroImage as an optimizable asset, with no parsing glue. The
schema is the contract between writing and rendering.
Stage 2 — one route per post, resolved at build
Because every post is known at build time, the router asks the collection for the
full list and emits one HTML file per entry. There is no /blog/[slug] lambda
waiting for traffic:
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => !data.draft);
return posts.map((post) => ({ params: { slug: post.id }, props: post }));
}
Note the !data.draft filter: drafts never get a route, so they’re absent from
the build, the sitemap, and RSS in one stroke. This is the whole trick of static
generation — work that a dynamic site repeats on every request happens once,
here.
Stage 3 — zero JavaScript, on purpose
A rendered post ships as HTML and CSS. No framework runtime, no hydration. The only inline script on a content page is the pre-paint theme setter, kept tiny so it can run before first paint without blocking it:
const saved = localStorage.getItem('theme');
if (saved) document.documentElement.setAttribute('data-theme', saved);
Interactive pieces (the mobile menu, the FAQ accordion) are opt-in islands, not a global bundle. The default is nothing. That default is why the site is fast — you pay for interactivity only where you add it.
The tradeoffs (say them out loud)
Static generation is not free, and pretending otherwise is how you pick the wrong tool:
- Content changes need a rebuild. Publishing a post is a deploy, not a database write. Fine at blog cadence; wrong for user-generated content.
- No per-request personalization. Every visitor gets the same HTML. If you need per-user server rendering, you want an adapter and SSR — a different mode.
- Build time grows with content. At thousands of pages, image optimization and rendering start to cost real minutes. Incremental builds matter then.
For a marketing site — dozens of pages, content that changes on a human schedule, identical for every visitor — those costs are all upside. That’s the fit.
Why it’s worth it
Speed here isn’t an optimization you bolt on later; it falls out of the architecture. Content is data, routes are known, and the browser gets HTML. The result is a site that paints instantly, ranks well because crawlers get complete markup, and costs almost nothing to serve. Choose the constraint that makes the common case free — and let the rare case (SSR) be the thing you reach for deliberately, not by default.