How I built this blog · Part 1
How I built this blog, explained like you're five
The 30-second version
This blog is built with two tools. Astro turns my posts into plain, fast web pages ahead of time. Sanity is where I write those posts, kind of like Google Docs for website content. They live separately and talk to each other, which is what people mean by "headless." In this post I'll explain what all of that means and walk you through setting up the same foundation yourself.
On this page
Why this matters
At some point in your career, someone will say "we're going headless" or "let's make it a static site," and everyone will nod. Understanding these ideas helps you choose the right tool instead of the one you've always used.
It also matters for you personally. A personal site is one of the best things a developer can own: a place to show your work, write down what you've learned, and point people to when they ask what you do.
First, some ELI5
What's a website, really? It's a bunch of files (HTML, CSS, JavaScript, images) that a server hands to your browser when you visit a URL. That's it. Everything else is about how those files get made.
Dynamic vs. static: a restaurant analogy. A traditional WordPress site is like a restaurant that cooks every dish to order. Each time someone visits, the server runs PHP, asks the database for the post, assembles the page, and serves it. That's flexible, but it's work on every single visit.
A static site is like a bakery that bakes everything before opening. The pages are built once, ahead of time, and the server just hands them over. That makes them fast, cheap to host, and hard to hack, because there's no database sitting behind the page to break into.
What does "headless" mean? In traditional WordPress, the kitchen (where content is written and stored) and the dining room (what visitors see) are welded together. A headless CMS keeps only the kitchen. It stores your content and hands it out through an API, and you build the dining room however you like. Sanity is my kitchen. Astro builds my dining room.
Putting it together: I write a post in Sanity, Astro fetches it and bakes it into a page, and the server serves that finished page to you.
The slow work happens once, at build time, so every visit after that is just handing over a finished file
A post is written once, baked once, and served many times
- Sanity StudioYou write a post
- Astro buildBakes it into HTML
- Your serverStores the pages
- Reader's browserGets a ready page
Why not WordPress? And why Astro + Sanity?
I work with WordPress every day, and I like it. It powers a huge chunk of the web for good reason. But for my own site, I wanted to stretch past what I already know, and a blog that's mostly text doesn't need a database answering every visit.
Why Astro:
- It's built for content-heavy sites like blogs and docs.
- It ships almost no JavaScript by default, so pages load fast.
- It has "islands": you can drop an interactive component (React, Vue, Svelte) into one spot on a page and keep the rest plain HTML. Later posts will use this for interactive demos.
Why Sanity:
- I use it at work, so I could focus on the fun parts.
- I get to design my own content model. Instead of a single big text box, my posts are made of custom blocks like "Step," "ELI5," and "What could go wrong." You're reading those blocks right now.
- Sanity Studio can live right inside the Astro project, at /studio.
Step by step: building the foundation
You'll need Node.js installed, a terminal, and a free Sanity account.
Step 1:Create the Astro project
This command asks a few questions and sets up a starter project in a new folder. Pick the empty template and say yes to TypeScript.
npm create astro@latest my-blog
cd my-blog
npm run devOpen the local URL it prints (usually http://localhost:4321). If you see a page, your site is alive.
Step 2:Create a Sanity project
Sign in at sanity.io, create a new project, and note two things: the project ID and the dataset name (usually "production"). Think of the project ID as your kitchen's address.
Step 3:Connect Sanity to Astro
Astro has a one-line command for adding integrations. Sanity's Studio is built with React, so we add React too.
npx astro add @sanity/astro @astrojs/react
npm install sanityThen tell Astro where your kitchen is in astro.config.mjs:
import { defineConfig } from "astro/config";
import sanity from "@sanity/astro";
import react from "@astrojs/react";
export default defineConfig({
integrations: [
sanity({
projectId: "your-project-id",
dataset: "production",
useCdn: false,
studioBasePath: "/studio",
}),
react(),
],
});Step 4:Describe your content (the schema)
A schema is a recipe card that tells Sanity what a post is made of. Start small: a title, a URL slug, a date, and a body.
// schemaTypes/post.ts
import { defineType, defineField } from "sanity";
export const post = defineType({
name: "post",
title: "Post",
type: "document",
fields: [
defineField({ name: "title", type: "string" }),
defineField({ name: "slug", type: "slug", options: { source: "title" } }),
defineField({ name: "publishedAt", type: "datetime" }),
defineField({ name: "body", type: "array", of: [{ type: "block" }] }),
],
});Then register it in sanity.config.ts at the root of your project:
import { defineConfig } from "sanity";
import { structureTool } from "sanity/structure";
import { post } from "./schemaTypes/post";
export default defineConfig({
projectId: "your-project-id",
dataset: "production",
plugins: [structureTool()],
schema: { types: [post] },
});Restart the dev server and visit /studio. You now have your own writing app. Write a test post and hit Publish.
Step 5:Show your posts on the site
In src/pages/index.astro, ask Sanity for your posts with a GROQ query (Sanity's query language), then list them:
---
import { sanityClient } from "sanity:client";
const posts = await sanityClient.fetch(
`*[_type == "post"] | order(publishedAt desc){ title, "slug": slug.current }`
);
---
<ul>
{posts.map((p) => <li><a href={`/blog/${p.slug}`}>{p.title}</a></li>)}
</ul>Refresh the homepage and your test post's title appears. That's the whole loop: write in the kitchen, Astro serves it in the dining room.
What could go wrong
- "Request blocked" or CORS errors in the browser. Sanity only answers requests from addresses it trusts. In your project's settings on sanity.io, add http://localhost:4321 (and later your real domain) under CORS origins.
- Studio errors after editing config. Double-check that the project ID and dataset in astro.config.mjs and sanity.config.ts match exactly. A typo in either sends you to the wrong kitchen.
- "I published, but my site didn't change!" That's the static bakery at work. The live site only updates when it's rebuilt. Locally the dev server refreshes for you, but in production you need a rebuild on every publish. Fixing that is the star of Part 2.
- Committing secrets. Your project ID is fine to share. API tokens are not. Keep tokens in a .env file and make sure .env is in your .gitignore.
What I'd tell past me
Don't let picking the stack become the project. I spent longer choosing tools than setting them up. Any modern stack would have worked; what matters is that the site exists and you write on it.
Right now this blog only runs on my computer. In Part 2, I'll put it on a real server with Cloudflare, connect my domain, add SSL, and set up a Sanity webhook so the site rebuilds itself every time I hit Publish.