The Day I Stopped Overthinking Blogging
I’ve built blog systems before. Multiple times. Different frameworks, different approaches:
-
The Database Approach: “We’ll use PostgreSQL + GraphQL + a CMS API”
- Built a whole backend. Deployed it. Paid for hosting.
- Nobody updated the blog for 6 months anyway.
-
The Headless CMS Approach: “We’ll use Strapi/Contentful/Sanity”
- Another service to manage. Another API to call. Another dashboard to learn.
- Content lives somewhere else. Syncing nightmares.
-
The Manual API Approach: “We’ll build custom endpoints”
- Spent hours on REST architecture that nobody cared about.
- Still slow. Still complicated.
Then I discovered Astro Content Collections and realized I’d been solving the wrong problem the entire time.
What Changed Everything
Here’s the thing: A blog doesn’t need a database. It needs files and structure.
Astro Content Collections take markdown files, apply TypeScript validation, generate type-safe routes, and handle all the metadata automatically. That’s it. That’s the whole system.
No backend. No database. No API. Just markdown + a config file.
How It Actually Works
Step 1: Define Your Collection Schema
Create src/content.config.ts:
import { defineCollection, z } from 'astro:content';
const blogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
tags: z.array(z.string()),
category: z.string(),
featured: z.boolean().optional(),
}),
});
export const collections = {
blog: blogCollection,
};
That’s your schema. One file. TypeScript validates everything.
Step 2: Write Posts as Markdown
Create src/content/blog/my-post.md:
---
title: "My Awesome Post"
description: "This is about something cool"
pubDate: 2026-03-28
tags: ["astro", "web-dev"]
category: "Dev Guide"
featured: true
---
# My Awesome Post
Here's the content...
No database entry. No CMS form. Just a file in your repo.
Step 3: Query Your Posts Programmatically
In any .astro page:
---
import { getCollection } from 'astro:content';
const allPosts = await getCollection('blog');
const publishedPosts = allPosts
.filter(post => post.data.featured)
.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());
---
<div>
{publishedPosts.map(post => (
<article>
<h2>{post.data.title}</h2>
<p>{post.data.description}</p>
<a href={`/blog/${post.slug}`}>Read More →</a>
</article>
))}
</div>
That’s it. You get:
- Type-safe access to post metadata
- Automatic slug generation from filenames
- Filtered, sorted results
- All at build time
Step 4: Generate Individual Post Pages
Create src/pages/blog/[...slug].astro:
---
import { getCollection } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
interface Props {
post: CollectionEntry<'blog'>;
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<BlogPost {...post.data}>
<Content />
</BlogPost>
Astro automatically generates a page for every post. No route definitions. No dynamic rendering. No API calls at runtime.
Why This Is Absolutely Brilliant
1. Type Safety
Your schema validates at build time. If a post is missing a required field, Astro throws an error during build.
// This is enforced at build time
const title: string = post.data.title; // ✅ TypeScript knows it's always a string
const pubDate: Date = post.data.pubDate; // ✅ Always a Date
No runtime errors. No “oops, that field was undefined.”
2. Zero Runtime Overhead
Content Collections are processed at build time, not at request time:
- Posts are compiled to static HTML
- No database queries on every visit
- No API round trips
- Your site is just static files
Result: Blazing fast. Cacheable. CDN-friendly.
3. Git as Your CMS
Your blog lives in your repository. Every post is version controlled:
git log -- src/content/blog/
# See the history of your blog
git diff blog/post.md
# See exactly what changed
git revert <commit>
# Undo a post in one command
No proprietary CMS. No lock-in. No migration pain.
4. Markdown + React Components
Write posts in Markdown, but embed React/Astro components:
---
title: "Interactive Demo"
---
# Here's a regular markdown heading
<MyReactComponent />
More markdown below...
You get the simplicity of Markdown with the power of components.
5. Automatic Slugs
File: 2026-03-28-my-awesome-post.md
Route: /blog/2026-03-28-my-awesome-post
Astro uses your filename as the URL slug. No configuration needed.
Real Example: This Blog
Here’s exactly how I structured this site’s blog:
File Structure
src/
├── content/
│ └── blog/
│ ├── 2026-03-28-astro-content-collections-game-changer.md
│ ├── 2026-03-28-snipcart-astro-integration-guide.md
│ ├── 2026-03-28-ai-exosuit-debugging-popup.md
│ └── [50+ more posts]
├── layouts/
│ └── BlogPost.astro
├── pages/
│ └── blog/
│ ├── index.astro (list all posts)
│ └── [...slug].astro (individual post page)
└── content.config.ts (schema definition)
Schema Definition
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
const blogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.date(),
tags: z.array(z.string()),
category: z.string(),
featured: z.boolean().optional(),
}),
});
export const collections = {
blog: blogCollection,
};
Blog List Page
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import BlogCard from '../../components/BlogCard.astro';
const allPosts = await getCollection('blog');
const publishedPosts = allPosts.sort(
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()
);
---
<BaseLayout title="Blog">
<div class="posts-grid">
{publishedPosts.map(post => (
<BlogCard post={post} />
))}
</div>
</BaseLayout>
Individual Post Page
---
// src/pages/blog/[...slug].astro
import { getCollection } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';
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();
---
<BlogPost {...post.data}>
<Content />
</BlogPost>
That’s your entire blog system. The complexity is gone.
The Developer Experience Just Hits Different
Writing a New Post
- Create a new
.mdfile insrc/content/blog/ - Add frontmatter with your metadata
- Write content
- Save
npm run devhot-reloads- See it live instantly
No database migrations. No form inputs. No deploy wait times. Just write.
Updating Posts
Edit the file. Watch it update in real-time in dev mode. Commit. Deploy. Done.
Publishing Schedule
Want to publish posts on a specific date? Add a filter:
---
const allPosts = await getCollection('blog');
const now = new Date();
const published = allPosts.filter(post => post.data.pubDate <= now);
---
Posts with future dates won’t show until that date passes. No scheduled publishing service needed.
Exporting to Other Platforms
Your content is just Markdown in a Git repo. Export to Medium, Dev.to, Substack—whatever:
cp src/content/blog/*.md ~/my-substack-export/
Your content isn’t locked into Astro. It’s just text files.
Performance Numbers
Because everything is static:
- Build time: 2-3 seconds (regardless of post count)
- Site speed: ~50-100ms TTL (it’s just HTML)
- Lighthouse: 100/100 performance
- Bandwidth: ~50KB per post (mostly the layout, not the content)
Compared to a typical Next.js blog with database:
- Build time: 30-60 seconds
- TTFB: 200-500ms (database queries on every visit)
- Lighthouse: 80-90 performance (need caching layers)
- Cost: Server + database + CDN
Astro: One static file host. That’s it.
What About Comments?
“But what if I want comments on my blog?”
You have options:
- Disqus - Embed a script, comments hosted elsewhere
- Giscus - Comments powered by GitHub discussions
- Utterances - Comments as GitHub issues
- Custom backend - Webhook to your own service
All of these work with Astro because you’re just embedding a script. Your static blog doesn’t change.
The Plot Twist
The craziest part? This was always the answer.
I have spent decades building comment systems, database backends, and admin panels for blogs and other types of systems… when the best blogs are just well-organized Markdown files.
WordPress solved a problem that didn’t actually exist.
Astro is the correction.
One More Thing: RSS Feeds
Astro can auto-generate RSS feeds from your content collection:
---
// src/pages/rss.xml.js
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';
export async function GET(context) {
const posts = await getCollection('blog');
return rss({
title: 'My Blog',
description: 'All my thoughts',
site: context.site,
items: posts.map(post => ({
title: post.data.title,
pubDate: post.data.pubDate,
description: post.data.description,
link: `/blog/${post.slug}/`,
})),
});
}
People can subscribe with their RSS reader. Your blog gets a feed automatically. No effort.
The Bottom Line
Content Collections are the feature that makes Astro feel different from every other static site generator.
It’s not just “write markdown + get HTML.” It’s:
- Type-safe content
- Scalable to thousands of posts
- Queryable with full JavaScript power
- Version controlled in Git
- Deploy anywhere (no backend required)
You stop thinking about infrastructure and start thinking about content.
That’s when web development gets fun again.
Now go write some posts. 🚀
Comments
Loading comments...
Leave a Comment