Skip to the content.
seo

Search Engine Optimization

Glossary

Detailed Description

Crawling & Indexing

Search Engine Optimization in modern frontend architecture solves a fundamental conflict: search engines prefer fast-loading, semantic HTML with clear content hierarchy, while modern SPAs prioritize rich interactivity through JavaScript frameworks. Without proper SEO implementation, Google’s crawler sees <div id="root"></div> and nothing more—your beautifully designed React app is invisible to search engines, social media previews show blank pages, and organic traffic remains zero despite excellent content. The fix is to give crawlers real HTML on the first response — see ssr.md for server-side rendering and ssg.md for build-time pre-rendering.

The core SEO pillars for frontend applications: (1) Server-Side Rendering ensuring crawlers receive fully-rendered HTML instead of waiting for JavaScript execution (critical for initial indexing and social previews — see ssr.md), (2) Meta Tags providing title, description, Open Graph, Twitter Cards in HTML <head> (controls how pages appear in search results and social shares), (3) Structured Data using JSON-LD schema markup describing content semantically (enables rich snippets like star ratings, breadcrumbs, FAQs in search results), (4) Semantic HTML with proper heading hierarchy, alt text, ARIA labels (improves accessibility and crawler understanding), (5) Performance Optimization via Core Web Vitals (LCP, INP, CLS directly impact rankings since Google’s Page Experience update — note that INP replaced FID as the responsiveness metric in March 2024).

Core Web Vitals

Performance SEO gained massive importance with Google’s Page Experience ranking factor prioritizing Core Web Vitals (see also web.dev’s vitals reference under References). LCP (Largest Contentful Paint) <2.5s measures main content visibility—optimize by preloading critical assets, using SSG/SSR for above-fold content (see ssg.md), implementing image optimization. INP (Interaction to Next Paint) <200ms measures interactivity responsiveness across the page lifecycle (replacing the older FID metric in 2024)—optimize by code splitting, lazy loading below-fold components, minimizing long JavaScript tasks. CLS (Cumulative Layout Shift) <0.1 prevents jarring layout changes—fix by always specifying image dimensions, reserving space for dynamic content, avoiding inserting content above existing content.

Metadata & Open Graph

Meta tag implementation requires dynamic per-page values, not static sitewide tags. Homepage needs “Buy Premium Widgets WidgetCo” title and “Shop our collection of 500+ premium widgets…” description, while product pages need “Widget Pro 3000 - $99.99 WidgetCo” with specific product descriptions. Next.js generateMetadata, React 19’s native <title>/<meta> hoisting, or framework-specific solutions inject tags dynamically based on route/content. Critical tags: title (50-60 chars), description (150-160 chars), canonical URL (prevent duplicate content), Open Graph (og:title, og:description, og:image for Facebook/LinkedIn), Twitter Cards (twitter:card, twitter:image for Twitter previews).

Structured Data

Structured data transforms search results from plain blue links into rich snippets with images, ratings, prices, breadcrumbs, FAQs. JSON-LD script tags inject schema.org vocabulary describing content types—Product schema includes name/price/availability, Article schema has headline/author/datePublished, Recipe schema shows cooking time/ingredients. Google’s Rich Results Test validates markup and previews appearance. Example: Product schema creates search results showing “$99.99 - In Stock (234 reviews)” instead of just plain text description, dramatically improving click-through rates (20-30% boost typical).

URL Structure & Canonicals

Canonical URLs prevent duplicate content penalties when same content appears at multiple URLs (www vs non-www, trailing slashes, query parameters). Use <link rel="canonical"> specifying preferred URL. Implement 301 redirects from duplicate URLs to canonical versions. For paginated content, link pages together with prev/next semantics in your navigation. Sitemap generation automates discovery of all indexable pages, especially critical for large SPAs where crawlers might miss dynamically generated routes. Generate XML sitemaps during build listing all URLs with lastmod dates, changefreq hints, priority scores. Submit to Google Search Console for faster indexing. For dynamic content, implement dynamic sitemaps that read your database/CMS and generate XML on demand. Include image sitemaps for image search optimization and video sitemaps for video content discovery.

hreflang & Internationalization

International sites use hreflang tags indicating language/region variants preventing wrong-language pages ranking in wrong regions. Each localized URL must reference every other variant — including itself — with a matching <link rel="alternate" hreflang="en-US" href="…"> entry, plus an x-default for the fallback. Mismatches (one page lists fr-CA but the French page doesn’t list back) cause search engines to ignore the cluster entirely.

Performance & Mobile

Beyond Core Web Vitals, mobile-first indexing means Google primarily uses the mobile version of your site for ranking. Ensure parity between desktop and mobile content (don’t hide text behind “read more” toggles that aren’t expanded), test with real-device throttling, and prefer responsive images (srcset / <picture>) over UA-sniffed assets. Pair this with the build/runtime tactics described in ssg.md and the runtime work in ssr.md for the best result.

Key Insight

