一键导入
seo-metadata
SEO patterns for React apps — Next.js Metadata API, Open Graph, structured data (JSON-LD), sitemap generation, and Core Web Vitals optimization.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
SEO patterns for React apps — Next.js Metadata API, Open Graph, structured data (JSON-LD), sitemap generation, and Core Web Vitals optimization.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Orchestrates end-to-end Figma-to-React conversion pipeline with enforced TDD, automated pixel-diff visual QA, E2E testing, and app-type awareness (web apps, Chrome extensions, PWAs). Keywords: Figma to React, design tokens, autonomous component generation, Figma conversion, Tailwind config, component library, TDD, E2E, pixel-perfect
Concurrent phase runner that dispatches independent pipeline phases in parallel, respecting dependency graphs and resource constraints
Automated visual QA with pixel-level diff comparison, iterative fix loop, and cross-browser verification. Uses pixelmatch for programmatic screenshot comparison with region-based analysis. Covers responsive checks, Lighthouse audits, and accessibility validation. Keywords: verify app, visual QA, compare to Figma, check screenshots, responsive test, pixel-perfect check, cross-browser testing, visual diff, pixelmatch
Exports generated components + design-tokens.lock.json as a publishable pnpm workspace. Generates a framework-agnostic tokens package and a framework-specific component library (React/Vue/Svelte via Vite library mode, React Native via tsc). Includes Tailwind preset, ThemeProvider, Changesets versioning, and a properly configured exports map. Keywords: design system export, component library, npm package, vite lib mode, tailwind preset, changesets, monorepo, publishable
Use after React components are built (Phase 4.5 of the Figma/Canva/screenshot pipeline) or whenever a project needs Storybook coverage — auto-generates .stories.tsx and .mdx docs from components via ts-morph AST parsing, with prop controls, variant stories, action args, and default args. Keywords: Storybook, generate stories, .stories.tsx, MDX docs, argTypes, controls, autodocs, CSF3, variant stories, action args, component documentation, story generation, ts-morph
Convert an exported Adobe InDesign IDML package or PDF into typed React components, design tokens, and Storybook stories via the @aurelius/pipeline InDesign pipeline. Use when a designer hands you an .idml or .pdf and you need a starting React component set. Keywords: InDesign to React, IDML, PDF to React, indesign pipeline, design tokens from InDesign, brochure to React, print to web, aurelius pipeline indesign
| name | seo-metadata |
| description | SEO patterns for React apps — Next.js Metadata API, Open Graph, structured data (JSON-LD), sitemap generation, and Core Web Vitals optimization. |
| triggers | ["SEO","metadata","open graph","og image","sitemap","structured data","json-ld","meta tags","social sharing"] |
Define static metadata in any layout.tsx or page.tsx using the exported metadata object. The title.template pattern in the root layout ensures consistent page titles site-wide.
// app/layout.tsx
import { type Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://example.com"),
title: {
default: "My App — Build faster with React",
template: "%s | My App",
},
description:
"A modern React application built with Next.js, TypeScript, and Tailwind CSS.",
openGraph: {
type: "website",
locale: "en_US",
url: "https://example.com",
siteName: "My App",
title: "My App — Build faster with React",
description:
"A modern React application built with Next.js, TypeScript, and Tailwind CSS.",
images: [
{
url: "/og-default.png",
width: 1200,
height: 630,
alt: "My App",
},
],
},
twitter: {
card: "summary_large_image",
title: "My App — Build faster with React",
description:
"A modern React application built with Next.js, TypeScript, and Tailwind CSS.",
creator: "@myapp",
images: ["/og-default.png"],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
alternates: {
canonical: "https://example.com",
},
};
Page-level metadata merges with the layout metadata and uses the title.template:
// app/about/page.tsx
import { type Metadata } from "next";
export const metadata: Metadata = {
title: "About Us", // Renders as "About Us | My App"
description: "Learn about our team and mission.",
alternates: {
canonical: "https://example.com/about",
},
};
export default function AboutPage() {
return <main>...</main>;
}
Use generateMetadata for pages where metadata depends on route params or fetched data.
// app/blog/[slug]/page.tsx
import { type Metadata } from "next";
import { notFound } from "next/navigation";
import { getPost } from "@/lib/posts";
interface PageProps {
params: Promise<{ slug: string }>;
}
export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
return { title: "Post Not Found" };
}
return {
title: post.title,
description: post.excerpt,
authors: [{ name: post.author.name }],
openGraph: {
type: "article",
title: post.title,
description: post.excerpt,
url: `https://example.com/blog/${slug}`,
publishedTime: post.publishedAt,
modifiedTime: post.updatedAt,
authors: [post.author.name],
images: [
{
url: post.coverImage,
width: 1200,
height: 630,
alt: post.title,
},
],
tags: post.tags,
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
alternates: {
canonical: `https://example.com/blog/${slug}`,
},
};
}
export default async function BlogPostPage({ params }: PageProps) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) {
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Use ImageResponse from next/og to generate Open Graph images on demand. This produces shareable social cards with dynamic content.
// app/api/og/route.tsx
import { ImageResponse } from "next/og";
import { type NextRequest } from "next/server";
export const runtime = "edge";
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl;
const title = searchParams.get("title") ?? "My App";
const description = searchParams.get("description") ?? "";
return new ImageResponse(
(
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "flex-start",
width: "100%",
height: "100%",
padding: "60px 80px",
backgroundColor: "#0f172a",
color: "#f8fafc",
fontFamily: "Inter, sans-serif",
}}
>
<div
style={{
fontSize: 64,
fontWeight: 700,
lineHeight: 1.2,
maxWidth: "80%",
}}
>
{title}
</div>
{description && (
<div
style={{
fontSize: 28,
color: "#94a3b8",
marginTop: 24,
maxWidth: "70%",
lineHeight: 1.4,
}}
>
{description}
</div>
)}
<div
style={{
display: "flex",
alignItems: "center",
marginTop: "auto",
fontSize: 24,
color: "#64748b",
}}
>
example.com
</div>
</div>
),
{
width: 1200,
height: 630,
}
);
}
Reference the OG image in metadata:
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
openGraph: {
images: [
{
url: `/api/og?title=${encodeURIComponent(post.title)}&description=${encodeURIComponent(post.excerpt)}`,
width: 1200,
height: 630,
alt: post.title,
},
],
},
};
}
Structured data helps search engines understand your content. Use JSON-LD format, which Google recommends.
// components/JsonLd.tsx
interface JsonLdProps {
data: Record<string, unknown>;
}
export function JsonLd({ data }: JsonLdProps) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
);
}
// app/blog/[slug]/page.tsx
import { JsonLd } from "@/components/JsonLd";
export default async function BlogPostPage({ params }: PageProps) {
const { slug } = await params;
const post = await getPost(slug);
const articleJsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
description: post.excerpt,
image: post.coverImage,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: {
"@type": "Person",
name: post.author.name,
url: post.author.url,
},
publisher: {
"@type": "Organization",
name: "My App",
logo: {
"@type": "ImageObject",
url: "https://example.com/logo.png",
},
},
mainEntityOfPage: {
"@type": "WebPage",
"@id": `https://example.com/blog/${slug}`,
},
};
return (
<>
<JsonLd data={articleJsonLd} />
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
</>
);
}
const productJsonLd = {
"@context": "https://schema.org",
"@type": "Product",
name: product.name,
description: product.description,
image: product.images,
sku: product.sku,
brand: {
"@type": "Brand",
name: product.brand,
},
offers: {
"@type": "Offer",
url: `https://example.com/products/${product.slug}`,
priceCurrency: "USD",
price: product.price,
availability: product.inStock
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",
seller: {
"@type": "Organization",
name: "My Store",
},
},
aggregateRating: product.reviewCount > 0
? {
"@type": "AggregateRating",
ratingValue: product.averageRating,
reviewCount: product.reviewCount,
}
: undefined,
};
// app/layout.tsx
import { JsonLd } from "@/components/JsonLd";
const organizationJsonLd = {
"@context": "https://schema.org",
"@type": "Organization",
name: "My App",
url: "https://example.com",
logo: "https://example.com/logo.png",
sameAs: [
"https://twitter.com/myapp",
"https://github.com/myapp",
"https://linkedin.com/company/myapp",
],
contactPoint: {
"@type": "ContactPoint",
email: "support@example.com",
contactType: "customer support",
},
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<JsonLd data={organizationJsonLd} />
{children}
</body>
</html>
);
}
Next.js supports sitemap.ts files that generate sitemaps at build time or on request.
// app/sitemap.ts
import { type MetadataRoute } from "next";
import { getAllPosts } from "@/lib/posts";
import { getAllProducts } from "@/lib/products";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = "https://example.com";
// Static pages
const staticPages: MetadataRoute.Sitemap = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: "daily",
priority: 1.0,
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${baseUrl}/blog`,
lastModified: new Date(),
changeFrequency: "daily",
priority: 0.9,
},
{
url: `${baseUrl}/contact`,
lastModified: new Date(),
changeFrequency: "yearly",
priority: 0.5,
},
];
// Dynamic blog posts
const posts = await getAllPosts();
const blogPages: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: "weekly" as const,
priority: 0.7,
}));
// Dynamic product pages
const products = await getAllProducts();
const productPages: MetadataRoute.Sitemap = products.map((product) => ({
url: `${baseUrl}/products/${product.slug}`,
lastModified: new Date(product.updatedAt),
changeFrequency: "daily" as const,
priority: 0.8,
}));
return [...staticPages, ...blogPages, ...productPages];
}
For large sites with more than 50,000 URLs, split into multiple sitemaps:
// app/sitemap/[id]/route.ts
import { type MetadataRoute } from "next";
export async function generateSitemaps() {
const totalProducts = await getProductCount();
const sitemapCount = Math.ceil(totalProducts / 50000);
return Array.from({ length: sitemapCount }, (_, i) => ({ id: i }));
}
export default async function sitemap({
id,
}: {
id: number;
}): Promise<MetadataRoute.Sitemap> {
const products = await getProducts({ offset: id * 50000, limit: 50000 });
return products.map((product) => ({
url: `https://example.com/products/${product.slug}`,
lastModified: product.updatedAt,
}));
}
// app/robots.ts
import { type MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
const baseUrl = "https://example.com";
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: ["/api/", "/admin/", "/private/"],
},
{
userAgent: "GPTBot",
disallow: "/",
},
],
sitemap: `${baseUrl}/sitemap.xml`,
};
}
SPAs cannot use the Next.js Metadata API. Use react-helmet-async for client-side meta tags, or consider Vike (formerly vite-plugin-ssr) for SSR.
pnpm add react-helmet-async
// main.tsx
import { HelmetProvider } from "react-helmet-async";
createRoot(document.getElementById("root")!).render(
<HelmetProvider>
<App />
</HelmetProvider>
);
// components/SEO.tsx
import { Helmet } from "react-helmet-async";
interface SEOProps {
title: string;
description: string;
url: string;
image?: string;
type?: "website" | "article";
}
export function SEO({
title,
description,
url,
image = "/og-default.png",
type = "website",
}: SEOProps) {
const siteName = "My App";
const fullTitle = `${title} | ${siteName}`;
return (
<Helmet>
<title>{fullTitle}</title>
<meta name="description" content={description} />
<link rel="canonical" href={url} />
{/* Open Graph */}
<meta property="og:type" content={type} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:url" content={url} />
<meta property="og:image" content={image} />
<meta property="og:site_name" content={siteName} />
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
</Helmet>
);
}
Usage:
function AboutPage() {
return (
<>
<SEO
title="About Us"
description="Learn about our team and mission."
url="https://example.com/about"
/>
<main>...</main>
</>
);
}
SPA Limitations: Client-side meta tags only work if the crawler executes JavaScript. For full SEO support with a Vite app, use Vike for SSR/SSG or a pre-rendering service.
pnpm add vike vike-react
Vike provides file-based routing with SSR, enabling proper meta tags in the initial HTML response. See the Vike documentation for full setup.
Every page shipped to production must satisfy these requirements:
| Requirement | How to Verify |
|---|---|
Unique <title> per page | Each page has distinct, descriptive title (50-60 characters) |
| Meta description | Each page has a unique description (150-160 characters) |
| Open Graph tags | og:title, og:description, og:image, og:url present |
| Twitter card tags | twitter:card, twitter:title, twitter:image present |
| Canonical URL | <link rel="canonical"> on every page to prevent duplicates |
| JSON-LD structured data | Article, Product, Organization, or BreadcrumbList as appropriate |
| Sitemap | sitemap.xml generated and submitted to Google Search Console |
| Robots.txt | Exists, allows indexing of public pages, blocks private routes |
| Image alt text | Every <img> has a descriptive alt attribute |
| Heading hierarchy | Single <h1> per page, logical <h2>-<h6> nesting |
| Core Web Vitals | LCP < 2.5s, INP < 200ms, CLS < 0.1 |
| Mobile-friendly | Responsive layout, <meta name="viewport"> present |
| HTTPS | All pages served over HTTPS |
| No broken links | Internal and external links return 200 |
lang attribute | <html lang="en"> (or appropriate language code) |
# Lighthouse SEO audit
npx lighthouse https://example.com --only-categories=seo --output=json
# Check Open Graph tags
curl -s https://example.com | grep -E 'og:|twitter:'
# Validate structured data
# Use Google's Rich Results Test: https://search.google.com/test/rich-results
# Check sitemap
curl -s https://example.com/sitemap.xml | head -50
# Check robots.txt
curl -s https://example.com/robots.txt