Migration Next.js Astro

Migrating from Next.js to Astro: A Complete Guide

Step-by-step guide for migrating your Next.js application to Astro with minimal downtime, including routing, data fetching, and deployment strategies.

Mike Chen
Mike Chen
12 min read

Migrating from Next.js to Astro: A Complete Guide

Migrating from Next.js to Astro can significantly improve your website’s performance while maintaining the developer experience you love. This comprehensive guide will walk you through the entire migration process, from planning to deployment.

Why Migrate to Astro?

Performance Benefits

  • Zero JavaScript by Default: Only ship JavaScript when needed
  • Faster Build Times: Astro’s optimized build process
  • Better Core Web Vitals: Improved LCP, FID, and CLS scores
  • Smaller Bundle Sizes: Reduced client-side JavaScript

Developer Experience

  • Component Islands: Use React, Vue, Svelte together
  • Familiar Syntax: Similar to JSX with enhanced features
  • Built-in Optimizations: Image optimization, CSS bundling
  • TypeScript Support: First-class TypeScript integration

Pre-Migration Assessment

Analyze Your Current Next.js App

Before starting the migration, audit your existing application:

# Analyze bundle size
npx @next/bundle-analyzer

# Check dependencies
npm list --depth=0

# Review page structure
find pages -name "*.js" -o -name "*.tsx" | wc -l

Compatibility Checklist

  • Static pages vs dynamic pages
  • API routes usage
  • Client-side routing requirements
  • Third-party integrations
  • Custom webpack configuration
  • Middleware usage

Step 1: Project Setup

Initialize Astro Project

# Create new Astro project
npm create astro@latest my-astro-site

# Choose template
 How would you like to start your new project? Just the basics
 Install dependencies? Yes
 Initialize a new git repository? Yes
 TypeScript? Yes

Install React Integration

Since you’re migrating from Next.js, you’ll likely want to keep using React:

npx astro add react
npx astro add tailwind  # If using Tailwind CSS

Project Structure Comparison

Next.js Structure:
pages/
├── _app.js
├── _document.js
├── index.js
├── about.js
└── api/
    └── users.js

Astro Structure:
src/
├── layouts/
│   └── Layout.astro
├── pages/
│   ├── index.astro
│   └── about.astro
├── components/
└── content/

Step 2: Layout Migration

Convert _app.js to Layout

Next.js _app.js:

import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  return (
    <div>
      <header>My Site</header>
      <Component {...pageProps} />
      <footer>© 2024</footer>
    </div>
  )
}

export default MyApp

Astro Layout.astro:

---
export interface Props {
  title: string;
}

const { title } = Astro.props;
---

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{title}</title>
  </head>
  <body>
    <header>My Site</header>
    <main>
      <slot />
    </main>
    <footer>© 2024</footer>
  </body>
</html>

Step 3: Page Migration

Static Pages

Next.js page:

// pages/about.js
import Head from 'next/head'

export default function About() {
  return (
    <>
      <Head>
        <title>About Us</title>
      </Head>
      <div>
        <h1>About Us</h1>
        <p>Welcome to our company!</p>
      </div>
    </>
  )
}

Astro page:

---
// src/pages/about.astro
import Layout from '../layouts/Layout.astro';
---

<Layout title="About Us">
  <div>
    <h1>About Us</h1>
    <p>Welcome to our company!</p>
  </div>
</Layout>

Dynamic Pages with getStaticProps

Next.js dynamic page:

// pages/blog/[slug].js
export async function getStaticProps({ params }) {
  const post = await fetchPost(params.slug);
  return { props: { post } };
}

export async function getStaticPaths() {
  const posts = await fetchAllPosts();
  return {
    paths: posts.map(post => ({ params: { slug: post.slug } })),
    fallback: false
  };
}

export default function BlogPost({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

Astro dynamic page:

---
// src/pages/blog/[slug].astro
import Layout from '../../layouts/Layout.astro';

export async function getStaticPaths() {
  const posts = await fetchAllPosts();
  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post }
  }));
}