SEO for frontend applications centers on making JavaScript-rendered content discoverable and crawlable by search engines through server-side rendering, semantic HTML, meta tags, structured data (JSON-LD), and Core Web Vitals optimization—transforming dynamic SPAs from invisible black boxes (empty <div id="root">) into rich, indexable content that ranks highly in search results and generates engaging social media previews.

Code Examples

Basic Example: Next.js App Router generateMetadata

Modern App Router pages declare metadata via the async generateMetadata export — Next.js renders the <head> for you, so there’s no <Head> component or pages/_document.js boilerplate. Cross-reference: ssr.md for the rendering pipeline this metadata rides on.

// ===== app/layout.js =====
// Root metadata applies to every route unless a child overrides it.

export const metadata = {
  metadataBase: new URL('https://example.com'),
  title: {
    default: 'YourSite',
    template: '%s | YourSite',
  },
  description: 'Default site description',
  openGraph: {
    siteName: 'YourSite',
    type: 'website',
    images: ['/default-og-image.jpg'],
  },
  twitter: {
    card: 'summary_large_image',
    creator: '@yoursite',
  },
  robots: { index: true, follow: true },
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}


// ===== app/products/[id]/page.js =====
// Per-route metadata is computed asynchronously from the same data
// the page itself uses — Next dedupes the fetch.

export async function generateMetadata({ params }) {
  const product = await fetchProduct(params.id);
  const url = `/products/${product.id}`;

  return {
    title: `${product.name} - $${product.price}`,
    description: product.description.slice(0, 155),
    alternates: { canonical: url },
    openGraph: {
      type: 'product',
      url,
      title: product.name,
      description: product.description.slice(0, 155),
      images: [product.image],
    },
    twitter: {
      card: 'summary_large_image',
      title: product.name,
      images: [product.image],
    },
  };
}

export default async function ProductPage({ params }) {
  const product = await fetchProduct(params.id);

  return (
    <div>
      <h1>{product.name}</h1>
      <p>${product.price}</p>
      <img src={product.image} alt={product.name} />
    </div>
  );
}

Practical Example: Structured Data with JSON-LD

Rich snippets for products, articles, and breadcrumbs:

// ===== components/StructuredData.js =====
export default function StructuredData({ data }) {
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
    />
  );
}


// ===== pages/products/[id].js =====
import StructuredData from '@/components/StructuredData';

export default function ProductPage({ product }) {
  // Product Schema
  const productSchema = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    image: product.images,
    description: product.description,
    sku: product.sku,
    brand: {
      '@type': 'Brand',
      name: product.brand
    },
    offers: {
      '@type': 'Offer',
      url: `https://example.com/products/${product.id}`,
      priceCurrency: 'USD',
      price: product.price,
      availability: product.inStock 
        ? 'https://schema.org/InStock'
        : 'https://schema.org/OutOfStock',
      priceValidUntil: '2026-12-31'
    },
    aggregateRating: {
      '@type': 'AggregateRating',
      ratingValue: product.rating,
      reviewCount: product.reviewCount
    }
  };
  
  // Breadcrumb Schema
  const breadcrumbSchema = {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    itemListElement: [
      {
        '@type': 'ListItem',
        position: 1,
        name: 'Home',
        item: 'https://example.com'
      },
      {
        '@type': 'ListItem',
        position: 2,
        name: product.category,
        item: `https://example.com/category/${product.categorySlug}`
      },
      {
        '@type': 'ListItem',
        position: 3,
        name: product.name,
        item: `https://example.com/products/${product.id}`
      }
    ]
  };
  
  return (
    <>
      <SEO
        title={`${product.name} | YourSite`}
        description={product.description}
      />
      <StructuredData data={productSchema} />
      <StructuredData data={breadcrumbSchema} />
      
      <ProductView product={product} />
    </>
  );
}


// ===== pages/blog/[slug].js =====
// Article Schema for blog posts

export default function BlogPost({ post }) {
  const articleSchema = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: post.title,
    image: post.coverImage,
    author: {
      '@type': 'Person',
      name: post.author.name,
      url: `https://example.com/authors/${post.author.slug}`
    },
    publisher: {
      '@type': 'Organization',
      name: 'YourSite',
      logo: {
        '@type': 'ImageObject',
        url: 'https://example.com/logo.png'
      }
    },
    datePublished: post.publishedAt,
    dateModified: post.updatedAt,
    description: post.excerpt,
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': `https://example.com/blog/${post.slug}`
    }
  };
  
  return (
    <>
      <SEO
        title={`${post.title} | Blog`}
        description={post.excerpt}
        image={post.coverImage}
        type="article"
      />
      <StructuredData data={articleSchema} />
      
      <article>
        <h1>{post.title}</h1>
        <time>{post.publishedAt}</time>
        <div dangerouslySetInnerHTML={{ __html: post.content }} />
      </article>
    </>
  );
}


