July 2024
·3 min read
Next.js App Router SEO: Server-Side Rendering Done Right
Use the Next.js App Router metadata API, JSON-LD structured data, and SSR to ship crawlable pages that rank — from generateMetadata to sitemaps.
Introduction
- Why SEO needs SSR: crawlers receive fully-rendered HTML; client-rendered SPAs often miss the first-crawl window.
- App Router defaults to Server Components — every page is SSR out of the box.
- This guide covers the metadata API, Open Graph, JSON-LD, sitemaps, robots, and performance signals that affect rankings.
How the App Router Renders Pages for Crawlers
- Every
page.tsxin the App Router is a Server Component — HTML is generated at request time (or build time for static). - Crawlers like Googlebot receive the complete document; no JavaScript execution required for core content.
fetchcache andrevalidatecontrol freshness: userevalidate: 3600for content that changes hourly.
The Metadata API: generateMetadata and Static metadata
- Export
metadata(static) orgenerateMetadata(dynamic) from anypage.tsxorlayout.tsx. - Required fields for every page:
title,description,alternates.canonical.
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlug(slug);
return {
title: post.title,
description: post.excerpt,
alternates: { canonical: `/blog/${slug}` },
openGraph: {
title: `${post.title} — Mark Lowel Montealto`,
description: post.excerpt,
type: 'article',
images: [{ url: post.coverImage ?? '/og.png', width: 1200, height: 630 }],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
},
};
}The Title Template Pattern
- Root layout sets
title: { template: '%s | Mark Lowel Montealto', default: 'Mark Lowel Montealto' }. - Child pages provide a short
title— the template appends the brand. - Result: "Next.js App Router SEO | Mark Lowel Montealto" — good for both SERP snippets and brand recognition.
Open Graph & Twitter Card
- OG images must be 1200×630 px; use Next.js
ImageResponse(app/opengraph-image.tsx) to generate them dynamically. - Always re-declare
imagesin child pages — Next.js replaces (does not deep-merge) parentopenGraph. - Twitter card:
summary_large_imagefor blog posts;summaryfor smaller pages.
JSON-LD Structured Data
- Inject via a
<JsonLd>component that renders a<script type="application/ld+json">in the<head>. - Root layout:
WebSite+Personschemas for brand entity. - Blog posts:
BlogPostingschema withheadline,author,datePublished,image.
// components/JsonLd.tsx
export function JsonLd({ data }: { data: object }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
);
}sitemap.ts and robots.ts
- Next.js App Router supports
app/sitemap.ts(returnsMetadataRoute.Sitemap). - Dynamically fetch all post slugs and generate URL entries with
lastModifiedandchangeFrequency. app/robots.tsreturns{ rules: { userAgent: '*', allow: '/' }, sitemap: '...' }.
// app/sitemap.ts
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPosts();
return posts.map(post => ({
url: `https://marklowelmontealto.com/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: 'monthly',
priority: 0.8,
}));
}Core Web Vitals and SEO
- Google's Page Experience ranking signals: LCP, INP, CLS.
- Next.js
<Image>auto-optimises format, size, and lazy loading — use it for every image. next/fonteliminates FOUT (Flash of Unstyled Text) — a common CLS source.- Route prefetching via
<Link>ensures fast navigations (INP).
Robots Meta and noindex Pages
- Admin, draft, and tag pagination pages should have
robots: { index: false }. - Apply via
metadataexport or programmatically ingenerateMetadata. - Never
noindexcanonical content pages.
Checklist: Before Every Deploy
- Every page has
title,description,alternates.canonical. - OG image defined (1200×630, alt text).
- JSON-LD present on homepage and all blog posts.
-
sitemap.tsincludes all public routes. -
robots.tsallows all crawlers, lists sitemap URL. -
<Image>used for every img — no bare<img>tags. - Run Lighthouse SEO audit — target 100.
Conclusion
- The App Router's metadata API covers 90% of SEO needs with minimal boilerplate.
- JSON-LD + canonical URLs + Core Web Vitals are the remaining 10% that move the needle for competitive terms.
- Treat SEO as a first-class concern from day one — retrofitting metadata onto 50 pages is painful.