Building a Blog with Astro and Sveltia CMS in One Day

Published on: 2026-02-27 · #astro

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

plaintext
UTF-8|15 Lines|
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.astro

Files 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:

TypeScript
UTF-8|9 Lines|
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:

Astro
UTF-8|16 Lines|
---
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:

Markdown
UTF-8|11 Lines|
---
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

CSS
UTF-8|4 Lines|
.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:

HTML
UTF-8|10 Lines|
<!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

YAML
UTF-8|30 Lines|
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

Bash
UTF-8|2 Lines|
npx netlify-cms-proxy-server   # terminal 1
npm run dev                     # terminal 2

Open 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:

  1. Set local_backend: false
  2. Create a GitHub OAuth app
  3. Deploy an OAuth handler
  4. Point your config at it
  5. Open /admin on your live URL
  6. Authenticate with GitHub
  7. 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.


JavaScript
UTF-8|13 Lines|
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.