Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Meta tag patterns, structured data (JSON-LD), Core Web Vitals optimization, and SSR/SSG strategies for search visibility.
.claude/skills/seo-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-16 | ✗→✓ | ▲ Improved | — | — |
| case-08 | ✗→✗ | = Same ✗ | — | — |
| case-09 | ✗→✗ | = Same ✗ | — | — |
| case-11 | ✗→✗ | = Same ✗ | — | — |
Technical SEO patterns for web applications and content sites.
typescript// Next.js App Router metadata import { Metadata } from 'next' export function generateMetadata({ params }): Metadata { const product = getProduct(params.slug) return { title: `${product.name} | Your App`, // 50-60 chars description: product.summary.slice(0, 155), // 150-160 chars alternates: { canonical: `https://example.com/products/${params.slug}`, languages: { 'en': `https://example.com/en/products/${params.slug}`, 'tr': `https://example.com/tr/products/${params.slug}`, } }, openGraph: { title: product.name, description: product.summary, url: `https://example.com/products/${params.slug}`, siteName: 'Your App', images: [{ url: product.imageUrl, width: 1200, height: 630, alt: product.name, }], type: 'website', locale: 'en_US', }, twitter: { card: 'summary_large_image', title: product.name, description: product.summary, images: [product.imageUrl], }, robots: { index: true, follow: true, 'max-image-preview': 'large', 'max-snippet': -1, } } }
typescript// Product schema function ProductJsonLd({ product }: { product: Product }) { const schema = { '@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: 'Your App', } }, aggregateRating: product.reviewCount > 0 ? { '@type': 'AggregateRating', ratingValue: product.avgRating, reviewCount: product.reviewCount, } : undefined, } return <script type="application/ld+json">{JSON.stringify(schema)}</script> } // FAQ schema (wins featured snippets) function FaqJsonLd({ faqs }: { faqs: { question: string; answer: string }[] }) { const schema = { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faqs.map(faq => ({ '@type': 'Question', name: faq.question, acceptedAnswer: { '@type': 'Answer', text: faq.answer, } })) } return <script type="application/ld+json">{JSON.stringify(schema)}</script> } // BreadcrumbList schema function BreadcrumbJsonLd({ items }: { items: { name: string; url: string }[] }) { const schema = { '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: items.map((item, i) => ({ '@type': 'ListItem', position: i + 1, name: item.name, item: item.url, })) } return <script type="application/ld+json">{JSON.stringify(schema)}</script> }
typescript// LCP (Largest Contentful Paint) - Target: < 2.5s // Priority: preload hero image, avoid lazy-loading above-fold content import Image from 'next/image' function HeroSection({ image }: { image: string }) { return ( <Image src={image} alt="Hero" width={1200} height={600} priority // Preload, no lazy loading sizes="100vw" // Responsive sizing hints quality={85} // Balance quality vs size /> ) } // CLS (Cumulative Layout Shift) - Target: < 0.1 // Always set explicit width/height on images and embeds function VideoEmbed({ videoId }: { videoId: string }) { return ( <div style={{ aspectRatio: '16/9', width: '100%' }}> <iframe src={`https://youtube.com/embed/${videoId}`} width="100%" height="100%" loading="lazy" title="Video" /> </div> ) } // INP (Interaction to Next Paint) - Target: < 200ms // Defer heavy computation, use web workers function SearchResults() { const [query, setQuery] = useState('') const [results, setResults] = useState([]) // Debounce input to prevent blocking main thread const debouncedSearch = useMemo( () => debounce((q: string) => { startTransition(() => { setResults(search(q)) }) }, 300), [] ) return ( <input type="search" onChange={(e) => { setQuery(e.target.value) debouncedSearch(e.target.value) }} /> ) }
typescript// Static Generation (SSG): content that rarely changes // Best for: blog posts, product pages, documentation export async function generateStaticParams() { const products = await getProductSlugs() return products.map(slug => ({ slug })) } export default async function ProductPage({ params }) { const product = await getProduct(params.slug) return <ProductDetail product={product} /> } // Incremental Static Regeneration (ISR): SSG with refresh export const revalidate = 3600 // Regenerate every hour // Server-Side Rendering (SSR): dynamic, personalized content // Use only when content changes per request (user-specific, real-time data) export const dynamic = 'force-dynamic'
typescript// app/sitemap.ts export default async function sitemap(): MetadataRoute.Sitemap { const products = await getAllProducts() const posts = await getAllBlogPosts() return [ { url: 'https://example.com', lastModified: new Date(), priority: 1.0 }, ...products.map(p => ({ url: `https://example.com/products/${p.slug}`, lastModified: p.updatedAt, changeFrequency: 'weekly' as const, priority: 0.8, })), ...posts.map(p => ({ url: `https://example.com/blog/${p.slug}`, lastModified: p.updatedAt, changeFrequency: 'monthly' as const, priority: 0.6, })), ] } // app/robots.ts export default function robots(): MetadataRoute.Robots { return { rules: [ { userAgent: '*', allow: '/', disallow: ['/api/', '/admin/', '/checkout/'] }, ], sitemap: 'https://example.com/sitemap.xml', } }
┌──────────────────────────────────────┐
│ Nav: Logo | Features | Pricing | CTA │ ← Sticky, minimal
├──────────────────────────────────────┤
│ HERO SECTION │
│ H1: Value proposition (6-12 words) │
│ Subtitle: How it works (1 sentence) │
│ CTA button + social proof line │
│ Hero image/screenshot │
├──────────────────────────────────────┤
│ SOCIAL PROOF BAR │
│ "Trusted by X teams" + logos │
├──────────────────────────────────────┤
│ FEATURES (3-4 cards) │
│ Icon + Title + 1 sentence each │
├──────────────────────────────────────┤
│ HOW IT WORKS (3 steps) │
│ Step 1 → Step 2 → Step 3 │
├──────────────────────────────────────┤
│ TESTIMONIALS (2-3 quotes) │
│ Photo + Name + Role + Quote │
├──────────────────────────────────────┤
│ PRICING (3 tiers) │
│ Free | Pro (highlighted) | Enterprise│
├──────────────────────────────────────┤
│ FAQ (5-8 questions, JSON-LD) │
├──────────────────────────────────────┤
│ FINAL CTA │
│ Repeat hero CTA with urgency │
├──────────────────────────────────────┤
│ FOOTER │
│ Legal links + sitemap links │
└──────────────────────────────────────┘Formula 1 — Problem-Solution:
H1: "Stop [pain point]. Start [desired outcome]."
Example: "Stop losing leads. Start converting visitors."
Formula 2 — Before-After:
H1: "[Tool] turns [bad state] into [good state]."
Example: "[Product] turns messy spreadsheets into real-time dashboards."
Formula 3 — Social Proof Lead:
H1: "[N]+ teams use [tool] to [outcome]."
Example: "2,000+ teams use [Product] to ship 3x faster."typescript// Pricing page structured data function PricingJsonLd({ plans }: { plans: Plan[] }) { const schema = { '@context': 'https://schema.org', '@type': 'WebPage', name: 'Pricing', description: 'Plans and pricing for Your App', mainEntity: plans.map(plan => ({ '@type': 'Offer', name: plan.name, price: plan.price, priceCurrency: 'USD', description: plan.description, eligibleDuration: { '@type': 'QuantitativeValue', value: 1, unitCode: 'MON' } })) } return <script type="application/ld+json">{JSON.stringify(schema)}</script> } // SEO tips for pricing pages: // - Title: "[Product] Pricing — Free, Pro & Enterprise Plans" // - Include pricing in meta description (Google shows it in snippets) // - Use comparison table with feature checkmarks // - FAQ section below pricing (common objections → JSON-LD) // - "Free tier" callout improves CTR from search results
typescript// SoftwareApplication schema (rich results for SaaS) function SoftwareAppJsonLd({ app }: { app: SaaSApp }) { const schema = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: app.name, applicationCategory: app.category, // 'BusinessApplication', 'DeveloperApplication' operatingSystem: 'Web', offers: { '@type': 'AggregateOffer', lowPrice: app.freeTier ? '0' : app.lowestPrice, highPrice: app.highestPrice, priceCurrency: 'USD', offerCount: app.planCount }, aggregateRating: app.reviewCount > 0 ? { '@type': 'AggregateRating', ratingValue: app.avgRating, ratingCount: app.reviewCount, bestRating: 5, worstRating: 1 } : undefined, screenshot: app.screenshotUrl, featureList: app.features.join(', ') } return <script type="application/ld+json">{JSON.stringify(schema)}</script> }
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.