// ===== FAQ Schema =====
const faqSchema = {
  '@context': 'https://schema.org',
  '@type': 'FAQPage',
  mainEntity: [
    {
      '@type': 'Question',
      name: 'What is your return policy?',
      acceptedAnswer: {
        '@type': 'Answer',
        text: 'We accept returns within 30 days of purchase...'
      }
    },
    {
      '@type': 'Question',
      name: 'Do you ship internationally?',
      acceptedAnswer: {
        '@type': 'Answer',
        text: 'Yes, we ship to over 50 countries worldwide...'
      }
    }
  ]
};

Advanced Example: App Router Sitemap, Robots, and Vitals

App Router exposes file conventions for sitemap.xml and robots.txt, and the root layout handles preconnect/manifest links — no pages/_document.js needed.

// ===== app/sitemap.ts =====
// Returning an array of MetadataRoute.Sitemap entries; Next emits sitemap.xml.

import type { MetadataRoute } from 'next';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const [posts, products] = await Promise.all([
    fetchAllPosts(),
    fetchAllProducts(),
  ]);

  const staticUrls: MetadataRoute.Sitemap = [
    { url: 'https://example.com', lastModified: new Date(), changeFrequency: 'daily', priority: 1.0 },
    { url: 'https://example.com/about', lastModified: '2026-01-01', changeFrequency: 'monthly', priority: 0.8 },
  ];

  const postUrls = posts.map((post) => ({
    url: `https://example.com/blog/${post.slug}`,
    lastModified: post.updatedAt,
    changeFrequency: 'weekly' as const,
    priority: 0.7,
  }));

  const productUrls = products.map((product) => ({
    url: `https://example.com/products/${product.id}`,
    lastModified: product.updatedAt,
    changeFrequency: 'daily' as const,
    priority: 0.9,
  }));

  return [...staticUrls, ...postUrls, ...productUrls];
}


// ===== app/robots.ts =====
// File-convention robots.txt — Next serves it at /robots.txt.

import type { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
  return {
    rules: { userAgent: '*', allow: '/', disallow: ['/admin', '/api'] },
    sitemap: 'https://example.com/sitemap.xml',
  };
}


// ===== next.config.js =====
// Modern config: no swcMinify (default since 13) and no optimizeFonts (handled by next/font).

module.exports = {
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920],
  },
  compiler: {
    removeConsole: process.env.NODE_ENV === 'production',
  },
  headers: async () => [
    {
      source: '/:all*(svg|jpg|png)',
      headers: [{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }],
    },
  ],
};


// ===== app/layout.js =====
// Preconnect + manifest live in the root layout's <head>.

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link rel="preconnect" href="https://cdn.example.com" />
        <link rel="dns-prefetch" href="https://analytics.google.com" />
        <link rel="icon" href="/favicon.ico" />
        <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
        <link rel="manifest" href="/manifest.json" />
        <meta name="theme-color" content="#000000" />
      </head>
      <body>{children}</body>
    </html>
  );
}

Common Mistakes

1. Not Using Unique Titles/Descriptions Per Page

Mistake: Same meta tags across all pages.

// ❌ BAD: Static SEO on every page
<Head>
  <title>YourSite - Best Products</title>
  <meta name="description" content="Shop our products" />
</Head>
// Google sees duplicate titles/descriptions, hurts rankings
// ✅ GOOD: Dynamic per-page SEO
<SEO
  title={`${product.name} - $${product.price} | YourSite`}
  description={product.description.substring(0, 155)}
/>
// Each page has unique, relevant metadata

Why it matters: Duplicate titles confuse search engines about page topics. Unique, descriptive titles improve click-through rates by 20-30%.

2. Client-Side Only Rendering for Content

Mistake: Rendering content only via JavaScript.

// ❌ BAD: CSR without SSR
export default function BlogPost() {
  const [post, setPost] = useState(null);
  
  useEffect(() => {
    fetch(`/api/posts/${slug}`).then(r => r.json()).then(setPost);
  }, []);
  
  return <div>{post?.title}</div>;
}
// Crawlers see empty page, no content indexed
// ✅ GOOD: SSR/SSG for indexable content (App Router)
export default async function BlogPost({ params }) {
  const post = await fetchPost(params.slug);
  return <h1>{post.title}</h1>;
  // Crawlers receive fully-rendered HTML
}

Why it matters: Google can execute JavaScript, but SSR/SSG (see ssr.md, ssg.md) guarantees immediate indexing and proper social previews.

3. Missing Alt Text on Images

Mistake: Images without descriptive alt attributes.

// ❌ BAD: No alt text
<img src="/product.jpg" />
// Screen readers can't describe image
// Image search doesn't index
// ✅ GOOD: Descriptive alt text
<img
  src="/product.jpg"
  alt="Red leather messenger bag with brass buckles"
/>
// Accessible + indexed in image search

Why it matters: Alt text improves accessibility (WCAG requirement) and drives image search traffic (10-15% of total traffic for e-commerce).

Quick Quiz

What's the difference between Open Graph and Twitter Card meta tags?

How does JSON-LD structured data help SEO?

Why are Core Web Vitals important for SEO?

When should you use a canonical tag?

How do you make client-side-navigated SPA pages SEO-friendly?