add-seo
Add SEO meta tags, Open Graph tags, and structured data to routes. Use when optimizing pages for search engines or social sharing.
When & Why to Use This Skill
This Claude skill automates the implementation of technical SEO for web applications using modern React 19 patterns. It enables developers to seamlessly integrate essential meta tags, Open Graph data, Twitter cards, and Schema.org structured data (JSON-LD) into their routes, significantly improving search engine visibility and social media preview quality.
Use Cases
- Dynamic Page Metadata: Automatically generate unique, data-driven titles and meta descriptions for product pages or blog posts using React 19 native JSX tags.
- Social Media Optimization: Configure Open Graph and Twitter Card metadata to ensure rich, engaging previews when website links are shared across social platforms.
- Rich Snippet Integration: Implement JSON-LD structured data for Products, Organizations, and Breadcrumbs to help search engines better understand content and display rich search results.
- Crawler Management: Easily manage search engine indexing by adding robots 'noindex' tags to private or sensitive routes to prevent them from appearing in search results.
| name | add-seo |
|---|---|
| description | Add SEO meta tags, Open Graph tags, and structured data to routes. Use when optimizing pages for search engines or social sharing. |
Add SEO
Adds SEO optimization including meta tags, Open Graph, Twitter cards, and structured data using React 19 patterns.
When to Use
- Adding page titles and descriptions
- Setting up Open Graph for social sharing
- Adding Twitter card metadata
- Implementing structured data (JSON-LD)
- User asks to "add SEO", "add meta tags", or "optimize for search"
React 19 Meta Pattern
Use native JSX tags, NOT meta() export.
import type { Route } from './+types/product';
export default function ProductPage({ loaderData }: Route.ComponentProps) {
const { product } = loaderData;
return (
<>
{/* Required */}
<title>{product.name} | Iridium</title>
<meta name="description" content={product.description} />
{/* Open Graph */}
<meta property="og:title" content={product.name} />
<meta property="og:description" content={product.description} />
<meta property="og:image" content={product.imageUrl} />
<meta property="og:type" content="product" />
<meta property="og:url" content={`https://example.com/products/${product.id}`} />
{/* Twitter Card */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={product.name} />
<meta name="twitter:description" content={product.description} />
<meta name="twitter:image" content={product.imageUrl} />
{/* Page content */}
<Container>
<h1>{product.name}</h1>
</Container>
</>
);
}
Basic Page SEO
export default function AboutPage() {
return (
<>
<title>About Us | Iridium</title>
<meta name="description" content="Learn about our mission, team, and values." />
<meta name="robots" content="index, follow" />
<Container>
{/* Page content */}
</Container>
</>
);
}
Dynamic SEO from Loader Data
import type { Route } from './+types/blog-post';
export async function loader({ params }: Route.LoaderArgs) {
const post = await getPost(params.slug);
return { post };
}
export default function BlogPost({ loaderData }: Route.ComponentProps) {
const { post } = loaderData;
const canonicalUrl = `https://example.com/blog/${post.slug}`;
return (
<>
<title>{post.title} | Blog | Iridium</title>
<meta name="description" content={post.excerpt} />
<link rel="canonical" href={canonicalUrl} />
{/* Article-specific Open Graph */}
<meta property="og:type" content="article" />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
<meta property="og:image" content={post.featuredImage} />
<meta property="og:url" content={canonicalUrl} />
<meta property="article:published_time" content={post.publishedAt} />
<meta property="article:author" content={post.author.name} />
<Container>
<article>
<h1>{post.title}</h1>
{/* Post content */}
</article>
</Container>
</>
);
}
Structured Data (JSON-LD)
import type { Route } from './+types/product';
export default function ProductPage({ loaderData }: Route.ComponentProps) {
const { product } = loaderData;
const structuredData = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
description: product.description,
image: product.imageUrl,
offers: {
'@type': 'Offer',
price: product.price,
priceCurrency: 'USD',
availability: 'https://schema.org/InStock',
},
};
return (
<>
<title>{product.name} | Iridium</title>
<meta name="description" content={product.description} />
{/* Structured Data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(structuredData),
}}
/>
<Container>
{/* Product content */}
</Container>
</>
);
}
Organization Structured Data
const organizationData = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'Iridium',
url: 'https://example.com',
logo: 'https://example.com/logo.png',
sameAs: [
'https://twitter.com/iridium',
'https://github.com/iridium',
],
};
Breadcrumb Structured Data
const breadcrumbData = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: 'Home',
item: 'https://example.com',
},
{
'@type': 'ListItem',
position: 2,
name: 'Products',
item: 'https://example.com/products',
},
{
'@type': 'ListItem',
position: 3,
name: product.name,
item: `https://example.com/products/${product.id}`,
},
],
};
No-Index Pages
For pages that shouldn't appear in search:
export default function PrivatePage() {
return (
<>
<title>Private Page | Iridium</title>
<meta name="robots" content="noindex, nofollow" />
{/* Content */}
</>
);
}
SEO Checklist
- Unique
<title>for each page (50-60 chars) -
<meta name="description">(150-160 chars) - Open Graph tags for social sharing
- Twitter Card tags
- Canonical URL for duplicate content
- Structured data where appropriate
- No-index on private pages
Anti-Patterns
- Using
meta()export (old pattern) - Duplicate titles across pages
- Missing descriptions
- Descriptions over 160 characters
- Missing Open Graph image
- Using same meta on all pages
Full Reference
See .github/instructions/seo.instructions.md for comprehensive documentation.