Next SEO
"Next.js SEO and metadata management: meta tags, Open Graph, Twitter cards, JSON-LD structured data, canonical URLs, robots directives, and sitemap generation using the Metadata API and next-seo."
Modern SEO in Next.js revolves around the built-in Metadata API (App Router) and the `next-seo` package (Pages Router). The goal is declarative, type-safe metadata that is colocated with route segments, automatically merged through the layout hierarchy, and rendered as server-side HTML so crawlers see complete meta tags on first load. Every page should have a unique title, description, canonical URL, and structured data. Open Graph and Twitter cards ensure rich previews when links are shared. Sitemaps and robots.txt guide crawlers efficiently. ## Key Points - Set `metadataBase` in the root layout so relative URLs in `openGraph.images` and `alternates.canonical` resolve correctly. - Use the `title.template` pattern to keep page titles consistent without repeating the site name. - Always provide `alternates.canonical` to avoid duplicate content penalties. - Generate OG images dynamically with `ImageResponse` rather than maintaining static files for every page. - Place JSON-LD in the page component body (not in `<head>`) — Google parses it from `<body>` just fine, and it simplifies component composition. - Keep descriptions between 120 and 160 characters. - Validate structured data with Google's Rich Results Test after deployment. - Use `generateStaticParams` alongside `generateMetadata` so metadata is generated at build time for static routes. - Set `changeFrequency` and `priority` in sitemaps to reflect actual update cadence. - **Client-side only meta tags**: Using `useEffect` or `document.title` means crawlers never see the metadata. Always use server-rendered metadata. - **Duplicate titles across pages**: Every page needs a unique title and description. The `title.template` pattern helps but each page must still set its own `title` value. - **Missing `metadataBase`**: Without it, relative OG image URLs break in production, leading to missing previews on social platforms. ## Quick Example ```bash npm install next-seo ```
skilldb get seo-content-skills/Next SEOFull skill: 304 linesNext.js SEO & Metadata
Core Philosophy
Modern SEO in Next.js revolves around the built-in Metadata API (App Router) and the next-seo package (Pages Router). The goal is declarative, type-safe metadata that is colocated with route segments, automatically merged through the layout hierarchy, and rendered as server-side HTML so crawlers see complete meta tags on first load. Every page should have a unique title, description, canonical URL, and structured data. Open Graph and Twitter cards ensure rich previews when links are shared. Sitemaps and robots.txt guide crawlers efficiently.
Setup
App Router (Metadata API)
No extra packages needed. Export metadata or generateMetadata from any layout.tsx or page.tsx:
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://example.com"),
title: {
default: "My Site",
template: "%s | My Site",
},
description: "A comprehensive Next.js application.",
openGraph: {
type: "website",
locale: "en_US",
siteName: "My Site",
},
twitter: {
card: "summary_large_image",
creator: "@handle",
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
};
Pages Router (next-seo)
npm install next-seo
// pages/_app.tsx
import { DefaultSeo } from "next-seo";
import type { AppProps } from "next/app";
const DEFAULT_SEO = {
titleTemplate: "%s | My Site",
defaultTitle: "My Site",
description: "A comprehensive Next.js application.",
openGraph: {
type: "website",
locale: "en_US",
site_name: "My Site",
},
};
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<DefaultSeo {...DEFAULT_SEO} />
<Component {...pageProps} />
</>
);
}
Key Techniques
Dynamic Metadata with generateMetadata
// app/blog/[slug]/page.tsx
import type { Metadata, ResolvingMetadata } from "next";
import { getPost } from "@/lib/posts";
import { notFound } from "next/navigation";
interface Props {
params: Promise<{ slug: string }>;
}
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
if (!post) return {};
const previousImages = (await parent).openGraph?.images || [];
return {
title: post.title,
description: post.excerpt,
authors: [{ name: post.author }],
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
publishedTime: post.publishedAt,
authors: [post.author],
images: [
{
url: post.ogImage,
width: 1200,
height: 630,
alt: post.title,
},
...previousImages,
],
},
alternates: {
canonical: `/blog/${slug}`,
},
};
}
JSON-LD Structured Data
// components/json-ld.tsx
import React from "react";
interface ArticleJsonLdProps {
title: string;
description: string;
url: string;
imageUrl: string;
datePublished: string;
dateModified: string;
authorName: string;
}
export function ArticleJsonLd({
title,
description,
url,
imageUrl,
datePublished,
dateModified,
authorName,
}: ArticleJsonLdProps) {
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: title,
description,
url,
image: imageUrl,
datePublished,
dateModified,
author: {
"@type": "Person",
name: authorName,
},
publisher: {
"@type": "Organization",
name: "My Site",
logo: {
"@type": "ImageObject",
url: "https://example.com/logo.png",
},
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
);
}
Sitemap Generation
// app/sitemap.ts
import type { MetadataRoute } from "next";
import { getAllPosts } from "@/lib/posts";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts();
const blogEntries = posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: "weekly" as const,
priority: 0.7,
}));
const staticPages = [
{ url: "https://example.com", priority: 1.0 },
{ url: "https://example.com/about", priority: 0.8 },
{ url: "https://example.com/blog", priority: 0.9 },
].map((page) => ({
...page,
lastModified: new Date(),
changeFrequency: "monthly" as const,
}));
return [...staticPages, ...blogEntries];
}
Robots.txt
// app/robots.ts
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: ["/api/", "/admin/", "/_next/"],
},
],
sitemap: "https://example.com/sitemap.xml",
};
}
Open Graph Images (Route Handler)
// app/og/route.tsx
import { ImageResponse } from "next/og";
import { NextRequest } from "next/server";
export const runtime = "edge";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const title = searchParams.get("title") ?? "My Site";
return new ImageResponse(
(
<div
style={{
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#0f172a",
color: "white",
fontSize: 48,
fontWeight: 700,
padding: "40px 80px",
}}
>
<div style={{ marginBottom: 24 }}>{title}</div>
<div style={{ fontSize: 24, color: "#94a3b8" }}>example.com</div>
</div>
),
{ width: 1200, height: 630 }
);
}
Best Practices
- Set
metadataBasein the root layout so relative URLs inopenGraph.imagesandalternates.canonicalresolve correctly. - Use the
title.templatepattern to keep page titles consistent without repeating the site name. - Always provide
alternates.canonicalto avoid duplicate content penalties. - Generate OG images dynamically with
ImageResponserather than maintaining static files for every page. - Place JSON-LD in the page component body (not in
<head>) — Google parses it from<body>just fine, and it simplifies component composition. - Keep descriptions between 120 and 160 characters.
- Validate structured data with Google's Rich Results Test after deployment.
- Use
generateStaticParamsalongsidegenerateMetadataso metadata is generated at build time for static routes. - Set
changeFrequencyandpriorityin sitemaps to reflect actual update cadence.
Anti-Patterns
- Client-side only meta tags: Using
useEffectordocument.titlemeans crawlers never see the metadata. Always use server-rendered metadata. - Duplicate titles across pages: Every page needs a unique title and description. The
title.templatepattern helps but each page must still set its owntitlevalue. - Missing
metadataBase: Without it, relative OG image URLs break in production, leading to missing previews on social platforms. - Hardcoded production URLs in development: Use environment variables or
metadataBaseto avoid committing localhost URLs. - Blocking metadata fetches: If
generateMetadatacalls a slow external API, it delays the entire page response. Cache aggressively or usefetchwithnext.revalidate. - Ignoring
robotsfor staging environments: Staging sites withoutnoindexdirectives get indexed by Google, creating duplicate content issues. - Overstuffing keywords in descriptions: Search engines penalize keyword stuffing. Write natural, human-readable descriptions.
- Forgetting
alttext on OG images: Screen readers and some platforms display alt text; omitting it hurts accessibility and can reduce click-through.
Install this skill directly: skilldb add seo-content-skills
Related Skills
Contentlayer
"Contentlayer and Velite for type-safe content management: transforming Markdown/MDX into typed data, schema validation, computed fields, Next.js integration, hot reload, and migration between content tools."
Core Web Vitals
Core Web Vitals optimization: LCP, INP, and CLS measurement, diagnosis, and improvement strategies for better search rankings and user experience.
Fumadocs
"fumadocs documentation framework: Next.js App Router native, MDX content collections, full-text search, OpenAPI integration, TypeScript-first, customizable UI components, and content source adapters."
Mdx
"MDX authoring with Next.js: Markdown + JSX, custom components, frontmatter extraction, @next/mdx, mdx-bundler, contentlayer integration, rehype/remark plugins, and syntax highlighting with Shiki or Prism."
Nextra
"Nextra documentation framework: MDX-powered Next.js docs and blog sites, sidebar navigation, full-text search, i18n, themes (docs and blog), frontmatter configuration, and custom components."
Programmatic SEO
Programmatic SEO strategies: generating thousands of search-optimized pages from structured data, template design, internal linking, and indexing at scale.