plugins/frontend-toolkit/skills/seo-metadata/SKILL.md
Apply Next.js Metadata API per route — title template, og:image, generateMetadata for dynamic pages, JSON-LD structured data, robots, sitemap, canonical, hreflang. Use when adding a new route, when search visibility drops, when rich results are needed, or before shipping. Not for choosing a route's render mode (use render-strategy-decision); align generateMetadata with that route caching choice.
npx skillsauth add jaykim88/claude-ai-engineering seo-metadataInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Every public route is correctly indexable, shows good social previews, and signals canonical URLs to crawlers. Bad metadata costs traffic — and is invisible until traffic data shows the loss.
Universal fields, platform-bound Procedure — the metadata fields (title, description, og:*, robots, canonical, sitemap, robots.txt) are HTML/HTTP standards. But the Procedure below is written concretely against the Next.js Metadata API (export const metadata, generateMetadata, sitemap.ts); the Other stacks section maps the equivalent API per framework (Nuxt useSeoMeta, SvelteKit <svelte:head>, Angular Meta/Title).
Set a base URL (Next.js: metadataBase in root layout — required for relative URLs)
// app/layout.tsx
export const metadata = {
metadataBase: new URL('https://yoursite.com'),
// ...
};
metadataBase set, openGraph.images: '/og.png' resolves to absolute URLs automaticallyStatic pages: use export const metadata
title, description, openGraph, twitter, robotstitle (~50–60 chars) and description (~150–160 chars) per route — duplicate or template-only titles across pages are a common ranking lossDynamic pages: generateMetadata function
generateMetadata and the page render is deduplicated automaticallygenerateMetadata (unnecessary overhead)Define title template at the root layout
export const metadata = {
title: { template: '%s | App Name', default: 'App Name' }
}
title: 'About' → renders About | App Nametitle.default is required when title.template is set — missing it will error at build time. The default covers routes that don't set their own title.Open Graph (social previews)
og:title, og:description, og:image per routeog:type, og:url, og:site_name, image alt, and twitter.card: 'summary_large_image' — partial OG tags render poor previewsopengraph-image.png next to page.tsx<body> for JS-capable bots, but HTML-limited bots (e.g., facebookexternalhit) still block on generateMetadata — keep dynamic metadata generation fast. Rely on Next.js fetch memoization to dedupe data between generateMetadata and the page render.Block non-public pages from indexing
metadata = { robots: { index: false, follow: false } }robots.txt Disallow ≠ noindex: Disallow blocks crawling, but a blocked URL can still be indexed (without content). To remove a page from the index use a noindex meta — not Disallownoindex / Disallow: / reaching production (a staging config leaking to prod silently de-indexes everything) — verify prod in step 8Canonical URLs and locale alternates
metadata = { alternates: { canonical: '/the-canonical-path' } }noindex/redirected URL, and don't canonical paginated pages back to page 1 (each page is its own canonical)alternates.languages (hreflang) so crawlers serve the right locale (coordinate with i18n-localization)Generate sitemap.ts and robots.ts
sitemap.ts exports a function returning all public URLsrobots.ts declares allow/disallow ruleslastModified; for > 50k URLs use a sitemap index7b. Add structured data (JSON-LD) for rich results
<script type="application/ld+json"> with the schema.org type that fits the page: Article, Product, BreadcrumbList, FAQPage, OrganizationuseEffect) is unreliable for crawlers, so align the render strategy to SSR/SSG (see render-strategy-decision)noindex / Disallow: /rendering-performance, responsive-design)metadata (static) or generateMetadata (dynamic)robots: { index: false }; production NOT accidentally noindexedsitemap.ts and robots.ts exist; sitemap submitted to Search Consoleexport const metadata (static) or export async function generateMetadata (dynamic) — each route fileapp/sitemap.ts and app/robots.ts (or framework equivalent)opengraph-image.{png,tsx} next to relevant page.tsx, 1200x630, < 1MBmetadataBaseexport const metadata = { ... }export async function generateMetadata({ params }) { ... } (returns same shape)title: { template: '%s | App', default: 'App' } — default is REQUIRED when template is setmetadataBase: new URL('https://...')opengraph-image.png next to page.tsx (or opengraph-image.tsx for dynamic)twitter: { card: 'summary_large_image', ... }alternates: { languages: { 'en-US': '/en', 'ko-KR': '/ko' } }<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }} /> in a Server Component (app-controlled schema object — safe, not user input)app/sitemap.ts exports function returning URL listapp/robots.ts exports MetadataRoute.RobotsuseSeoMeta({ title, description, ogTitle, ogImage }) in <script setup>; useHead() for low-level control; nuxt-simple-sitemap module for sitemap; nuxt-simple-robots for robots<svelte:head> in +page.svelte for per-page meta; +server.ts routes for sitemap.xml / robots.txt; svelte-meta-tags for higher-level APIMeta and Title services injected; Universal SSR for server-rendered meta; ngx-meta for declarative APIrender-strategy-decision — generateMetadata runs at strategy time; align with route caching, and SSR/SSG so crawlers see the contenti18n-localization — hreflang alternates.languages for multi-locale sitesrendering-performance — Core Web Vitals are a ranking signal, not just UXgenerateMetadata (unnecessary overhead) — use the static export const metadata form. For dynamic pages, rely on Next.js fetch memoization to dedupe data calls between metadata generation and page render (free win, no manual caching needed). Metadata is necessary but not sufficient: add JSON-LD for rich results, make sure metadata + content are in the server-rendered HTML (client-injected metadata is unreliable for crawlers), and remember robots.txt Disallow blocks crawling but does not remove a page from the index — use noindex for that. The classic disaster is a staging noindex/Disallow: / leaking to production.development
Audit and optimize third-party scripts — analytics, tag managers, chat widgets, embeds — with the right loading strategy, performance budget, facades, and CSP/consent controls. Use when adding a script, when TBT/INP regress, when a GDPR/CCPA consent requirement arises, or before shipping. Not for first-party bundle size (use bundle-optimization) or broad Core Web Vitals diagnosis (use rendering-performance).
development
Apply the Testing Trophy (mostly integration tests with RTL + MSW, sparing E2E with Playwright) and set coverage thresholds. Use before new feature work, after bug fixes, when CI coverage falls below target, or when tests are flaky or break on every refactor. Not for wiring coverage gates + Playwright into the GitHub Actions matrix (use cicd-pipeline) or auditing WCAG a11y compliance (use accessibility-audit).
development
Inventory and prioritize technical debt — TODO/FIXME/HACK, any usage, deprecated APIs, untested logic — with impact × effort matrix. Use at quarter start, before a refactoring sprint, when a new teammate joins, or when feature velocity slows. Not for actually paying down debt (use code-refactoring) or recording a migration approach (use decision-records) — this only inventories and prioritizes.
development
Decision framework for choosing the right state location — URL, server cache, local component, or shared/global store. Use when state-sync bugs appear, prop drilling gets deep (3+ levels), filters/tabs lose state on reload, or quarterly review. Not for form state specifically (use form-ux) or when the state is actually server data (use api-caching-optimization).