Web Development

Rebuilding My Blog From Scratch

Why I threw away my over-engineered blog and started over with Astro, Bun, and a design system inspired by a certain island.

A wooden desk with a laptop, overlooking the ocean at sunset

Every developer rewrites their personal site eventually. This is the story of mine.

What went wrong the first time

The old blog was a Next.js app with a headless CMS, a GraphQL layer to talk to that CMS, and a component library I’d copied in from a client project because it “already had most of what I needed.” It did have most of what I needed. It also had a design tokens file I never opened, three different date-formatting utilities, and a /experiments route I’d built to try MDX plugins and then forgotten to delete. None of it was wrong, exactly. It just wasn’t mine anymore — every time I opened the repo to write a post, I spent the first twenty minutes remembering how the pieces fit together instead of writing.

If you can’t remember how your own blog works, the blog is the problem, not your memory.

The concrete failures, in roughly the order I noticed them:

  • A production bundle north of 300kb for pages that were, functionally, styled <article> tags
  • Content stored in a CMS I paid for monthly, to publish posts I wrote in Markdown anyway
  • Zero design system — colors and spacing were whatever felt right in the moment, per component
  • No dark mode, because retrofitting one meant touching every one of those per-component colors

I didn’t need a platform. I needed a place to write.

The philosophy this time

Astro was the obvious pick once I stopped trying to justify a framework and just counted the JavaScript my blog actually needs to ship, which is none. Everything renders to static HTML at build time; there’s no client runtime unless a component earns one. Bun replaces Node for the same reason — faster installs, one tool instead of three, less to configure before I get to write a single sentence.

Content lives in Markdown files, validated by a schema instead of a CMS:

const blog = defineCollection({
	loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
	schema: z.object({
		title: z.string().max(80, 'Long titles break layouts and SERPs'),
		publishedAt: z.coerce.date(),
		category: z.enum(['dev', 'lifestyle']),
		tags: z.array(z.string()).default([]),
	}),
})

That’s the whole publishing pipeline. Add a file, satisfy the types, done.

The design system is the part I’m actually proud of. Every color starts as an OKLCH primitive named for something on an island at dusk — sand, ink, dust, night, sunset, ocean, palm — and gets mapped to a semantic token like --color-surface or --color-accent that components consume. Light and dark mode aren’t two themes I maintain in parallel; they’re light-dark() picking a value out of the same primitive scale. Change the primitive, both themes update.

Simple. Elegant. Maintainable.