Migrating from Next.js to Astro.js is not just a framework swap. It is usually a shift in how you think about rendering, JavaScript delivery, and content-heavy pages.
If your current project is mostly made of marketing pages, docs, blogs, case studies, legal pages, or a lightweight product site with only a few interactive widgets, Astro can be a very strong fit. It helps you ship less JavaScript by default, keeps static pages simple, and still gives you room for server-side behavior when you need it.
The best migrations are not rushed. They are planned, incremental, and focused on preserving URLs, styling, metadata, and user experience while reducing unnecessary framework overhead.
Astro is usually a great choice when your Next.js project is:
It is not always the best move for:
If your app sits somewhere in the middle, Astro can still work well, but you should expect a more selective migration rather than a quick one-to-one replacement.
Before moving code, make a short inventory of the project. This saves a lot of confusion later.
| Audit item | What to capture | Why it matters |
|---|---|---|
| Routes | public pages, dynamic pages, nested paths | keeps URLs stable |
| Layouts | root shell, navigation, footer, wrappers | helps rebuild structure cleanly |
| Content | blogs, docs, marketing copy, MDX, CMS data | identifies what should become Astro-first |
| Interactive UI | forms, toggles, search, accordions, carousels | decides what stays hydrated |
| APIs | route handlers, webhooks, server helpers | shows what needs endpoints |
| Assets | fonts, images, favicons, OG assets | prevents broken references |
| Environment variables | public vs server-only values | avoids leaking secrets |
After that, split the app into two buckets:
That single decision makes the rest of the migration much easier.
Do not migrate everything in one pass. The cleanest path is:
about, services, pricing, or blogThis order gives you quick visual progress while keeping risk low.
Here is the simplest mental model for the move:
| In Next.js | In Astro | Notes |
|---|---|---|
app/page.tsx | src/pages/index.astro | page entry points move into src/pages |
app/layout.tsx | src/layouts/BaseLayout.astro | layouts become standard Astro components |
next/link | regular <a> | no framework wrapper needed for simple navigation |
next/image | <img> or Astro image tools | start simple, optimize later if needed |
next/font | self-hosted fonts, CSS imports, or @fontsource | keep font loading explicit |
metadata export | <head> tags in layout/page | SEO stays straightforward |
app/api/.../route.ts | src/pages/api/...ts | route handlers become Astro endpoints |
This is where Astro often feels refreshing: many framework-specific abstractions disappear.
A basic Astro project often looks like this:
src/
layouts/
BaseLayout.astro
pages/
index.astro
blog/
[slug].astro
api/
health.ts
components/
Header.astro
Footer.astro
ThemeToggle.tsx
public/
fonts/
images/
favicon.icoIf you already have a solid public/ folder in Next.js, that part usually transfers with very little effort.
Your layout migration sets the tone for the rest of the project. Move the document shell, shared metadata defaults, and global CSS into a reusable Astro layout.
---
import "../styles/global.css";
interface Props {
title: string;
description?: string;
}
const { title, description } = Astro.props;
---
<!doctype html>
<html lang=
Once this is in place, every migrated page becomes easier because you are no longer repeating the same document setup.
One of the biggest mistakes in a Next.js to Astro migration is bringing over every React habit unchanged.
Astro works best when static content stays static, and only the truly interactive pieces are hydrated on the client.
Good examples for Astro islands:
If you already have a React component that still makes sense, you can keep it and hydrate it only when needed:
---
import ThemeToggle from "../components/ThemeToggle.tsx";
---
<ThemeToggle client:load />Useful hydration directives include:
client:load for immediate interactivityclient:idle for lower-priority widgetsclient:visible for below-the-fold componentsThis is one of Astro's biggest wins: you decide exactly where browser JavaScript is worth paying for.
Static pages are usually the easiest to move. Blog pages, guides, and marketing pages often become simpler in Astro, especially if you store content in Markdown or MDX.
For dynamic routes, the main pattern is getStaticPaths():
---
export async function getStaticPaths() {
return [
{ params: { slug: "about" } },
{ params: { slug: "pricing" } },
{ params: { slug: "docs" } },
];
}
const { slug } = Astro.params;
---That makes route generation explicit and easy to reason about.
If your Next.js project has route handlers, map them intentionally instead of treating them like an afterthought.
Next.js:
app/api/status/route.tsAstro:
src/pages/api/status.tsExample endpoint:
export async function GET() {
return Response.json({ ok: true });
}If you use Prisma, Drizzle, or another database layer, keep those imports server-only and avoid failing at module load time when an environment variable is missing. Lazy initialization is usually safer during a migration.
This part gets missed a lot.
In Next.js, it is common to see browser-safe values prefixed with NEXT_PUBLIC_. In Astro, public values usually move to PUBLIC_.
That means a variable like:
NEXT_PUBLIC_SITE_URL=https://example.comoften becomes:
PUBLIC_SITE_URL=https://example.comAlso double-check:
.next output or Next-specific runtime behaviorA migration is already a large enough change. Avoid mixing it with a visual rewrite unless there is a strong reason.
If you use Tailwind, keep it. If you already have design tokens, utility classes, CSS variables, or a typography system, bring them over first and redesign later.
The goal of a good migration is stability:
That is a much safer path than changing framework, layout, styles, and content structure all at once.
These are simple habits, but they prevent most of the frustrating regressions.
Once the migration is in place, test it like a real product, not just a successful build.
404Astro is not meant to recreate every Next.js abstraction exactly as-is. Usually the cleaner result comes from simplifying.
If pages, APIs, styles, and content structure all change together, debugging becomes painful very quickly.
If a section does not need browser interactivity, let it stay server-rendered or static.
Public sites often look finished while still shipping broken titles, canonicals, or social previews.
Static output, server output, and hybrid rendering all have different hosting expectations. Decide that early.
Migrating an existing Next.js project to Astro.js works best when you treat it as a simplification exercise, not just a framework replacement.
Keep the content stable, move the layout first, reintroduce interactivity only where it matters, and test URLs and metadata as carefully as the UI. If your project is content-first, the result is often a codebase that feels lighter, faster, and much easier to maintain.