seo-meta
Skill1 starsChanged 8 months agoApache-2.0View on GitHub
Implement SEO metadata for Next.js applications: Metadata API, Open Graph and Twitter tags, JSON-LD structured data, sitemaps, robots, canonical URLs, and dynamic OG images. Use when adding or fixing page titles, descriptions, canonical tags, social cards, schema.org markup, sitemap.ts or robots.ts, or when the user mentions SEO, meta tags, or rich results.
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 4 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 220
Always use absolute URLs
- Very specific: line 303
Use generateMetadata for all meta tags (no manual <meta>)
- Distinctive: line 305
Add JSON-LD structured data (Article, Product, FAQ, Organization)
- Distinctive: line 309
Use title templates in layouts for consistent branding
- Distinctive: line 311
Test with Google Rich Results Test and Search Console
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
SEO Meta Skill
Implement modern SEO patterns using Next.js Metadata API, structured data, and dynamic meta tag generation.
Next.js Metadata API
BAD: Manual meta tags with duplicates and missing canonical.
export default function BlogPost() {
return (
<>
<head>
<title>My Blog Post</title>
<meta name="description" content="Post content" />
{/* missing canonical, og tags, twitter cards */}
</head>
</>
);
}GOOD: Use generateMetadata with complete meta tags.
import { Metadata } from 'next';
// In Next.js 15 and 16, params and searchParams are Promises.
type Props = { params: Promise<{ slug: string }> };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
const url = `https://example.com/blog/${slug}`;
const ogImage = `/api/og?title=${encodeURIComponent(post.title)}`;
return {
title: post.title,
description: post.excerpt,
authors: [{ name: post.author }],
openGraph: {
title: post.title,
description: post.excerpt,
url,
siteName: 'My Site',
images: [{ url: ogImage, width: 1200, height: 630 }],
type: 'article',
publishedTime: post.publishedAt,
},
twitter: { card: 'summary_large_image', images: [ogImage] },
alternates: { canonical: url },
};
}JSON-LD Structured Data
BAD: Invalid JSON and wrong schema types.
<script type="application/ld+json">
{{ name: "Product", price: "$99.99" }} {/* missing @context, wrong types */}
</script>GOOD: Type-safe JSON-LD with proper schema.org vocabulary.
// lib/structured-data.ts
import { WithContext } from 'schema-dts';
export function createArticleSchema(article: {
title: string; author: string; publishedAt: string; url: string;
}): WithContext<'Article'> {
return {
'@context': 'https://schema.org',
'@type': 'Article',
headline: article.title,
datePublished: article.publishedAt,
author: { '@type': 'Person', name: article.author },
publisher: {
'@type': 'Organization',
name: 'My Site',
logo: { '@type': 'ImageObject', url: 'https://example.com/logo.png' },
},
mainEntityOfPage: { '@type': 'WebPage', '@id': article.url },
};
}
export function createProductSchema(product: {
name: string; price: number; currency: string; availability: string;
}): WithContext<'Product'> {
return {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
offers: {
'@type': 'Offer',
price: product.price.toFixed(2),
priceCurrency: product.currency,
availability: `https://schema.org/${product.availability}`,
},
};
}
// Usage
export default function BlogPost({ article }: Props) {
const schema = createArticleSchema(article);
return (
<>
<script
type="application/ld+json"
// Escape "<" so a title like "</script>" cannot break out of the tag.
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema).replace(/</g, '\\u003c') }}
/>
<article>{/* content */}</article>
</>
);
}Dynamic OG Images
BAD: Hardcoded static image for all pages.
export async function generateMetadata(): Promise<Metadata> {
return { openGraph: { images: ['/static-og.png'] } }; // same everywhere
}GOOD: Generate dynamic OG images with next/og (a route handler, or the opengraph-image.tsx file convention next to the page).
// app/api/og/route.tsx
import { ImageResponse } from 'next/og';
export const runtime = 'edge';
export async function GET(request: Request) {
const title = new URL(request.url).searchParams.get('title') || 'Default';
return new ImageResponse(
(
<div style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
width: '100%', height: '100%', display: 'flex',
alignItems: 'center', justifyContent: 'center', padding: '80px',
}}>
<div style={{ fontSize: 60, fontWeight: 'bold', color: 'white' }}>
{title}
</div>
</div>
),
{ width: 1200, height: 630 }
);
}Sitemap and Robots.txt
BAD: Static XML file that never updates.
<!-- public/sitemap.xml -->
<urlset><url><loc>https://example.com/blog/post-1</loc></url></urlset>GOOD: Dynamic sitemap and robots.txt.
// app/sitemap.ts
import { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// Google ignores changeFrequency and priority, so they are left out.
const base = 'https://example.com';
const STATIC_PAGES_UPDATED_AT = '2026-01-15'; // set this when the static pages last changed
const routes = ['', '/about'].map((route) => ({
url: `${base}${route}`,
// Use a real change date. Search engines ignore lastmod when it always equals "now".
lastModified: STATIC_PAGES_UPDATED_AT,
}));
const posts = await getAllPosts();
const postRoutes = posts.map((post) => ({
url: `${base}/blog/${post.slug}`,
lastModified: post.updatedAt,
}));
return [...routes, ...postRoutes];
}
// app/robots.ts
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{ userAgent: '*', allow: '/', disallow: ['/api/', '/admin/'] },
{ userAgent: 'GPTBot', disallow: ['/'] }, // example: opt out of one AI crawler. Remove to allow it.
],
sitemap: 'https://example.com/sitemap.xml',
};
}Canonical URLs and Meta Priority
Canonical URL workflow:
- Identify page type: static, dynamic, paginated, duplicate
- Set canonical in
generateMetadatausingalternates.canonical - For paginated content, point to current page
- For duplicate content, point to original
- Always use absolute URLs
// app/blog/page.tsx (paginated)
export async function generateMetadata({
searchParams,
}: {
searchParams: Promise<{ page?: string }>;
}): Promise<Metadata> {
const { page: pageParam } = await searchParams;
const page = Number(pageParam) || 1;
const base = 'https://example.com/blog';
return {
alternates: { canonical: page === 1 ? base : `${base}?page=${page}` },
};
}
// app/blog/[slug]/print/page.tsx (duplicate)
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
return {
alternates: { canonical: `https://example.com/blog/${slug}` },
robots: { index: false },
};
}Meta tag priority (highest first): Page generateMetadata > Parent layout > Root layout > Manual <meta>.
// app/layout.tsx
export const metadata: Metadata = {
metadataBase: new URL('https://example.com'), // resolves relative OG image and canonical URLs
title: { default: 'My Site', template: '%s | My Site' },
};
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return { title: post.title }; // becomes "Post Title | My Site"
}Core Web Vitals Optimization
BAD: Blocking resources, unoptimized images.
export default function Page() {
return (
<>
<script src="https://example.com/heavy.js" /> {/* blocks render */}
<img src="/hero.jpg" width={1200} /> {/* no optimization */}
</>
);
}GOOD: Preload critical resources, optimize images.
import Image from 'next/image';
import Script from 'next/script';
import { preconnect, prefetchDNS } from 'react-dom';
export default function Page() {
// React 19 emits these as <link> tags in <head>.
preconnect('https://fonts.googleapis.com');
prefetchDNS('https://analytics.example.com');
return (
<>
{/* Load fonts with next/font: it self-hosts and preloads them for you. */}
<Script src="https://example.com/analytics.js" strategy="afterInteractive" />
<Image src="/hero.jpg" width={1200} height={800} alt="Hero" priority />
</>
);
}Implementation Workflow
- Use
generateMetadatafor all meta tags (no manual<meta>) - Include Open Graph and Twitter Card tags with dynamic OG images
- Add JSON-LD structured data (Article, Product, FAQ, Organization)
- Generate dynamic sitemap.ts that updates with content
- Configure robots.ts with proper allow/disallow rules
- Set canonical URLs for all pages (especially paginated/duplicate)
- Use title templates in layouts for consistent branding
- Optimize Core Web Vitals: preload fonts, defer scripts, use next/image
- Test with Google Rich Results Test and Search Console
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- NaveenGumaste/keyskills★ 2same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository21 other files it gives its agents.
- accessibility-wcagSkill
- api-designSkill
- bugbounty-reportSkill
- ci-cd-pipelinesSkill
- codebase-analysisSkill
- database-designSkill
- design-docSkill
- docker-kubernetesSkill
- git-workflowSkill
- go-devSkill
- i18n-localizationSkill
- llm-integrationSkill
- nextjs-16Skill
- oauth-authSkill
- performance-optimizationSkill
- python-best-practicesSkill
- scraping-automationSkill
- security-reviewSkill
- tailwind-cssSkill
- testingSkill
- ts-devSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

