Next.js App Router: A Pragmatic SEO Playbook
How to build SEO-first sites with the App Router - generateMetadata, structured data, ISR, and Core Web Vitals.
The App Router fundamentally changes how SEO ships in Next.js. The Pages Router relied on Head components and bespoke per-page wiring. The App Router gives you a Metadata API that consolidates titles, descriptions, Open Graph, Twitter cards, canonical URLs, robots directives, and verification meta into a single typed object - either statically exported or generated per request. Treating that API as the single source of truth is the difference between SEO that compounds and SEO that drifts.
Static and dynamic metadata
Static metadata is the simplest pattern: export a metadata constant from page.tsx and Next.js bakes the tags into prerendered HTML. For dynamic content - blog posts, services, products - use generateMetadata({ params }) to fetch the right data and return per-page values. The function is async, runs at build time for SSG and per-request for dynamic routes, and is fully typed against the Metadata interface so missing or misnamed fields fail at compile time rather than at audit time.
A pragmatic helper pattern: define a buildMetadata({ title, description, path, image }) factory in lib/seo.ts that fills in defaults (canonical URL, site name, OG locale, Twitter creator, robots directives) and merges with overrides. Every page calls it; nothing is duplicated; site-wide changes happen in one place. This is the single highest-leverage pattern we deploy on every Next.js engagement.
Structured data without ceremony
Structured data goes through the same App Router primitives. Inject script tags with type "application/ld+json" in your layout for site-wide schemas and per-page for context-specific ones. The Metadata API does not wrap JSON-LD natively - use the next/script component with dangerouslySetInnerHTML and JSON.stringify for clean injection. Default schemas we ship on every project:
- Organization and LocalBusiness in the root layout
- WebSite with optional SearchAction in the root layout
- SiteNavigationElement reflecting the header navigation
- BreadcrumbList per detail page (services, blog, etc.)
- FAQPage where applicable - homepage, service pages, FAQ sections
- BlogPosting on blog detail pages with author and dates
- Service on service detail pages with provider and areaServed
File-based sitemaps and robots
Drop app/sitemap.ts and app/robots.ts. Sitemap is a function returning MetadataRoute.Sitemap, where you map your data layer (services, blog posts, etc.) to URL entries with lastModified, changeFrequency, and priority. Robots is even simpler - one function, returns rules and sitemap URL. Both regenerate on rebuild, so they stay in sync with your routes automatically. No build step, no third-party packages, no drift.
Dynamic Open Graph images
Open Graph images are where most teams cut corners. Drop app/opengraph-image.tsx with next/og and you get edge-rendered, branded social cards on every page - no Photoshop, no broken previews. The component returns an ImageResponse with JSX-styled inline content (gradients, typography, your logo). Per-page custom OG images go in app/[route]/opengraph-image.tsx to override the default. Cost is a few CPU-ms per render at the edge.
ISR for content cadence
ISR (Incremental Static Regeneration) is the App Router pattern for content that changes occasionally - blog posts, marketing sites, documentation. Set revalidate = 3600 on the route to cache at the edge for an hour, regenerate on the next request after expiry. Pair with dynamicParams = false to lock generated routes to your data layer and 404 anything else. The result is a site that feels static and ranks like static while letting content editors ship without redeploying.
Core Web Vitals as a deploy gate
LCP, INP, and CLS are real ranking signals. The patterns we apply by default:
- next/image with explicit width and height eliminates CLS from late-loading images
- next/font with display swap preloads critical fonts
- Audit third-party scripts ruthlessly - GA4 alone is fine, every additional pixel adds INP cost
- Lighthouse CI in the PR pipeline gates regressions before they ship
Canonical URL hygiene
Canonical URL hygiene matters more than most teams realize. Set metadataBase and alternates.canonical in every page metadata so duplicate apex/www, protocol mismatches, and trailing-slash drift do not split SEO authority. Pair with a 301/308 redirect at the framework level (Next.js redirects() config or hosting platform) to enforce a single canonical hostname. Without this, a single site can show up as four indexed copies in Google.
We deploy these patterns across production engagements in fintech, healthcare, and eCommerce. Talk to a senior engineer about how they apply to your platform.
