Shipping SEO that actually works in Next.js 16
Metadata is the easy part. Canonical URLs, hreflang pairs, structured data and streaming metadata are where most App Router sites quietly lose rankings.
Hieu Nguyen
Founder & lead engineer
Published on · 9 min read
Most Next.js sites stop at exporting a metadata object with a title and description, then wonder why they never rank. Metadata is table stakes. The work that moves rankings sits in four places.
One canonical URL per page
Every page must declare exactly one canonical URL, and it has to match the URL you actually want indexed — including its locale prefix. If /en/products and /products both render, you have split your own authority in half.
Set metadataBase once in the root layout, then give every route an explicit alternates.canonical. Relative paths resolve against metadataBase, so you write the path once and never hand-concatenate a domain again.
hreflang has to be reciprocal
Search engines only trust an hreflang cluster when every URL in it points at every other URL, including itself. A one-way link from the English page to the Vietnamese one is ignored. Generate the whole alternates.languages map from a single list of locales so the two pages can never disagree.
Add x-default for the URL you want served to users whose language you don't support.
Structured data is how you become an entity
JSON-LD isn't a ranking factor on its own — it's how you tell a crawler what a page is. A product page should emit Product with an Offer, a BreadcrumbList matching the visible breadcrumb, and AggregateRating only if real reviews exist. Inventing ratings is the fastest way to lose rich results entirely.
Render it as a plain <script type="application/ld+json">. It is data, not executable code, so next/script is the wrong tool.
Streaming metadata and bots
Next.js streams metadata for dynamically rendered pages so visual content paints first, and it disables streaming for known crawlers that expect tags in <head>. This is the right default. It only bites you if your generateMetadata awaits something slow — a crawler will wait for it. Memoize shared fetches with React's cache so the page and its metadata pay for one query, not two.
The checklist
metadataBasein the root layout, explicit canonical on every route- Reciprocal
alternates.languagesplusx-default - One
<h1>per page that matches the title's intent - A sitemap generated from the database, not hand-maintained
robots.txtthat points at the sitemap- OG images generated per route, 1200×630
- Structured data that describes what is actually on the page
None of it is difficult. All of it is easy to skip.
- nextjs seo
- generateMetadata
- structured data
- hreflang
- app router