---
{
title: 'Porting a Next.js Site to Astro Step-by-Step',
description: "Let's port a site from Next.js to Astro, expanding on the official migration guide.",
published: '2023-06-29T22:12:03.284Z',
authors: ['crutchcorn'],
tags: ['nextjs', 'react', 'astro'],
attached: [],
license: 'cc-by-nc-sa-4'
}
---
[Astro is a WebDev meta-framework](https://astro.build) that allows you to build highly performant websites, that, out-of-the-box compile down to 0kb of JavaScript in your bundle.
If your site is a marketing site, or if performance is a substantial concern for your app, it may make sense to migrate from Next.js to Astro - as we [at Unicorn Utterances](https://unicorn-utterances.com) once did.
While Astro provides a good guide of [how to migrate from Next.js to Astro](https://docs.astro.build/en/guides/migrate-to-astro/from-nextjs/) (written by yours truly!), I felt it would be helpful to see an expanded step-by-step guide on how to migrate a [Pokédex](https://www.pokemon.com/us/pokedex) application from Next.js to Astro.

> For the full codebase of the Pokédex application, [check the original repo here](https://github.com/crutchcorn/nextjs-pokemon-small-app).
Let's start the migration by changing our layout file to an Astro layout file.
## Next base layout to Astro
To start migrating a Next.js layout file from Next.js to Astro, you'll:
1. Identify the return().
```jsx {5-17}
// _document.js
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
)
}
```
2. Create `Layout.astro` and add this `return` value, [converted to Astro syntax](#reference-convert-nextjs-syntax-to-astro).
Note that:
- `` becomes ``
- `` becomes ``
- `` becomes ``
- `className` becomes `class`
- We do not need ``
```astro
---
// src/layouts/Layout.astro
---
```
3. Import the CSS (found in `_app.js`)
In addition to the `_document` file, the Next.js application has a `_app.js` file that imports global styling via a CSS import:
```jsx {2}
// pages/_app.js
import '../styles/index.css'
export default function MyApp({ Component, pageProps }) {
return
}
```
This CSS import can be moved to the Astro Layout component:
```astro {1-4}
---
// src/layouts/Layout.astro
import '../styles/index.css'
---
```
## Convert a Next.js `getStaticProps` Page to Astro
Next up, let's migrate [Next.js' `getStaticProps` method](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-props) to use Astro's data fetching.
We'll start with a Next.js page that lists the first 151 Pokémon using [the REST PokéAPI](https://pokeapi.co/):
```jsx
// pages/index.js
import Link from 'next/link'
import Head from 'next/head'
import styles from '../styles/poke-list.module.css';
export default function Home({ pokemons }) {
return (
<>
Pokedex: Generation 1
{pokemons.map((pokemon) => (
No. {pokemon.id}
{pokemon.name}
))}
>
)
}
export const getStaticProps = async () => {
const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151")
const resJson = await res.json();
const pokemons = resJson.results.map(pokemon => {
const name = pokemon.name;
// https://pokeapi.co/api/v2/pokemon/1/
const url = pokemon.url;
const id = url.split("/")[url.split("/").length - 2];
return {
name,
url,
id
}
});
return {
props: {
pokemons,
},
}
}
```
### Move Next Page Templating to Astro
To start migrating this page to Astro, start with the returned JSX and place it within an `.astro` file:
```astro
---
// src/pages/index.astro
import styles from '../styles/poke-list.module.css';
---
Pokedex: Generation 1
```
You should now have a fully working Pokédex entries screen.
## Convert a Next.js `getStaticPaths` Page to Astro
This is a Next.js dynamic page that generates a detail screen for each of the first 151 Pokémon using [the REST PokéAPI](https://pokeapi.co/).
```jsx
// pages/pokemon/[name].js
import { useRouter } from 'next/router';
import Head from 'next/head'
import styles from '../../styles/pokemon-entry.module.css';
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export default function Pokemon({ pokemon }) {
const router = useRouter();
const title = `Pokedex: ${pokemon.name}`;
return (
<>
{title}
No. {pokemon.id}: {pokemon.name}
Types
{pokemon.types}
Height
{pokemon.height}
Weight
{pokemon.weight}
{pokemon.flavorText}
>
)
}
export const getStaticPaths = async () => {
const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151")
const resJson = await res.json();
const pokemons = resJson.results;
return {
paths: pokemons.map(({ name }) => ({
params: { name },
}))
}
}
export const getStaticProps = async (context) => {
const { name } = context.params
const [pokemon, species] = await Promise.all([
fetch(`https://pokeapi.co/api/v2/pokemon/${name}`).then(res => res.json()),
fetch(`https://pokeapi.co/api/v2/pokemon-species/${name}`).then(res => res.json())
])
return {
props: {
pokemon: {
id: pokemon.id,
image: pokemon.sprites.front_default,
name: capitalize(pokemon.name),
height: pokemon.height,
weight: pokemon.weight,
flavorText: species.flavor_text_entries[0].flavor_text,
types: pokemon.types.map(({ type }) => type.name).join(', ')
},
},
}
}
```
### Move Next Page Templating to Astro
To start migrating this page to Astro, start with the returned JSX and place it within an `.astro` file:
```astro
---
// src/pages/pokemon/[name].astro
import styles from '../../styles/pokemon-entry.module.css';
---
No. {pokemon.id}: {pokemon.name}
Types
{pokemon.types}
Height
{pokemon.height}
Weight
{pokemon.weight}
{pokemon.flavorText}
```
Like before:
- Imported styles are moved to the code fence.
- `className` becomes `class`.
- `` contents are moved into ``.
- `{pokemon.id}` values are interpolated the same as before.
However, in addition, now:
- [HTML's standard `onclick`](https://developer.mozilla.org/en-US/docs/Web/Events/Event_handlers#using_onevent_properties) function is used to call [the browser's `history.go` function](https://developer.mozilla.org/en-US/docs/Web/API/History/go) to navigate back.
### Move Next `getStaticPaths` to Astro
Astro supports a function called `getStaticPaths` to generate dynamic paths, similar to Next.
Given a Next page:
```jsx
// pages/pokemon/[name].js
export const getStaticPaths = async () => {
const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151")
const resJson = await res.json();
const pokemons = resJson.results;
return {
paths: pokemons.map(({ name }) => ({
params: { name },
}))
}
}
```
Migrate the `getStaticPaths` method to Astro by removing the `paths` route prefix and returning an array:
```astro {10-12}
---
// src/pages/pokemon/[name].astro
import styles from '../../styles/pokemon-entry.module.css';
export const getStaticPaths = async () => {
const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151")
const resJson = await res.json();
const pokemons = resJson.results;
return pokemons.map(({ name }) => ({
params: { name },
}))
}
---
No. {pokemon.id}: {pokemon.name}
Types
{pokemon.types}
Height
{pokemon.height}
Weight
{pokemon.weight}
{pokemon.flavorText}
```
Then, similar to the previous page, migrate the `getStaticProps` method to non-function-wrapped code in the Astro page's code fence.
Given the Next page logic:
```jsx
// pages/pokemon/[name].js
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export const getStaticProps = async (context) => {
const { name } = context.params
const [pokemon, species] = await Promise.all([
fetch(`https://pokeapi.co/api/v2/pokemon/${name}`).then(res => res.json()),
fetch(`https://pokeapi.co/api/v2/pokemon-species/${name}`).then(res => res.json())
])
return {
props: {
pokemon: {
id: pokemon.id,
image: pokemon.sprites.front_default,
name: capitalize(pokemon.name),
height: pokemon.height,
weight: pokemon.weight,
flavorText: species.flavor_text_entries[0].flavor_text,
types: pokemon.types.map(({ type }) => type.name).join(', ')
},
},
}
}
```
Migrate this to the Astro page's code fence:
> Use `Astro.props` to access the `params` returned from the `getStaticPaths` function
```astro {15-33}
---
// src/pages/pokemon/[name].astro
import styles from '../../styles/pokemon-entry.module.css';
export const getStaticPaths = async () => {
const res = await fetch("https://pokeapi.co/api/v2/pokemon?limit=151")
const resJson = await res.json();
const pokemons = resJson.results;
return pokemons.map(({ name }) => ({
params: { name },
}))
}
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
const { name } = Astro.props;
const [pokemonData, species] = await Promise.all([
fetch(`https://pokeapi.co/api/v2/pokemon/${name}`).then(res => res.json()),
fetch(`https://pokeapi.co/api/v2/pokemon-species/${name}`).then(res => res.json())
])
const pokemon = {
id: pokemonData.id,
image: pokemonData.sprites.front_default,
name: capitalize(pokemonData.name),
height: pokemonData.height,
weight: pokemonData.weight,
flavorText: species.flavor_text_entries[0].flavor_text,
types: pokemonData.types.map(({ type }) => type.name).join(', ')
};
---
No. {pokemon.id}: {pokemon.name}
Types
{pokemon.types}
Height
{pokemon.height}
Weight
{pokemon.weight}
{pokemon.flavorText}
```
You have now fully migrated a Pokédex application from Next to Astro.