SEO with the Next.js App Router: metadata, sitemaps and structured data
Damien Mercier · · 3 min read
The Next.js App Router ships everything a technically excellent SEO setup needs (metadata, sitemaps, structured data, streaming server rendering), but the pieces are spread across conventions that are easy to half-use. This is the setup behind the site you are reading, with the reasoning for each piece.
Start with metadataBase and a title template
Every page needs a unique title and description, and every relative URL in your metadata needs a base to resolve against. Both live in the root layout:
// app/layout.tsx
export const metadata: Metadata = {
metadataBase: new URL("https://example.com"),
title: {
default: "Site name | what it does",
template: "%s | Site name",
},
description: "One honest sentence about the site.",
};
The template means child pages only declare their own part:
// app/services/page.tsx
export const metadata: Metadata = {
title: "Services", // renders as "Services | Site name"
description: "A unique description for this page.",
alternates: { canonical: "/services" },
};
Two details that separate a correct setup from a sloppy one:
- Canonical on every indexable page. With
metadataBaseset, a relativealternates.canonicalis enough, and it protects you when URLs grow query parameters (UTM tags, pagination). - Unique descriptions. Duplicated meta descriptions are the most common audit finding on developer sites. If two pages need the same description, one of them probably shouldn't be indexed.
Sitemap and robots as code
The App Router treats these as routes, which means they can't drift out of date if they read from the same source of truth as your pages:
// app/sitemap.ts
export default function sitemap(): MetadataRoute.Sitemap {
const pages = ["", "/services", "/about", "/contact"];
const articles = posts.map((post) => `/writing/${post.slug}`);
return [...pages, ...articles].map((path) => ({
url: `https://example.com${path}`,
}));
}
The important habit: generate sitemap entries from the same registry that renders the content. A sitemap maintained by hand is a sitemap that lies.
Structured data without a library
JSON-LD is a <script> tag with a JSON payload; you do not need a package:
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
/>
What matters is discipline, not volume. Emit Person and WebSite once from
the layout, Article on articles, BreadcrumbList where breadcrumbs exist,
and nothing that doesn't correspond to visible content. Structured data that
misrepresents the page is worse for you than none: it is the one part of SEO
where over-claiming carries an explicit penalty.
Open Graph images from a route
Instead of exporting static images from a design tool, the opengraph-image
file convention generates them at build time with JSX. One typographic
template produces a correct, on-brand card for every article, and it can never
show a stale title, because it renders from the same data as the page.
Rendering strategy is an SEO decision
The quiet advantage of the App Router: with Server Components and static generation, an SEO-first site ships almost no JavaScript by default. Everything above (metadata, sitemap, JSON-LD) is table stakes; rankings also ride on Core Web Vitals, and no metadata compensates for a slow page. The rules of thumb I follow:
- Static generation for everything content-shaped; interactivity as small client islands, not client pages.
next/imageandnext/fontfrom day one, because retrofitting layout stability is miserable work.- Third-party scripts are guilty until proven necessary.
The performance half of this subject is its own topic, covered in React performance in practice.
The checklist
For every indexable page: unique title, unique description, canonical URL,
correct heading hierarchy with one h1, structured data that matches the
visible content. Site-wide: sitemap and robots generated from code, Open
Graph cards, fast Core Web Vitals.
None of it is glamorous. All of it compounds. Technical SEO is one of the few marketing investments that keeps paying with zero maintenance. This site practises what this article preaches; if you want your product's site or app to do the same, that is work I take on, so let's talk.