Capstone: Building a Content-Driven Site

Chapter 12
Capstone: Building a Content-Driven Site
One real blog, tying together every prior chapter

This capstone builds one real, working blog — every earlier chapter contributes a real, working piece of it, not just a concept in isolation.

// The finished project structure src/ ├── content/ │ ├── config.ts // Ch5 — the blog collection's Zod schema │ └── blog/ │ ├── zero-js-by-default.md // Ch11 — plain Markdown │ └── live-demo-post.mdx // Ch11 — MDX with an embedded island ├── layouts/ │ └── BaseLayout.astro // Ch4/Ch6 — shared shell, global styles ├── components/ │ └── NewsletterSignup.jsx // Ch7/Ch8 — a React island ├── pages/ │ ├── blog/ │ │ ├── index.astro // Ch5 — getCollection() listing │ │ └── [slug].astro // Ch3/Ch5 — getStaticPaths() + render() │ └── api/ │ └── subscribe.ts // Ch9/Ch10 — a real POST endpoint └── astro.config.mjs // Ch8/Ch10 — mdx, react, and server output

The Content Collection

// src/content/config.ts import { defineCollection, z } from 'astro:content'; const blog = defineCollection({ type: 'content', schema: z.object({ title: z.string(), publishDate: z.date(), }), }); export const collections = { blog };

Both zero-js-by-default.md and live-demo-post.mdx satisfy this same schema (Chapters 5 and 11).

Listing & Rendering Posts

// src/pages/blog/index.astro --- import BaseLayout from '../../layouts/BaseLayout.astro'; import { getCollection } from 'astro:content'; const posts = await getCollection('blog'); --- <BaseLayout title="Blog"> <ul> {posts.map((post) => ( <li><a href={`/blog/${post.slug}`}>{post.data.title}</a></li> ))} </ul> </BaseLayout>
// src/pages/blog/[slug].astro --- import BaseLayout from '../../layouts/BaseLayout.astro'; import { getCollection } from 'astro:content'; export async function getStaticPaths() { const posts = await getCollection('blog'); return posts.map((post) => ({ params: { slug: post.slug }, props: { post }, })); } const { post } = Astro.props; const { Content } = await post.render(); --- <BaseLayout title={post.data.title}> <h1>{post.data.title}</h1> <Content /> </BaseLayout>

This is Chapter 3's own getStaticPaths() pattern and Chapter 5's own props-passing shortcut, working together exactly as designed.

An Island for the Newsletter Signup

// src/components/NewsletterSignup.jsx import { useState } from 'react'; export default function NewsletterSignup() { const [email, setEmail] = useState(''); const [sent, setSent] = useState(false); async function handleSubmit(e) { e.preventDefault(); await fetch('/api/subscribe', { method: 'POST', body: JSON.stringify({ email }), }); setSent(true); } return sent ? <p>Thanks — you're subscribed!</p> : ( <form onSubmit={handleSubmit}> <input value={email} onChange={(e) => setEmail(e.target.value)} /> <button>Subscribe</button> </form> ); }

Placed on the blog index below the fold via <NewsletterSignup client:visible /> — Chapter 7's own genuine performance payoff: its JS never even downloads unless a visitor scrolls that far.

The API Endpoint — and Why It Forces a Rendering-Mode Decision

// src/pages/api/subscribe.ts export async function POST({ request }) { const { email } = await request.json(); // in a real app: validate and store the email return new Response(JSON.stringify({ status: 'ok' }), { headers: { 'Content-Type': 'application/json' }, }); }

A real newsletter signup has to accept whatever email a visitor types, at the moment they submit it — exactly the case Chapter 10 identified as needing server output, not a static build. So the finished project's config reflects that decision:

// astro.config.mjs import { defineConfig } from 'astro/config'; import react from '@astrojs/react'; import mdx from '@astrojs/mdx'; import node from '@astrojs/node'; export default defineConfig({ integrations: [react(), mdx()], output: 'server', adapter: node({ mode: 'standalone' }), });
A real trade-off, made deliberately
Choosing output: 'server' for the whole project means the blog index and post pages now render per-request instead of once at build time — a genuine cost, since Chapter 3's own getStaticPaths() isn't even necessary anymore in this mode. A more finely-tuned version of this same project could keep the blog pages statically pre-rendered (export const prerender = true on just those two files, per Chapter 10) while leaving only subscribe.ts dynamic — the right call depends on how much of the site is genuinely static versus genuinely dynamic, exactly the judgment call Chapter 10 raised.

Every Chapter's Own Contribution

PieceChapter(s)
Zero-JS static rendering as the baseline1
Frontmatter, props, markup expressions2
Blog index/detail routing, getStaticPaths()3
BaseLayout, <slot />4
The blog Content Collection & schema5
Scoped and global dark-theme styling6
client:visible on the newsletter island7
The React integration itself8
The /api/subscribe POST endpoint9
output: 'server' + the Node adapter10
The MDX post with its own embedded island11

Coding Challenges

Challenge 1

Build this capstone's own blog collection, BaseLayout, index page, and [slug].astro route from scratch, with at least two real Markdown posts.

📄 View solution
Challenge 2

Add the NewsletterSignup island with client:visible, and the /api/subscribe endpoint it posts to, confirming a real submission returns { status: 'ok' }.

📄 View solution
Challenge 3

Refine the project by marking the blog index and [slug] pages export const prerender = true while leaving subscribe.ts dynamic, and confirm the site still builds correctly with this mixed approach.

📄 View solution

Chapter 12 Quick Reference

  • Content Collections + Zod — one schema, mixed .md/.mdx entries
  • getStaticPaths() + props — dynamic routing without a second lookup
  • client:visible — an island whose JS never downloads until scrolled into view
  • A real POST endpoint — the concrete reason a project needs server output, not static
  • Per-page prerender — the finer-grained alternative to an all-or-nothing rendering mode

★ Astro Course Complete — 12 / 12 chapters

From zero-JS-by-default and the islands architecture through Content Collections, styling, cross-framework composition, data fetching, and rendering modes. Chapters 3 and 5 in particular — arbitrary-depth routing and structured content — are the direct foundation a future Website Rebuild with Astro course can now build on.