Overview
SEO-friendly React
Next.js serves content-complete HTML pages to clients and crawlers, instead of the empty-shell SPAs pages built by Vite or Create React App, which are served mostly empty.
Such complete pages rank better in search-engines results and in chatbots results.
other benefits
- built-in router that offers client-side navigation when appropriate.
- static page generation at build time when appropriate.
- ubiquitous, widespread support, known to deploy platforms and LLMs.
temporary scope of this guide: Pages router
The guide currently focuses on the Pages router. It excludes the App router and React server components (RSC).
Initialization
We scaffold the project with create-next-app:
npx create-next-app
We pick options such as:
- using TypeScript or JavaScript
- using a linter
- using Tailwind CSS
- adding a src/ directory, or everything at root level
- picking between the pages router or the app router
File-based routing
We describe the pages router, which works with a pages/ directory.
pages directory content
The pages/ directory contains:
- route files and route directories
- wildcard files such as
[book].tsx - special files (see Special files)
route files and route directories
A route file creates a route of the same name. E.g. for pages/users.tsx:
abc.com/users
If the file is in a nested directory, the directory is also part of the path. E.g. for pages/settings/main.tsx:
abc.com/settings/main
Index files don't add to the path. Instead, they match the path of their parent. E.g. for pages/users/index.tsx:
abc.com/users
route conflict
As seen above, a route file can clash with a nested index file:
users.tsx
users/index.tsx
## abc.com/users
Next.js detects such conflicts and requests us to keep only one of those files.
Special files
_app.tsx and _document.tsx are special files in pages/. They are optional for the developer: If we don't provide/implement them, Next.js provides them in their default version.
_app.tsx
It serves as a global wrapper: it matches every routes.
import global stylesheets
It is the designated file to import global stylesheets, which Next.js only allows in this file:
import "../styles/global.css"
load local fonts
We may import and expose a local font: see Expose a local font.
favicon
We can switch between separate favicon dynamically.
boilerplate
We must add some boilerplate: The file must have a default export and feed pageProps to Component.
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Component {...pageProps} />
</>
)
}
_document.tsx
It serves a limited role. Since we have control over the <html> element, we can
- add the
langattribute. - import Google fonts outside of Next.js control.
- import a static favicon.
Next.js does not expose an index.html file. _document.tsx is the only place where we have direct control over the <html> and <body> elements, though technically we rely on <Html> and other Next.js provided wrappers, and the content we set in <Head> is not exhaustive.
import { Html, Head, Main, NextScript } from "next/document"
const FAVICON_URL = "/favicon.ico"
export default function Document() {
return (
<Html lang="en">
<Head>
<link
href="https://fonts.googleapis.com/css2?family=PT+Mono&display=swap"
rel="stylesheet"
/>
<link rel="icon" href={FAVICON_URL} />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
Build pages programmatically
programmatic pages : pages that don't match a route file
Route files hardcode a page, one that match their name: one page for one file.
Sometimes, we want to create pages programmatically.
from slugs to pages
The goal is to build pages programmatically, from a data-source, that provides slugs for the pages along with their content. The data source can be any data: local files or data received from a server.
We determine:
- a list of pages
- where they stand in the URL
- their content
- which React component they use.
pipeline overview
-
We determine where the generated pages live in the URL. If we want them to live scoped under the
books/path, we create abooks/directory. -
We then create a (wildcard) React file under that directory, with a special name that uses brackets, such as
[book].tsx, to signal a special page.- (Optional) We can also add a wildcard directory in addition to the wildcard file, such as
[book]/[chapter].tsx. In this scenario, we generate a list of books programmatically, each one have its own list of chapters generated programmatically.
- (Optional) We can also add a wildcard directory in addition to the wildcard file, such as
-
In this file, we read the data-source to determine a list of slugs that we are to make pages for. For example, we read a list of markdown files and compute a list of slugs from them. We register the slug list.
-
Then, we establish a data-gathering function that fetches data according to a slug, and returns the data. We must return the data in a specific, structured format. For example, we reads a markdown file and returns its content in a structured object.
- Implementation detail: the structured object has a root
propsproperty, which itself is an object as well, and as its name implies, hold a series of props, each one being given to the template React component as a React prop.
- Implementation detail: the structured object has a root
-
Finally, we default export a React component to make it become the template React component. It receives the gathered data . It can set
<title>in<Head>to ensure the generated page has an appropriate title.
other details
- The pipeline runs at build-time.
- Next.js ensures there is a fully-fledged HTML page for each slug.
determine and provide the slugs
What Next.js expects: an array of paths (paths), each member being a path and about to receive its own page.
Each path has a params object, which comes with the slug(s).
The single slug scenario is when the path is made of a single slug. We provide the slug, keyed to the wildcard file name, such as book for [book].tsx.
// [
// { params: { book: "typescript"}},
// { params: { book: "javascript"}},
// { params: { book: "clang"}}
// ]
Note: if we provide several slugs in the params object, it denotes that at least one slug is to match a wildcard directory, in addition to the slug matching the wildcard file.
provide the slugs (implementation example)
[book].tsx
export function getStaticPaths() {
// make use of a data source, usually an array
const books = getBooks()
// build the array of paths, here by looping over the data source array
const paths = books.map((book) => ({
params: { book: book.slug },
}))
// return it, along with options
return { paths, fallback: false }
}
gather data for a given slug
getStaticProps is expected to return this shape, with props being the main content, a series of props given to the React component:
// {
// props: {
// book: {
// title: book.title,
// slug: book.slug,
// author: book.author,
// lastUpdate: book.lastUpdate,
// },
// },
// )
[book].tsx
export async function getStaticProps({ params }: { params: { book: string } }): Promise<{
props: BookUIProps
}> {
// 1.0 get slug from params (book name)
const bookSlug = params.book
// 2.0 fetch data according to the slug (book content)
const books = getBooks()
const book = books.find((b) => b.slug === bookSlug)
if (!book) throw new Error(`Book not found: ${bookSlug}`)
const data = book.data
// 3.0 returned the object with props
return {
props: {
book: {
title: book.title,
slug: book.slug,
author: book.author,
lastUpdate: book.lastUpdate,
},
},
}
}
make use of the data
[book].tsx
export default function BookUI({ book }) {
const { title, slug, author, lastUpdate } = book
return <>{/*...*/}</>
}
Local fonts
overview and setup
We place the font files, such as the woff2 files, in a directory such as src/fonts.
We provide them to Next.js with localFont({}). We call localFont({}) in _app.tsx, and provide the paths to the fonts in a src array. We also set the name of the CSS variable that will hold the font stack in variable.
We assign the result to a variable, whose name is important: it determines the CSS-facing font-name, that is, the one declared in @font-face and that we may use in font-family:
import localFont from "next/font/local"
const altinnDin = localFont({
src: [
{
path: "../fonts/Altinn-DINExp.woff2",
weight: "400",
style: "normal",
},
{
path: "../fonts/Altinn-DINExp-Italic.woff2",
weight: "400",
style: "italic",
},
{
path: "../fonts/Altinn-DINExp-Bold.woff2",
weight: "700",
style: "normal",
},
],
variable: "--font-altinn-din", // set the CSS variable's name.
})
Next.js generates the @font-face rules to import each face. It sets font-display: swap by default. To remove layout shift on font swap, it modifies the fallback font such as Arial to fit the metrics of the local font we added.
@font-face {
font-family: altinnDin;
src: url("../media/Altinn_DINExp-s.p.61719b9f.woff2") format("woff2");
font-display: swap;
font-weight: 400;
font-style: normal;
}
set the font immediately with a Next.js-generated class
Next.js generates a CSS class, which, when added to an element, makes it use the local font (along with a fallback font). The class becomes available at myFont.className.
.altinndin_6897fd0e-module__QFpdhW__className {
font-family: altinnDin, altinnDin Fallback;
}
If we add the class to a high-level wrapper, the font effectively becomes the site's global font. For example, in _app.tsx:
<div className={altinnDin.className}>
<Component {...pageProps} />
</div>
expose the font as a CSS variable, by using a Next.js-generated class
The class, when added to an element, merely defines a CSS variable, whose name was defined in localFont({}). The class becomes available at myFont.variable.
.altinndin_6897fd0e-module__QFpdhW__variable {
--font-altinn-din: "altinnDin", "altinnDin Fallback";
}
We add the class to a high-level wrapper. For example, in _app.tsx:
<div className={altinnDin.variable}>
<Component {...pageProps} />
</div>
It's up to elements to set font-family with that variable:
h1 {
font-family: var(--font-altinn-din);
}
Misc
shorten import paths
We want to prevent overly long import paths, caused by deep file nesting. Instead, we want to start a path straight from the src directory.
We cannot start the path with the src string, because it would refer to a node package from node_modules. We cannot start it with /src because it would refer to the host computer's root directory. Instead, the convention is to create a hardcoded alias using the @ symbol instead, which refers to the src directory.
import { x } from "@/types/foo"
import { x } from "../../../../types/foo"
We define the path alias(es) in tsconfig.json, and Next.js automatically parses them and registers them.
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}