SEO metadata rules for Next.js pages. Use when creating or modifying page metadata, title tags, meta descriptions, Open Graph tags, canonical URLs, or Twitter cards. Covers Next.js Metadata API (generateMetadata, metadata exports) and head elements.
Technical rules for implementing page metadata in a Next.js e-commerce site.
generateMetadata or metadata export) — never hardcode titles in JSX <head>{Page-Specific Content} | {BRAND_NAME} — brand suffix comes from a shared config constant
{Product Name} {Model} | {BRAND_NAME}{Category Name} - Buy Online | {BRAND_NAME}{Service Name} | {BRAND_NAME}generateMetadata must return a description field${SITE_URL}/${locale}/products/${id}/${slug}/en-GB/, never just /products/123?page=1; subsequent pages should self-reference (the canonical for ?page=2 points to ?page=2)/en-GB/product/123 must NOT have its canonical set to /de-DE/product/123Why it matters: Canonical URLs tell search engines which version of a page is the "master" copy. Without them, search engines may split ranking signals across duplicate URLs.
Required OG tags for every page — generateMetadata must return these in the openGraph field:
| Tag | Rule |
|---|---|
og:title | Same as title tag (or slightly adapted for social) |
og:description | Same as meta description |
og:url | Must match the canonical URL exactly |
og:type | product for product pages, website for all others |
og:image | Product image (min 1200x630px) or site default |
og:site_name | Value from BRAND_NAME config |
og:locale | BCP 47 format matching the page locale: en_GB, de_DE, etc. |
og:locale:alternate | Array of all other available locales for this page |
| Tag | Rule |
|---|---|
twitter:card | summary_large_image for product/category pages, summary for others |
twitter:title | Same as og:title |
twitter:description | Same as og:description |
twitter:image | Same as og:image works fine; X/Twitter's optimal is 1200x675 (2:1 ratio) vs OG's 1200x630 (1.91:1), but both are accepted |
Code examples use App Router conventions. For Pages Router, use <Head> from next/head with getStaticProps/getServerSideProps.
Use generateMetadata for dynamic pages:
// app/[locale]/products/[id]/[slug]/page.tsx
// Example imports — use your project's actual config
import { SITE_URL, BRAND_NAME } from '@/config/site';
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const product = await getProduct(params.id);
const locale = params.locale; // e.g. "en-GB"
return {
title: `${product.name} ${product.model} | ${BRAND_NAME}`,
description: product.metaDescription,
alternates: {
canonical: `${SITE_URL}/${locale}/products/${product.id}/${product.slug}`,
languages: buildHreflangMap(product.id, product.slug), // all locale variants
},
openGraph: {
title: `${product.name} ${product.model} | ${BRAND_NAME}`,
description: product.metaDescription,
url: `${SITE_URL}/${locale}/products/${product.id}/${product.slug}`,
type: 'product' as any, // Next.js doesn't type 'product' but it's valid OG
images: [{ url: product.imageUrl, width: 1200, height: 630, alt: `${product.name} ${product.model}` }],
siteName: BRAND_NAME,
locale: locale.replace('-', '_'), // en-GB -> en_GB
},
twitter: {
card: 'summary_large_image',
title: `${product.name} ${product.model} | ${BRAND_NAME}`,
description: product.metaDescription,
images: [product.imageUrl],
},
};
}
Use static metadata export for static pages:
// app/[locale]/contact-us/page.tsx
import { BRAND_NAME } from '@/config/site';
export const metadata: Metadata = {
title: `Contact Us | ${BRAND_NAME}`,
description: 'Get in touch for product information, service, and support.',
};
generateMetadata or metadata export<head> — it handles deduplication and ordering (App Router). In Pages Router, use next/head consistently.generateMetadata — static metadata export cannot access route params<head> — always use the Metadata API; Next.js handles deduplication and orderinggenerateMetadata on dynamic routes — static metadata export cannot access paramslocale param, never hardcode a locale stringopenGraph.images must always be populated (use a site default fallback)SITE_URL strings — use the config constant so the value is consistent and changeable