const { post } = Astro.props;
---

<Layout title={post.title}>
  <article>
    <h1>{post.title}</h1>
    <p>{post.content}</p>
  </article>
</Layout>

Step 4: Component Migration

Convert React Components

Most React components can be used directly in Astro with the React integration:

Create a React component:

// src/components/Counter.jsx
import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

Use in Astro page:

---
// src/pages/interactive.astro
import Layout from '../layouts/Layout.astro';
import Counter from '../components/Counter.jsx';
---

<Layout title="Interactive Page">
  <h1>Interactive Elements</h1>
  <Counter client:load />
</Layout>

Client Directives

Astro’s client directives control when components hydrate:

  • client:load - Hydrate immediately
  • client:idle - Hydrate when browser is idle
  • client:visible - Hydrate when component is visible
  • client:media - Hydrate based on media query

Step 5: Data Fetching Migration

Static Data Fetching

Next.js getStaticProps:

export async function getStaticProps() {
  const data = await fetch('https://api.example.com/data');
  return { props: { data } };
}

Astro frontmatter:

---
const response = await fetch('https://api.example.com/data');
const data = await response.json();
---

<Layout title="Data Page">
  <div>{JSON.stringify(data)}</div>
</Layout>

Content Collections

For blog posts and similar content, use Astro’s Content Collections:

// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    publishDate: z.date(),
    tags: z.array(z.string()),
  }),
});

export const collections = { blog };
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro';

const allPosts = await getCollection('blog');
---

<Layout title="Blog">
  <h1>Blog Posts</h1>
  {allPosts.map(post => (
    <article>
      <h2><a href={`/blog/${post.slug}`}>{post.data.title}</a></h2>
      <p>{post.data.description}</p>
    </article>
  ))}
</Layout>

Step 6: API Routes Migration

Simple API Routes

Next.js API route:

// pages/api/hello.js
export default function handler(req, res) {
  res.status(200).json({ message: 'Hello World' });
}

Astro API route:

// src/pages/api/hello.ts
export async function GET() {
  return new Response(JSON.stringify({ message: 'Hello World' }), {
    status: 200,
    headers: {
      'Content-Type': 'application/json'
    }
  });
}

Complex API Routes

// src/pages/api/users/[id].ts
export async function GET({ params }) {
  const { id } = params;
  const user = await fetchUser(id);
  
  if (!user) {
    return new Response(null, { status: 404 });
  }
  
  return new Response(JSON.stringify(user), {
    headers: { 'Content-Type': 'application/json' }
  });
}

export async function POST({ request }) {
  const data = await request.json();
  const user = await createUser(data);
  
  return new Response(JSON.stringify(user), {
    status: 201,
    headers: { 'Content-Type': 'application/json' }
  });
}

Step 7: Styling Migration

CSS Modules

Next.js CSS Modules:

/* styles/Home.module.css */
.container {
  padding: 2rem;
}
import styles from '../styles/Home.module.css';

export default function Home() {
  return <div className={styles.container}>Content</div>;
}

Astro Scoped Styles:

---
// src/pages/index.astro
---

<div class="container">Content</div>

<style>
  .container {
    padding: 2rem;
  }
</style>

Global Styles

---
// src/layouts/Layout.astro
import '../styles/global.css';
---

<html>
  <!-- Layout content -->
</html>

Step 8: Image Optimization

Next.js Image Component

Next.js:

import Image from 'next/image';

<Image
  src="/hero.jpg"
  alt="Hero image"
  width={800}
  height={600}
  priority
/>

Astro:

---
import { Image } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---

<Image
  src={heroImage}
  alt="Hero image"
  width={800}
  height={600}
  loading="eager"
/>

Step 9: Routing Migration

File-based Routing

Both Next.js and Astro use file-based routing, but with some differences:

