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:
- Heavy dependency tree - My
node_modulesfolder was massive. Every update felt like a gamble. - GraphQL for everything - Even simple things like reading a markdown file required GraphQL queries.
- Slow builds - What should have been instant felt sluggish.
- 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:
git checkout -b gatsby-backupgit checkout -b astro-migrationThis way you can always go back if something goes wrong.
Step 2: Clean House
Remove all Gatsby-specific files. This includes:
gatsby-*.jsfiles (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 becomepublic/) - Your site configuration (you’ll convert this)
Step 3: Initialize Astro
Create a new package.json and install Astro:
{ "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:
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:
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:
| Gatsby | Astro |
|---|---|
template: post | (remove - not needed) |
date | pubDate |
slug | slug (optional) |
So this Gatsby frontmatter:
---template: posttitle: My Postdate: 2024-01-15T00:00:00.000Z---Becomes:
---title: My PostpubDate: 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:
---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:
---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:
---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:
[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:
| Metric | Gatsby | Astro |
|---|---|---|
| Build time | ~30-45 seconds | ~600ms |
| Direct dependencies | ~80 | 4 |
| node_modules size | ~500MB | 141MB |
| 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
-
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.
-
Content Collections are amazing - The type-safe markdown with Zod validation catches errors at build time instead of runtime.
-
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.
-
Keep your URLs - Make sure your post slugs match the old ones so you don’t break existing links.
-
Test locally first - Run
npm run build && npm run previewbefore 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.