1dbd507
← Back to posts

Why I Moved This Blog to Astro

Why I replaced a custom React/Webpack blog setup with Astro, and what got simpler in the process.

I recently migrated this blog from a custom React/Webpack setup to Astro. The old build pipeline worked — until it became easier to freeze dependencies than to risk breaking the build. The custom glue tying together file discovery, frontmatter parsing, and Webpack loaders was tightly coupled enough that every dependency update felt risky — the setup even required a maintained patch-package patch just to keep syntax highlighting working. Rather than untangle it incrementally, I wanted to move to a purpose-built, actively maintained framework. Astro had been gaining traction and looked worth experimenting with.

Previous Architecture (React + Webpack)

The old blog used:

  • React + React Router for rendering and routing
  • Webpack + Babel for bundling/transpilation
  • val-loader + glob + gray-matter to build article metadata
  • Markdown loading via mdx-loader

None of these tools were bad individually, but combining them meant maintaining a lot of glue code. Just to publish Markdown posts reliably, I had to manage:

  • file discovery rules
  • frontmatter parsing
  • publish filtering and sorting
  • MDX compilation setup
  • loader/interoperability behavior

A simplified part of the old metadata pipeline looked like this:

// makePostList.val.js (simplified)
glob('content/blog/*/+([0-9])_*.md', (_, files) => {
  const posts = files
    .map(matter.read)
    .map((file, i) => ({ ...file.data, filepath: files[i] }))
    .filter((p) => p.published)
    .sort((a, b) => (b.updated || b.date) - (a.updated || a.date));

  resolve({ code: 'module.exports = ' + JSON.stringify(posts) });
});

This approach gave full control, but required custom build glue for features that are now common in static site frameworks.

And that was only metadata. On top of that, Webpack + MDX loader integration had to stay healthy across dependency upgrades, config changes, and ecosystem churn.

A concrete example: mdx-loader had a bug in its Prism syntax highlighting code — a one-line fix, but the upstream PR was never merged. And because mdx-loader lives in an NPM monorepo, npm can’t install it directly from a fork subdirectory. The only option was patch-package: a diff committed to the repo and re-applied to node_modules on every npm install via a postinstall hook. That patch stayed in the repo for years. I wrote about the whole episode here.

Old content pipeline (Markdown/MDX to browser)

  1. Author writes a Markdown/MDX file in content/blog/....
  2. glob finds candidate files and gray-matter parses frontmatter.
  3. Custom logic filters published posts, sorts them, and generates metadata.
  4. val-loader injects that generated metadata into the React app at build time.
  5. Webpack processes Markdown/MDX through mdx-loader into JS modules.
  6. React Router maps URLs to the right article module.
  7. Browser loads bundled JS; React hydrates and renders article content.

So the final page depended on several custom integration points between file discovery, metadata generation, loader config, routing, and client rendering.

New Architecture (Astro)

The old stack needed custom solutions for problems that are standard in static site generation. Astro handles them natively.

Content Collections replace glob + gray-matter + custom filters. You define a frontmatter schema in content.config.ts; Astro handles file discovery and validation. Filtering and sorting become straightforward:

import { getCollection } from 'astro:content';

const posts = (await getCollection('blog'))
  .filter((post) => post.data.published !== false)
  .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());

File-based routing replaces React Router. A single src/pages/blog/[...slug].astro handles all post URLs, with post data injected at build time.

MDX support is built in via @astrojs/mdx — no custom Webpack loader config.

Static rendering is the default. Pages are pre-rendered at build time and served as plain HTML. No client-side JS needed to render content.

RSS and sitemap are first-party integrations that pull from the same content source.

New content pipeline (Markdown/MDX to browser)

  1. Author writes a Markdown/MDX file in src/content/blog/....
  2. Astro Content Collections discover files and validate frontmatter schema in content.config.ts.
  3. getCollection('blog') provides typed post data at build time.
  4. Astro builds static HTML per route (for list and post pages).
  5. RSS and sitemap are generated from the same content source.
  6. Browser receives mostly static HTML/CSS (minimal JS), and article content is already rendered.

The key difference: in Astro, the core content pipeline is built-in and typed, instead of being custom glue around Webpack loaders.

Why Astro Is a Better Fit Here

For a content site, the fit is natural:

  • No custom file discovery, frontmatter parsing, or loader glue
  • Static HTML by default — a better model for a reading experience than a client-side React bundle
  • First-class Markdown and MDX support
  • The content pipeline is maintained by the framework, not hand-rolled

The goal was to get off a fragile, hard-to-update setup and onto something purpose-built and actively maintained. Astro delivered that.