How I migrated my blog from Gatsby to Astro

A complete guide on how I migrated my personal blog from Gatsby to Astro. Learn the step-by-step process, the challenges I faced, and why Astro might be the right choice for your next static site.

I’ve been running this blog on Gatsby since 2020. It served me well, but lately I found myself dreading every time I needed to make changes. The build times were slow, the dependency tree was massive, and honestly, Gatsby felt like it was designed for a different era of web development.

So I decided to migrate to Astro. And I’m glad I did.

Why I Left Gatsby

Don’t get me wrong, Gatsby was revolutionary when it came out. The idea of using React to build static sites with GraphQL as a data layer was genuinely innovative. But over time, the complexity started to outweigh the benefits:

  1. Heavy dependency tree - My node_modules folder was massive. Every update felt like a gamble.
  2. GraphQL for everything - Even simple things like reading a markdown file required GraphQL queries.
  3. Slow builds - What should have been instant felt sluggish.
  4. Plugin ecosystem chaos - Plugins would break with updates, and finding compatible versions was frustrating.

Why Astro?

Astro takes a fundamentally different approach. Instead of shipping a JavaScript framework to the browser by default, it ships zero JavaScript. You only add interactivity where you need it.

Here’s what sold me:

  • Zero JS by default - Your pages are pure HTML until you explicitly add client-side JavaScript
  • Content Collections - Type-safe markdown/MDX with built-in validation
  • Simple file-based routing - No GraphQL, no complex configuration
  • Framework agnostic - Use React, Vue, Svelte, or nothing at all
  • Fast builds - My entire site builds in under a second

The Migration Process

Here’s the step-by-step process I followed. If you’re running a similar Gatsby blog, this should help you migrate smoothly.

Step 1: Create a Backup

Before touching anything, create a backup branch:

terminal
git checkout -b gatsby-backup
git checkout -b astro-migration

This way you can always go back if something goes wrong.

Step 2: Clean House

Remove all Gatsby-specific files. This includes:

  • gatsby-*.js files (config, node, browser, ssr)
  • The gatsby/ directory with page creation logic
  • React components in src/components/
  • Templates in src/templates/
  • GraphQL hooks in src/hooks/
  • SCSS/CSS modules
  • Jest tests and Flow types
  • Docker and CI/CD files you won’t need

Keep these:

  • Your markdown content (content/posts/, content/pages/)
  • Static assets (static/ - will become public/)
  • Your site configuration (you’ll convert this)

Step 3: Initialize Astro

Create a new package.json and install Astro:

package.json
{
"name": "your-blog",
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"dependencies": {
"astro": "^5.0.0",
"@astrojs/sitemap": "^3.0.0",
"@astrojs/rss": "^4.0.0"
}
}

Create astro.config.mjs:

astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://your-domain.com',
integrations: [sitemap()],
});

Step 4: Set Up Content Collections

This is Astro’s killer feature. Create src/content/config.ts:

src/content/config.ts
import { defineCollection, z } from 'astro:content';
const posts = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
category: z.string(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
export const collections = { posts };

Now move your markdown files to src/content/posts/.

Step 5: Update Your Frontmatter

Gatsby and Astro use slightly different frontmatter conventions. Here’s the mapping:

GatsbyAstro
template: post(remove - not needed)
datepubDate
slugslug (optional)

So this Gatsby frontmatter:

gatsby-post.md
---
template: post
title: My Post
date: 2024-01-15T00:00:00.000Z
---

Becomes:

astro-post.md
---
title: My Post
pubDate: 2024-01-15T00:00:00.000Z
---

Step 6: Create Your Layouts

Astro uses .astro files which combine HTML, CSS, and JavaScript in a single file. Create src/layouts/BaseLayout.astro:

src/layouts/BaseLayout.astro
---
import '../styles/global.css';
const { title, description } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>{title}</title>
<meta name="description" content={description} />
</head>
<body>
<slot />
</body>
</html>

The <slot /> is where your page content goes - similar to {children} in React.

Step 7: Create Your Pages

Astro uses file-based routing. Create src/pages/index.astro:

src/pages/index.astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';
const posts = await getCollection('posts', ({ data }) => !data.draft);
const sortedPosts = posts.sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
---
<BaseLayout title="My Blog">
<h1>Latest Posts</h1>
{sortedPosts.map((post) => (
<article>
<h2><a href={`/posts/${post.slug}`}>{post.data.title}</a></h2>
<p>{post.data.description}</p>
</article>
))}
</BaseLayout>

For dynamic routes like individual posts, create src/pages/posts/[...slug].astro:

src/pages/posts/[...slug].astro
---
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const posts = await getCollection('posts');
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>

Step 8: Update Netlify Configuration

If you’re on Netlify like me, update netlify.toml:

netlify.toml
[build]
command = "npm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "20"

That’s it. Netlify will automatically detect Astro and deploy your site.

The Results

The migration took me about a day. Here’s what changed:

MetricGatsbyAstro
Build time~30-45 seconds~600ms
Direct dependencies~804
node_modules size~500MB141MB
JavaScript shipped~150KB (React runtime + app)0 bytes

The JavaScript difference is the killer stat here. Gatsby ships React to every page by default. Astro ships nothing unless you explicitly add client-side interactivity with client: directives.

Things I Learned

  1. Start fresh, don’t port - Don’t try to convert your React components to Astro one-by-one. Start with a blank slate and build what you need.

  2. Content Collections are amazing - The type-safe markdown with Zod validation catches errors at build time instead of runtime.

  3. You probably don’t need React - For a blog, Astro components are more than enough. I only have a small script for dark mode toggle.

  4. Keep your URLs - Make sure your post slugs match the old ones so you don’t break existing links.

  5. Test locally first - Run npm run build && npm run preview before deploying.

Should You Migrate?

If you have a content-focused site (blog, docs, portfolio) and you’re frustrated with your current setup, yes. Astro is genuinely simpler and faster.

If you have a highly interactive application with lots of client-side state, Astro might not be the right fit. Stick with Next.js or Remix.

For me, it was the right call. The site is faster, the codebase is simpler, and I actually enjoy making updates now.

The best framework is the one that gets out of your way and lets you focus on content. For me, that’s Astro.