SEO & Analytics
Search Engine Optimization (SEO) ensures your site ranks well in search results and provides the right content to users. Analytics measures how users interact with your site once they arrive. Together, they form a feedback loop: SEO drives traffic, analytics measures effectiveness, and insights inform further optimization.
This guide covers technical SEO fundamentals (sitemaps, structured data, meta tags), analytics setup (Google Analytics 4, privacy-focused alternatives), and A/B testing frameworks.
Google Search Console (GSC) is the primary tool for monitoring your site's presence in Google Search. It shows indexing status, search queries, click-through rates, and technical issues.
| Feature | What It Shows | Action |
|---|---|---|
| Performance | Queries, clicks, impressions, CTR, position | Identify high-opportunity keywords |
| Index Coverage | Pages indexed vs. excluded with errors | Fix indexing errors, submit sitemaps |
| Core Web Vitals | URL-level LCP, CLS, INP from real users | Identify and fix slow pages |
| Mobile Usability | Mobile-specific rendering issues | Fix viewport, tap targets, font issues |
| Links | Internal and external linking | Improve internal link structure |
| 1 | # Verify site ownership via Google Search Console: |
| 2 | # Methods: DNS TXT record, HTML file upload, HTML meta tag, Google Analytics |
| 3 | |
| 4 | # DNS TXT record method: |
| 5 | # Add this to your DNS: |
| 6 | google-site-verification=abcdef1234567890 |
| 7 | |
| 8 | # Or HTML meta tag in <head>: |
| 9 | <meta name="google-site-verification" content="abcdef1234567890" /> |
| 10 | |
| 11 | # Submit sitemap via Search Console: |
| 12 | # 1. Go to Search Console > Sitemaps |
| 13 | # 2. Enter: https://example.com/sitemap.xml |
| 14 | # 3. Click Submit |
| 15 | |
| 16 | # Request indexing after content update: |
| 17 | # Use the URL Inspection tool to request re-crawling |
A sitemap.xml tells search engines which pages exist and when they were last updated. Next.js provides built-in sitemap generation via the app router.
| 1 | // app/sitemap.ts — Next.js sitemap generation |
| 2 | import { MetadataRoute } from 'next'; |
| 3 | |
| 4 | export default function sitemap(): MetadataRoute.Sitemap { |
| 5 | const baseUrl = 'https://example.com'; |
| 6 | |
| 7 | // Static routes |
| 8 | const staticRoutes = [ |
| 9 | { url: baseUrl, lastModified: new Date(), changeFrequency: 'weekly', priority: 1.0 }, |
| 10 | { url: `${baseUrl}/about`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.8 }, |
| 11 | { url: `${baseUrl}/blog`, lastModified: new Date(), changeFrequency: 'daily', priority: 0.9 }, |
| 12 | { url: `${baseUrl}/contact`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.7 }, |
| 13 | ] as MetadataRoute.Sitemap; |
| 14 | |
| 15 | // Dynamic routes (e.g., blog posts from CMS) |
| 16 | const posts = await getBlogPosts(); // Fetch from CMS |
| 17 | |
| 18 | const dynamicRoutes = posts.map((post) => ({ |
| 19 | url: `${baseUrl}/blog/${post.slug}`, |
| 20 | lastModified: new Date(post.updatedAt), |
| 21 | changeFrequency: 'weekly' as const, |
| 22 | priority: 0.6, |
| 23 | })); |
| 24 | |
| 25 | return [...staticRoutes, ...dynamicRoutes]; |
| 26 | } |
| 27 | |
| 28 | // robots.txt — app/robots.ts |
| 29 | import { MetadataRoute } from 'next'; |
| 30 | |
| 31 | export default function robots(): MetadataRoute.Robots { |
| 32 | return { |
| 33 | rules: [ |
| 34 | { |
| 35 | userAgent: '*', |
| 36 | allow: '/', |
| 37 | disallow: ['/api/', '/admin/', '/_next/'], |
| 38 | }, |
| 39 | { |
| 40 | userAgent: 'GPTBot', |
| 41 | disallow: '/', |
| 42 | }, |
| 43 | ], |
| 44 | sitemap: 'https://example.com/sitemap.xml', |
| 45 | }; |
| 46 | } |
info
Structured data helps search engines understand your content and display rich results (rich snippets, carousels, knowledge panels) in search results.
| 1 | // JSON-LD structured data in React/Next.js |
| 2 | import { JsonLd } from 'react-schemaorg'; |
| 3 | |
| 4 | // Article schema |
| 5 | function ArticleSchema({ post }) { |
| 6 | const jsonLd = { |
| 7 | '@context': 'https://schema.org', |
| 8 | '@type': 'Article', |
| 9 | headline: post.title, |
| 10 | description: post.excerpt, |
| 11 | image: post.coverImage, |
| 12 | datePublished: post.publishedAt, |
| 13 | dateModified: post.updatedAt, |
| 14 | author: { |
| 15 | '@type': 'Person', |
| 16 | name: post.author.name, |
| 17 | }, |
| 18 | publisher: { |
| 19 | '@type': 'Organization', |
| 20 | name: 'My Company', |
| 21 | logo: { |
| 22 | '@type': 'ImageObject', |
| 23 | url: 'https://example.com/logo.png', |
| 24 | }, |
| 25 | }, |
| 26 | mainEntityOfPage: { |
| 27 | '@type': 'WebPage', |
| 28 | '@id': `https://example.com/blog/${post.slug}`, |
| 29 | }, |
| 30 | }; |
| 31 | |
| 32 | return ( |
| 33 | <script |
| 34 | type="application/ld+json" |
| 35 | dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} |
| 36 | /> |
| 37 | ); |
| 38 | } |
| 39 | |
| 40 | // BreadcrumbList schema |
| 41 | function BreadcrumbSchema({ items }) { |
| 42 | const jsonLd = { |
| 43 | '@context': 'https://schema.org', |
| 44 | '@type': 'BreadcrumbList', |
| 45 | itemListElement: items.map((item, index) => ({ |
| 46 | '@type': 'ListItem', |
| 47 | position: index + 1, |
| 48 | name: item.name, |
| 49 | item: item.url, |
| 50 | })), |
| 51 | }; |
| 52 | |
| 53 | return ( |
| 54 | <script |
| 55 | type="application/ld+json" |
| 56 | dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} |
| 57 | /> |
| 58 | ); |
| 59 | } |
| 60 | |
| 61 | // Organization schema (for site-wide use) |
| 62 | const organizationSchema = { |
| 63 | '@context': 'https://schema.org', |
| 64 | '@type': 'Organization', |
| 65 | name: 'My Company', |
| 66 | url: 'https://example.com', |
| 67 | logo: 'https://example.com/logo.png', |
| 68 | sameAs: [ |
| 69 | 'https://twitter.com/mycompany', |
| 70 | 'https://linkedin.com/company/mycompany', |
| 71 | 'https://github.com/mycompany', |
| 72 | ], |
| 73 | contactPoint: { |
| 74 | '@type': 'ContactPoint', |
| 75 | telephone: '+1-555-555-5555', |
| 76 | contactType: 'customer service', |
| 77 | }, |
| 78 | }; |
Analytics tools track user behavior, page views, conversion rates, and acquisition channels. Choose a solution that balances data depth with privacy compliance.
| Tool | Type | Privacy | Best For |
|---|---|---|---|
| Google Analytics 4 | Event-based, free | GDPR consent required | Comprehensive analysis, ad integration |
| Plausible | Simple, cookie-less | GDPR-compliant by design | Privacy-focused, simple dashboard |
| Umami | Self-hosted, open source | Full data control | Self-hosted, no external dependency |
| Fathom | Simple, cookie-less | GDPR-compliant by design | Paid, but simple and reliable |
| PostHog | Product analytics, self-hosted | Self-hosted, full control | Product analytics, feature flags, heatmaps |
| 1 | // Google Analytics 4 — Next.js integration |
| 2 | // app/layout.tsx or app/components/GoogleAnalytics.tsx |
| 3 | import Script from 'next/script'; |
| 4 | |
| 5 | export default function GoogleAnalytics({ gaId }: { gaId: string }) { |
| 6 | return ( |
| 7 | <> |
| 8 | <Script |
| 9 | src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`} |
| 10 | strategy="afterInteractive" |
| 11 | /> |
| 12 | <Script id="google-analytics" strategy="afterInteractive"> |
| 13 | {} |
| 14 | window.dataLayer = window.dataLayer || []; |
| 15 | function gtag(){dataLayer.push(arguments);} |
| 16 | gtag('js', new Date()); |
| 17 | gtag('config', '{gaId}', { |
| 18 | page_path: window.location.pathname, |
| 19 | }); |
| 20 | {} |
| 21 | </Script> |
| 22 | </> |
| 23 | ); |
| 24 | } |
| 25 | |
| 26 | // Plausible — Lightweight, privacy-first |
| 27 | // app/layout.tsx |
| 28 | import Script from 'next/script'; |
| 29 | |
| 30 | export default function PlausibleAnalytics({ domain }: { domain: string }) { |
| 31 | return ( |
| 32 | <Script |
| 33 | defer |
| 34 | data-domain={domain} |
| 35 | src="https://plausible.io/js/script.js" |
| 36 | strategy="afterInteractive" |
| 37 | /> |
| 38 | ); |
| 39 | } |
| 40 | |
| 41 | // Custom event tracking |
| 42 | export function trackEvent(name: string, props?: Record<string, string>) { |
| 43 | if (typeof window !== 'undefined' && 'gtag' in window) { |
| 44 | gtag('event', name, props); |
| 45 | } |
| 46 | |
| 47 | if (typeof window !== 'undefined' && 'plausible' in window) { |
| 48 | plausible(name, { props }); |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | // Page view tracking for Next.js App Router |
| 53 | // Use useEffect or Route Change Events for page tracking |
| 54 | 'use client'; |
| 55 | import { usePathname, useSearchParams } from 'next/navigation'; |
| 56 | import { useEffect } from 'react'; |
| 57 | |
| 58 | export function usePageTracking() { |
| 59 | const pathname = usePathname(); |
| 60 | const searchParams = useSearchParams(); |
| 61 | |
| 62 | useEffect(() => { |
| 63 | const url = pathname + (searchParams?.toString() ? `?${searchParams.toString()}` : ''); |
| 64 | trackEvent('page_view', { page_path: url }); |
| 65 | }, [pathname, searchParams]); |
| 66 | } |
A/B testing compares two or more versions of a page to determine which performs better on a specific metric. Implement via feature flags, middleware, or dedicated tools.
| 1 | // A/B testing with Vercel Edge Middleware |
| 2 | // middleware.ts |
| 3 | import { NextResponse } from 'next/server'; |
| 4 | import type { NextRequest } from 'next/server'; |
| 5 | |
| 6 | export function middleware(request: NextRequest) { |
| 7 | const url = request.nextUrl; |
| 8 | |
| 9 | // Only run experiments on certain paths |
| 10 | if (!url.pathname.startsWith('/pricing')) { |
| 11 | return NextResponse.next(); |
| 12 | } |
| 13 | |
| 14 | // Check for existing variant in cookie |
| 15 | const variant = request.cookies.get('pricing-experiment'); |
| 16 | |
| 17 | if (!variant) { |
| 18 | // Assign variant: 50/50 split |
| 19 | const assigned = Math.random() < 0.5 ? 'control' : 'variant'; |
| 20 | const response = NextResponse.next(); |
| 21 | response.cookies.set('pricing-experiment', assigned, { |
| 22 | maxAge: 60 * 60 * 24 * 30, // 30 days |
| 23 | path: '/', |
| 24 | }); |
| 25 | return response; |
| 26 | } |
| 27 | |
| 28 | return NextResponse.next(); |
| 29 | } |
| 30 | |
| 31 | // Client-side A/B testing with Google Optimize (legacy) |
| 32 | // Or use GrowthBook (open source feature flags + experiments) |
| 33 | import { GrowthBook, GrowthBookProvider } from '@growthbook/growthbook-react'; |
| 34 | |
| 35 | const gb = new GrowthBook({ |
| 36 | apiHost: 'https://cdn.growthbook.io', |
| 37 | clientKey: 'sdk-xxx', |
| 38 | enableDevMode: true, |
| 39 | }); |
| 40 | |
| 41 | // Evaluate an experiment |
| 42 | const { value } = gb.evaluateFeature('new-pricing-page'); |
| 43 | |
| 44 | if (value) { |
| 45 | return <NewPricingPage />; |
| 46 | } |
| 47 | return <LegacyPricingPage />; |