SEO and metadata
Maintain unique title, meta description, and canonical per indexable route; pair Open Graph and Twitter fields; align with hreflang, structured data, and robots/sitemap policy.
For SPA + SSR mixes, the SKILL should state whether first HTML carries critical metadata; CSR-only routes may need prerender or a dynamic meta API.
JSON-LD must reflect visible content—no keyword stuffing. Multilingual sites configure hreflang and x-default with canonical URLs that match production host policy.
Metadata pipeline (skill-flow)
[ Route / build determines page identity ]
│
▼
┌────────────────────│
│HTML head: │──── title, meta description, canonical
│uniqueness + URL │ (matches user-visible URL)
└────────────────────│
│
▼
┌────────────────────│
│Social layer: │──── og:title / og:description / og:url
│OG + Twitter │ twitter:* mirrors or overrides OG
└────────────────────│ og:image / twitter:image: absolute HTTPS
│
▼
┌────────────────────│
│Extensions: │──── JSON-LD (visible content only)
│hreflang / schema │ multilingual alternates + x-default
└────────────────────│
│
▼
┌────────────────────│
│Crawl policy: │──── robots.txt, sitemap, noindex
│staging / accounts │ validate in Search Console–class tools
└────────────────────┘
Production should expose absolute HTTPS URLs for og:image and twitter:image (often 1200×630) with og:image:alt; this site’s head intentionally omits fixed image URLs to avoid broken assets.
Uniqueness and canonical
- Title and description lengths fit SERP norms; avoid sitewide duplicates.
- Canonicalize query, pagination, and tracking variants to the primary URL.
- Keep Core Web Vitals separate from SEO concerns inside the SKILL.
Complete HTML head example with required meta tags (SEO + Open Graph + Twitter Card):
<!-- Complete HTML head example: SEO + Open Graph + Twitter Card -->
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Core SEO: title 55-65 chars, description 150-160 chars -->
<title>Product Name - Concise Value Proposition | Brand</title>
<meta name="description"
content="Under 160 characters summarizing the page with core keywords, written to encourage clicks. Unique per page." />
<!-- canonical: eliminate duplicate indexing (tracking params, pagination, etc.) -->
<link rel="canonical" href="https://www.example.com/products/widget" />
<!-- Open Graph (social sharing) -->
<meta property="og:type" content="article" />
<meta property="og:url" content="https://www.example.com/products/widget" />
<meta property="og:title" content="Product Name - Value Proposition" />
<meta property="og:description" content="Summary for social sharing; may differ from meta description." />
<meta property="og:image" content="https://www.example.com/og/widget.jpg" />
<meta property="og:image:alt" content="Text description of product image (accessibility)" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:locale" content="en_US" />
<meta property="og:site_name" content="Brand Name" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Product Name (can be shorter than og:title)" />
<meta name="twitter:description" content="Twitter share summary." />
<meta name="twitter:image" content="https://www.example.com/og/widget.jpg" />
<meta name="twitter:image:alt" content="Product image description" />
<meta name="twitter:site" content="@yourbrand" />
<!-- Multilingual hreflang -->
<link rel="alternate" hreflang="en"
href="https://www.example.com/products/widget" />
<link rel="alternate" hreflang="zh-CN"
href="https://www.example.com/zh/products/widget" />
<link rel="alternate" hreflang="x-default"
href="https://www.example.com/products/widget" />
</head>
Open Graph and Twitter
og:url should match canonical; with twitter:card=summary_large_image, provide large image URLs and meaningful alt.
- Maintain share image dimensions and alt separately from the page hero if needed.
- Twitter-specific tags may override OG when you want shorter share titles.
Structured data and hreflang
JSON-LD types must match the page (Article, Product, BreadcrumbList, etc.); do not emit schema for invisible content.
JSON-LD structured data examples (Article / Product / BreadcrumbList):
<!-- Article JSON-LD -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "Article Title (under 110 characters)",
"description": "Article summary, may match meta description.",
"author": {
"@type": "Person",
"name": "Author Name",
"url": "https://www.example.com/authors/jane"
},
"publisher": {
"@type": "Organization",
"name": "Publisher Name",
"logo": { "@type": "ImageObject", "url": "https://www.example.com/logo.png" }
},
"datePublished": "2024-01-15T08:00:00Z",
"dateModified": "2024-03-20T12:00:00Z",
"image": "https://www.example.com/articles/hero.jpg",
"url": "https://www.example.com/articles/my-article"
}
</script>
<!-- Product JSON-LD -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Product Name",
"image": "https://www.example.com/products/widget.jpg",
"description": "Product description.",
"sku": "WIDGET-001",
"offers": {
"@type": "Offer",
"price": "29.99",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock",
"url": "https://www.example.com/products/widget"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.8",
"reviewCount": "120"
}
}
</script>
<!-- BreadcrumbList JSON-LD -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://www.example.com/" },
{ "@type": "ListItem", "position": 2, "name": "Products", "item": "https://www.example.com/products/" },
{ "@type": "ListItem", "position": 3, "name": "Widget", "item": "https://www.example.com/products/widget" }
]
}
</script>
robots, sitemap, and noindex
Split responsibilities: robots.txt, sitemap, and noindex for staging, account pages, and duplicate parameter URLs—re-validate after changes.
robots.txt configuration example:
# /robots.txt — production
User-agent: *
Allow: /
# Block indexing of account pages, duplicate params, and API paths
Disallow: /account/
Disallow: /api/
Disallow: /*?ref= # tracking parameters
Disallow: /*?sort= # sort-only parameter pages
Disallow: /search?q= # empty search result pages
# Declare sitemap location
Sitemap: https://www.example.com/sitemap.xml
Sitemap: https://www.example.com/sitemap-products.xml
sitemap.xml generation script (Node.js, compatible with Next.js / static sites):
// scripts/generate-sitemap.mjs
import { writeFileSync } from 'fs';
import { getAllProducts, getAllArticles } from './lib/api.mjs';
const BASE_URL = 'https://www.example.com';
async function generateSitemap() {
const products = await getAllProducts();
const articles = await getAllArticles();
const staticPages = ['/', '/about', '/contact', '/products'];
const urls = [
// Static pages
...staticPages.map(path => ({
loc: `${BASE_URL}${path}`,
changefreq: 'weekly',
priority: path === '/' ? '1.0' : '0.8',
lastmod: new Date().toISOString().split('T')[0],
})),
// Dynamic product pages
...products.map(p => ({
loc: `${BASE_URL}/products/${p.slug}`,
changefreq: 'daily',
priority: '0.9',
lastmod: p.updatedAt,
})),
// Article pages
...articles.map(a => ({
loc: `${BASE_URL}/articles/${a.slug}`,
changefreq: 'monthly',
priority: '0.7',
lastmod: a.updatedAt,
})),
];
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.map(u => ` <url>
<loc>${u.loc}</loc>
<lastmod>${u.lastmod}</lastmod>
<changefreq>${u.changefreq}</changefreq>
<priority>${u.priority}</priority>
</url>`).join('\n')}
</urlset>`;
writeFileSync('./public/sitemap.xml', xml);
console.log(`Generated sitemap with ${urls.length} URLs`);
}
generateSitemap();
SKILL snippet
---
name: seo-metadata
description: Maintain page SEO metadata, canonical URLs, and structured data
tags: [seo, metadata, structured-data, sitemap]
---
# Basic Metadata
- title: 55-65 chars, unique per page; format: keyword - value proposition | brand
- meta description: 150-160 chars with core keywords, never duplicated site-wide
- canonical matches the final user-visible URL, eliminating duplicate indexing from params/pagination
# Open Graph and Twitter Card
- og:url must exactly match canonical
- og:image: absolute HTTPS URL, 1200x630, with og:image:alt
- twitter:card = summary_large_image requires twitter:image
- og:locale corresponds to page language (en_US / zh_CN)
# Structured Data (JSON-LD)
- Type matches visible page content: Article / Product / BreadcrumbList
- Never generate JSON-LD for invisible or synthetic content (avoid penalties)
- Validate with Google Rich Results Test after each deployment
# Multilingual and Crawl Policy
- hreflang comes in pairs (mutually referencing); x-default points to the default locale
- robots.txt blocks account pages, API paths, tracking parameter variants
- Staging environment uses noindex in HTTP header or meta tag
- sitemap.xml contains only indexable canonical URLs; lastmod uses real modification time
# Core Web Vitals and CI
- LCP < 2.5s: prioritize image sizing, preload, CDN
- CLS < 0.1: set explicit width/height on images/ads/fonts to prevent layout shift
- INP < 200ms: reduce long tasks, split interaction handlers into idle frames
- Lighthouse CI gates performance / SEO / accessibility scores in PRs
Head checklist (page JS)
Generate an agent-facing head checklist from render mode and toggles—aligned with the pipeline above.
Output