Install
$ agentstack add skill-mvstepanek-nextjs-ecommerce-seo-skills-seo-metadata ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
About
SEO Metadata Guidelines
Technical rules for implementing page metadata in a Next.js e-commerce site.
Technical Requirements: Title Tags
- Always use the Next.js Metadata API (
generateMetadataormetadataexport) — never hardcode titles in JSX `` - Format:
{Page-Specific Content} | {BRAND_NAME}— brand suffix comes from a shared config constant - Product pages:
{Product Name} {Model} | {BRAND_NAME} - Category pages:
{Category Name} - Buy Online | {BRAND_NAME} - Service pages:
{Service Name} | {BRAND_NAME} - Keep titles concise — Google truncates them to fit device width (typically ~50-60 visible characters on desktop). There is no official character limit, but shorter titles display fully in more contexts.
- Must be unique per page — duplicate titles across pages confuse search engines about which page to rank
- Locale-aware — title content must use translated strings from i18n; brand name stays untranslated
- Include model/SKU identifiers on product pages — B2B buyers often search by model number
Technical Requirements: Meta Description
- Must be present on every indexable page —
generateMetadatamust return adescriptionfield - Keep concise — Google may truncate long snippets. There is no official character limit, but focus on making descriptions unique and relevant.
- Must be unique per page — never reuse the same description across multiple pages
- Locale-aware — pull translated descriptions from CMS/i18n, never hardcode English
Canonical URLs
- Every page MUST have a canonical URL — no exceptions
- Must be absolute — include the full URL with protocol and domain:
${SITE_URL}/${locale}/products/${id}/${slug} - Must include locale prefix — e.g.
/en-GB/, never just/products/123 - Self-referencing — a page's canonical should point to itself (its own URL)
- Pagination: page 1 canonical must NOT include
?page=1; subsequent pages should self-reference (the canonical for?page=2points to?page=2) - Never point a canonical across locales —
/en-GB/product/123must NOT have its canonical set to/de-DE/product/123 - Filters and sorting: category pages with filter/sort query params should have their canonical set to the base category URL (without filters)
Why 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.
Open Graph Tags
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 |
Twitter Card Tags
| 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 |
Next.js Implementation
Code examples use App Router conventions. For Pages Router, use ` from next/head with getStaticProps/getServerSideProps`.
Using Metadata API
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 {
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.',
};
Rules
- Never hardcode metadata in JSX — always use
generateMetadataormetadataexport - **Prefer the Next.js Metadata API over manual `
** — it handles deduplication and ordering (App Router). In Pages Router, usenext/head` consistently. - Layout metadata cascades — define base metadata (site_name, default OG image) in root layout, override specific fields in nested layouts and pages
- Dynamic pages must use
generateMetadata— staticmetadataexport cannot access route params - Use config constants — site URL and brand name should come from a shared config (however your project manages configuration), never hardcoded inline
Common Technical Anti-Patterns
- Hardcoded metadata in JSX `` — always use the Metadata API; Next.js handles deduplication and ordering
- Missing
generateMetadataon dynamic routes — staticmetadataexport cannot accessparams - Relative canonical URLs — canonical must be absolute with protocol and domain
- Canonical pointing to wrong locale — use the
localeparam, never hardcode a locale string - Missing OG image —
openGraph.imagesmust always be populated (use a site default fallback) - Same title on all locale variants — titles must use translated strings, not repeat the English version
- Hardcoded
SITE_URLstrings — use the config constant so the value is consistent and changeable
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: mvstepanek
- Source: mvstepanek/nextjs-ecommerce-seo-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.