Next.js:
pages/blog/[slug].js → /blog/:slug
pages/blog/[...slug].js → /blog/*

Astro:
src/pages/blog/[slug].astro → /blog/:slug
src/pages/blog/[...slug].astro → /blog/*

Dynamic Routes

---
// src/pages/products/[category]/[id].astro
export async function getStaticPaths() {
  return [
    { params: { category: 'electronics', id: '1' } },
    { params: { category: 'books', id: '2' } },
  ];
}

const { category, id } = Astro.params;
---

<Layout title={`Product ${id} in ${category}`}>
  <h1>Product {id}</h1>
  <p>Category: {category}</p>
</Layout>

Step 10: Environment Variables

Configuration

Next.js:

// next.config.js
module.exports = {
  env: {
    CUSTOM_KEY: process.env.CUSTOM_KEY,
  },
}

Astro:

// astro.config.mjs
export default defineConfig({
  // Configuration
});

Usage

---
// Access environment variables
const apiKey = import.meta.env.PUBLIC_API_KEY;
const secretKey = import.meta.env.SECRET_KEY; // Server-side only
---

Step 11: Testing Migration

Component Testing

// tests/components/Counter.test.jsx
import { render, fireEvent } from '@testing-library/react';
import Counter from '../src/components/Counter.jsx';

test('counter increments', () => {
  const { getByText } = render(<Counter />);
  const button = getByText('Increment');
  fireEvent.click(button);
  expect(getByText('Count: 1')).toBeInTheDocument();
});

Page Testing

// tests/pages/about.test.js
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import About from '../src/pages/about.astro';

test('about page renders', async () => {
  const container = await AstroContainer.create();
  const result = await container.renderToString(About);
  expect(result).toContain('About Us');
});

Step 12: Performance Optimization

Bundle Analysis

# Analyze Astro build
npm run build
npx astro build --analyze

Optimization Checklist

  • Remove unused dependencies
  • Optimize images with Astro’s Image component
  • Use client directives appropriately
  • Implement proper caching headers
  • Minimize client-side JavaScript

Step 13: Deployment

Vercel Deployment

// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';

export default defineConfig({
  output: 'server',
  adapter: vercel(),
});

Netlify Deployment

// astro.config.mjs
import { defineConfig } from 'astro/config';
import netlify from '@astrojs/netlify/functions';

export default defineConfig({
  output: 'server',
  adapter: netlify(),
});

Migration Checklist

Pre-Migration

  • Audit current Next.js application
  • Identify static vs dynamic pages
  • List all dependencies
  • Plan migration strategy

During Migration

  • Set up Astro project
  • Migrate layouts and components
  • Convert pages one by one
  • Update routing and navigation
  • Migrate API routes
  • Test functionality

Post-Migration

  • Performance testing
  • SEO verification
  • Cross-browser testing
  • Deploy to staging
  • Monitor Core Web Vitals
  • Update documentation

Common Pitfalls and Solutions

1. Client-Side State Management

Problem: Global state not working across pages Solution: Use Astro’s persistent state or move to client-side routing

2. Dynamic Imports

Problem: Dynamic imports behaving differently Solution: Use Astro’s dynamic imports or component islands

3. CSS-in-JS Libraries

Problem: Styled-components not working Solution: Use Astro’s scoped styles or CSS modules

Performance Comparison

After migration, you should see improvements in:

  • Lighthouse Performance: 90+ scores
  • Bundle Size: 50-80% reduction
  • Time to Interactive: Significant improvement
  • Core Web Vitals: Better LCP, FID, CLS scores

Conclusion

Migrating from Next.js to Astro can significantly improve your website’s performance while maintaining developer productivity. The key is to plan carefully, migrate incrementally, and test thoroughly.

Remember:

  • Start with static pages first
  • Use React components where interactivity is needed
  • Leverage Astro’s client directives for optimal performance
  • Test each step of the migration
  • Monitor performance improvements

The migration effort is worth it for the performance gains and improved user experience that Astro provides.