All Articles

Migrating My Blog from Gatsby to Astro with GLM 5.2

My last blog post was in November 2020. That was over five years ago. The world went through a pandemic, I changed jobs, and life happened. The blog sat there on its aging Gatsby 2 stack — React 16, Flow types, node-sass, Node 10 in CI — slowly rotting but never quite breaking enough to force my hand.

Every time I thought about writing again, I’d open the repo, see 300+ outdated dependencies, and close it. The friction wasn’t in the writing — it was in the stack. I didn’t want to publish on a framework I couldn’t even npm install without warnings.

So before I could write again, I needed to fix the foundation. This is the story of how I used GLM 5.2 (an AI coding assistant) to migrate the entire blog to Astro 5 and deploy it on Cloudflare Pages in a single session — and finally get back to writing.

Why Astro?

I considered Next.js and Astro. The decision came down to one question: what does this site actually need?

My blog is content-first. Markdown posts, tag archives, category pages, an RSS feed. No authentication, no server-side rendering, no dynamic data. It’s the textbook use case for Astro:

  • Content collections with typed schemas replaced Gatsby’s GraphQL layer
  • Zero JS by default — no React runtime shipped to the browser
  • Static output that deploys anywhere
  • Native SCSS support without the PostCSS/Lost Grid pile

Next.js would have worked too, but it would have shipped React for no reason. Astro ships HTML and CSS. That’s it.

The Migration Process

1. Inventory First

The first thing GLM 5.2 did was catalog everything before touching a single file:

  • 19 blog posts with explicit frontmatter slugs (some with posts/ prefix, some without)
  • 2 standalone pages (/about, /contact)
  • 26 unique tags and 9 categories (some comma-separated strings like "gatsby, react")
  • Images split across /media/ and /images/ directories
  • Disqus comments keyed by post title
  • A Decap CMS (formerly Netlify CMS) admin panel

This inventory was critical. Every URL had to be preserved exactly — including the typo in migrationg-from-hugo-to-gatsby and the mixed case in tp-link-ac1300-on-Ubuntu-20.

2. Scaffolding

GLM 5.2 created the Astro project structure from scratch:

src/
├── components/    # Feed, Post, Sidebar, ThemeToggle, etc.
├── layouts/       # BaseLayout with dark mode support
├── pages/         # All routes
├── styles/        # SCSS with CSS custom properties
├── consts.ts      # Site configuration
├── content.ts     # Content query helpers
└── content.config.ts  # Collection schemas

Content collections read directly from the existing content/posts/ and content/pages/ directories — no need to move or reformat any markdown.

3. URL Preservation

This was the hardest part. The original Gatsby code used lodash’s kebabCase() to generate tag and category URLs. Astro doesn’t ship lodash, so GLM 5.2 had to write a compatible implementation:

export function kebabCase(str: string): string {
  return str
    .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
    .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
    .replace(/([a-zA-Z])([0-9])/g, '$1-$2')
    .replace(/[^a-zA-Z0-9]+/g, '-')
    .replace(/^-+|-+$/g, '')
    .toLowerCase();
}

Without this, ES6 would become es6 instead of lodash’s es-6, and TodayILearned would become today-ilearned instead of today-i-learned. Those URL mismatches would silently break every inbound link.

4. Killing Render-Blocking Scripts

The original site had two performance killers:

  1. Google Analytics with a dead UA tracking ID — Universal Analytics was shut down in 2023, but the script was still loading in <head>, making pointless network requests
  2. Disqus loading eagerly on every post — 20+ requests before the user even scrolled to comments

The fix:

  • Removed GA entirely (will use Cloudflare Web Analytics instead — free, privacy-friendly, no JS payload)
  • Wrapped Disqus in an IntersectionObserver that only loads when the comments section scrolls into view, with a “Load Comments” button as fallback
var observer = new IntersectionObserver(
  function (entries) {
    if (entries[0].isIntersecting) {
      loadDisqus();
      observer.disconnect();
    }
  },
  { rootMargin: '200px' },
);
observer.observe(container);

Moving to Cloudflare Pages

The blog was previously on Netlify. Moving to Cloudflare Pages required a few changes:

Static Assets

Gatsby uses a static/ directory. Astro uses public/. Simple git mv and everything resolved.

Decap CMS Authentication

This was the trickiest part. On Netlify, Decap CMS used Git Gateway via Netlify Identity — a managed auth provider. Cloudflare doesn’t have an equivalent, so the CMS needs a GitHub OAuth proxy.

The config changed from:

backend:
  name: git-gateway

to:

backend:
  name: github
  repo: brijmcq/bri-blog
  base_url: https://decap-oauth.brijumaquio.workers.dev
  auth_endpoint: /auth

You need to deploy a small Cloudflare Worker that handles the OAuth flow. There are open-source templates for this — search for “decap cms oauth cloudflare worker.”

Deployment

Deployment is just:

npx wrangler pages deploy dist --project-name=bri-blog

Or connect the GitHub repo in the Cloudflare dashboard for automatic builds on push.

What I Learned

SCSS Modernization is Not Optional

Astro’s Sass compiler uses modern Dart Sass, which deprecates @import, lighten(), and / for division. The old Lumen theme used all three extensively. GLM 5.2 had to:

  • Replace @import with @use and @forward
  • Replace lighten($color, 40%) with color.adjust($color, $lightness: 40%)
  • Replace percentage(5 / 12) with hardcoded 41.6667%

CSS Custom Properties Enable Dark Mode for Free

Instead of maintaining separate SCSS color variables, I defined everything as CSS custom properties:

:root {
  --color-bg: #ffffff;
  --color-text: #1a1a2e;
}

[data-theme='dark'] {
  --color-bg: #0d1117;
  --color-text: #e6edf3;
}

Then SCSS variables just reference the CSS props:

$color-base: var(--color-text);

One toggle button swaps the data-theme attribute. No flash on reload thanks to an inline script in <head> that checks localStorage before paint.

AI-Assisted Migration is Fast but Needs Verification

GLM 5.2 completed the full migration — 67 pages, all routes, RSS, sitemap, Decap CMS, dark mode, card UI, performance fixes — in a single session. But it still needed human oversight on:

  • Draft filtering logic — the Zod schema defaulted draft to true, which hid 9 posts that didn’t have an explicit draft: false in frontmatter
  • Kebab-case compatibility — the initial implementation didn’t match lodash’s behavior for edge cases like ES6
  • Trailing slash redirects — Cloudflare Pages uses 308 redirects where Netlify served both variants

The lesson: AI gets you 90% there instantly. The last 10% is verifying edge cases you didn’t know existed.

The Results

MetricBefore (Gatsby)After (Astro)
FrameworkGatsby 2 (dead)Astro 5 (active)
JS shippedReact 16 + runtimeZero
Render-blocking scriptsGA + DisqusNone
CSS size~15KB~11KB
Code highlightingPrismJS (client)Shiki (build-time)
Dark modeNoYes
Deploy targetNetlifyCloudflare Pages

The site loads instantly now. No spinner, no jank, no React hydration. Just HTML and CSS, the way the web was meant to work.


If you’re sitting on an aging Gatsby blog, the migration is more feasible than you think. Astro is purpose-built for this use case, and tools like GLM 5.2 can handle the heavy lifting. Just remember to verify your URLs.

As for me — five years is a long gap between posts. But the stack is modern now, the friction is gone, and I’m looking forward to writing again. More posts coming soon.