Building a Blog with Astro and Sveltia CMS in One Day
Why
You wanted a blog. Not a Medium account. Not a Substack. A blog you own, on a domain you pay for, hosted on infrastructure that will outlive your motivation to write. You can also pretend you’re enlightened.
Astro and Sveltia CMS will get you there in a day. A long day.
Morning: Astro - The Static Site That Respects Your Time
Astro ships zero JavaScript by default. The documentation sells it much better than I can.
The Architecture
I’m not a designer. I just prefer setups that are hard to break by accident because I will absolutely test that theory (usually by accident). (If you’ve seen the “bits” page, you already know my taste can skew a little morose. Consider this me staying in my lane.)
Astro clicked for me because it is straightforward: markdown goes in, typed collections complain when your frontmatter is nonsense, and file routing keeps you from inventing a routing framework in your free time. Looking at you webdev ecosystem
That being said on we go.
Project Structure
src/
content/
essay/ # long form posts (like this one)
bits/ # short posts, photos (my attempt to convince my wife and friends that I'm a good photographer)
layouts/
BaseLayout.astro
ArticleLayout.astro
pages/
index.astro
essay/
[...slug].astro
bits/
index.astro
archive/
index.astroFiles in pages/ become routes. No router config. No middleware chain. Put a file there, it becomes a page.
Content Collections
Markdown files have frontmatter, the metadata block at the top. Astro validates it against a typed schema:
const essay = defineCollection({
schema: z.object({
title: z.string(),
description: z.string().optional(),
date: z.coerce.date(),
tags: z.array(z.string()).optional(),
draft: z.boolean().optional().default(false),
}),
});Misspell date as dat? Build fails.
Forget a title? Build fails.
Forget why you’re writing a blog instead of doing literally anything else? That one compiles fine, unfortunately.
Dynamic Routes
Each essay gets its own page, generated at build time:
---
export async function getStaticPaths() {
const essays = await getCollection("essay", ({ data }) => !data.draft);
return essays.map((entry) => ({
params: { slug: entry.id },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await render(entry);
---
<ArticleLayout frontmatter={entry.data}>
<Content />
</ArticleLayout>No GraphQL. No REST API. No MongoDB. Query a collection, generate paths, render markdown. The kind of simplicity that makes you suspicious.
Bites - The Short Posts
Not everything needs 2,000 words. Sometimes you just want to post a photo with a caption that makes you sound more contemplative than you are:
---
date: 2026-02-03T01:01:00
tags: ["photography", "night"]
images:
- src: ./night-walk.jpg
width: 1920
height: 1080
alt: "City lights at 1am"
---
Couldn't sleep. Went for a walk.(so original)Micro blogging without a platform. No algorithm deciding who sees it. No engagement metrics. Just a thought, on your site, existing quietly. The way the internet used to work.
Am I too old bruh?
Lunch: Styling
.prose {
max-width: 72ch;
line-height: 1.7;
}72 characters wide (Twitter/2 — oh wait, it’s X now). Allegedly optimal for reading. Dark mode support. Responsive layout.
I spent more time on the content pipeline than the CSS. That tells you everything about my priorities as an engineer and my limitations as a visual thinker. The site looks good. I couldn’t tell you why in precise terms. It just does. Sometimes my genius is almost frightening. (TopGear)
Afternoon: Sveltia CMS - The Admin Panel
Writing markdown in VS Code is fine. But sometimes you want a GUI with buttons and fields, like a person who doesn’t think terminal proficiency is a personality. That was for vim users.
Sveltia CMS is a drop in replacement for Decap/Netlify CMS. Faster, actively maintained, and doesn’t require a React runtime to let you edit a blog post.
Also, Sveltia I need a major release brother, its been 10000 years.
Setup
Two files:
public/admin/index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Admin</title>
</head>
<body>
<script src="https://unpkg.com/@sveltia/cms/dist/sveltia-cms.js"></script>
</body>
</html>public/admin/config.yml - this file you’ll edit seventeen times min
backend:
name: github
repo: username/repo
branch: main
local_backend: true
media_folder: public/images
public_folder: /images
collections:
- name: essay
label: Essays
folder: src/content/essay
create: true
fields:
- { label: "Title", name: "title", widget: "string" }
- { label: "Description", name: "description", widget: "string", required: false }
- { label: "Date", name: "date", widget: "datetime" }
- { label: "Tags", name: "tags", widget: "list", required: false }
- { label: "Draft", name: "draft", widget: "boolean", default: false }
- name: bits
label: Bits
folder: src/content/bits
create: true
fields:
- { label: "Date", name: "date", widget: "datetime" }
- { label: "Tags", name: "tags", widget: "list", required: false }
- { label: "Draft", name: "draft", widget: "boolean", default: false }The full config includes image arrays, author objects, badge labels, and media folder overrides per collection. But this is the skeleton. Get this working first. Celebrate.
Then comes sorrow. YAML. The less I talk about it the better I feel.
Local Development
npx netlify-cms-proxy-server # terminal 1
npm run dev # terminal 2Open http://localhost:4321/admin/. You now have a CMS that reads and writes your local files. No GitHub auth.(not yet)
No deploy cycle.
Just you, your markdown, and existential dread hovering in the background.
Production
For the live site:
- Set
local_backend: false - Create a GitHub OAuth app
- Deploy an OAuth handler
- Point your config at it
- Open
/adminon your live URL - Authenticate with GitHub
- Write something (the hardest step, always)
The CMS commits to your repo. Amplify rebuilds. Your post goes live. wow
The RSS Feed
Because some people still use RSS readers. God bless every one of them.
export async function GET(context) {
const essays = await getCollection("essay", ({ data }) => !data.draft);
return rss({
title: "My Blog",
description: "Words on the internet",
site: context.site,
items: essays.map((post) => ({
title: post.data.title,
pubDate: post.data.date,
link: `/essay/${post.id}/`,
})),
});
}Twelve lines. You now syndicate content. Somewhere, Aaron Swartz is smiling at what RSS made possible.
And somewhere, Aaron Kolanti is refreshing his feed stats to see if the subscriber count has moved from zero.
Two Aarons.
Very different legacies. But you work with what you’ve got.
Final Thoughts
You don’t need Next.js for a blog. You don’t need a database. You don’t need server side rendering for content that changes when inspiration strikes, which is less often than you promised yourself.
You need Astro, a CMS config file, and one day you won’t get back. But let’s be honest, you were going to spend it scrolling anyway. At least now you have a blog to not write on.
Cheers.