Vibecosystem seo-patterns
Meta tag patterns, structured data (JSON-LD), Core Web Vitals optimization, and SSR/SSG strategies for search visibility.
install
source · Clone the upstream repo
git clone https://github.com/vibeeval/vibecosystem
manifest:
skills/seo-patterns/skill.mdsource content
SEO Patterns
Technical SEO patterns for web applications and content sites.
Meta Tags Template
// 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, } } }
Structured Data (JSON-LD)
// 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> }
Core Web Vitals Optimization
// 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) }} /> ) }
SSR/SSG Strategy
// 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'
Sitemap and Robots
// 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', } }
Checklist
- Unique title (50-60 chars) and description (150-160 chars) per page
- Canonical URL set on every page (avoid duplicate content)
- JSON-LD structured data for products, FAQs, breadcrumbs, articles
- Open Graph + Twitter Card meta tags for social sharing
- LCP < 2.5s: preload hero images, inline critical CSS
- CLS < 0.1: explicit dimensions on all images/embeds
- INP < 200ms: debounce inputs, defer heavy JS
- Dynamic sitemap.xml updated with all crawlable pages
- robots.txt blocks API, admin, and auth routes from crawling
- hreflang tags for multi-language sites
SaaS Landing Page Anatomy
┌──────────────────────────────────────┐ │ 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 │ └──────────────────────────────────────┘
Hero Section Formulas
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."
Pricing Page SEO
// 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
SaaS-Specific Structured Data
// 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> }
Anti-Patterns
- Client-side only rendering: search engines may not execute JS
- Duplicate content without canonical: pages compete against themselves
- Missing alt text on images: accessibility and image search penalty
- Blocking CSS/JS in robots.txt: prevents proper rendering by crawlers
- Infinite scroll without pagination URLs: content invisible to crawlers
- Meta description over 160 chars: truncated in search results
- Pricing page without structured data: misses rich snippet opportunity
- Landing page H1 focused on brand, not value: low click-through from SERPs