|$ curl https://forge-ai.dev/api/markdown?path=docs/ops/seo
$cat docs/seo-&-analytics.md
updated Recently·30 min read·published

SEO & Analytics

OperationsSEOIntermediate
Introduction

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

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.

FeatureWhat It ShowsAction
PerformanceQueries, clicks, impressions, CTR, positionIdentify high-opportunity keywords
Index CoveragePages indexed vs. excluded with errorsFix indexing errors, submit sitemaps
Core Web VitalsURL-level LCP, CLS, INP from real usersIdentify and fix slow pages
Mobile UsabilityMobile-specific rendering issuesFix viewport, tap targets, font issues
LinksInternal and external linkingImprove internal link structure
search-console.sh
Bash
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:
6google-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
Sitemap.xml Generation

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.

app/sitemap.ts
TypeScript
1// app/sitemap.ts — Next.js sitemap generation
2import { MetadataRoute } from 'next';
3
4export 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
29import { MetadataRoute } from 'next';
30
31export 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

Set changeFrequency accurately: daily for blog homepage, weekly for articles, monthly for about/contact pages, yearly for evergreen content. Do not claim always — search engines may penalize for misleading metadata.
Structured Data (JSON-LD / Schema.org)

Structured data helps search engines understand your content and display rich results (rich snippets, carousels, knowledge panels) in search results.

structured-data.tsx
TypeScript
1// JSON-LD structured data in React/Next.js
2import { JsonLd } from 'react-schemaorg';
3
4// Article schema
5function 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
41function 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)
62const 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 Setup

Analytics tools track user behavior, page views, conversion rates, and acquisition channels. Choose a solution that balances data depth with privacy compliance.

ToolTypePrivacyBest For
Google Analytics 4Event-based, freeGDPR consent requiredComprehensive analysis, ad integration
PlausibleSimple, cookie-lessGDPR-compliant by designPrivacy-focused, simple dashboard
UmamiSelf-hosted, open sourceFull data controlSelf-hosted, no external dependency
FathomSimple, cookie-lessGDPR-compliant by designPaid, but simple and reliable
PostHogProduct analytics, self-hostedSelf-hosted, full controlProduct analytics, feature flags, heatmaps
analytics.tsx
TypeScript
1// Google Analytics 4 — Next.js integration
2// app/layout.tsx or app/components/GoogleAnalytics.tsx
3import Script from 'next/script';
4
5export 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
28import Script from 'next/script';
29
30export 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
42export 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';
55import { usePathname, useSearchParams } from 'next/navigation';
56import { useEffect } from 'react';
57
58export 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}
Meta Tags Optimization

Meta tags control how your pages appear in search results and on social media. Next.js provides a generateMetadata API for dynamic meta tag generation.

app/blog/[slug]/page.tsx
TypeScript
1// app/blog/[slug]/page.tsx — Dynamic metadata
2import type { Metadata } from 'next';
3
4export async function generateMetadata({ params }): Promise<Metadata> {
5 const post = await getPost(params.slug);
6
7 return {
8 title: `${post.title} | My Blog`,
9 description: post.excerpt,
10 openGraph: {
11 title: post.title,
12 description: post.excerpt,
13 url: `https://example.com/blog/${post.slug}`,
14 siteName: 'My Blog',
15 images: [
16 {
17 url: post.coverImage,
18 width: 1200,
19 height: 630,
20 alt: post.title,
21 },
22 ],
23 locale: 'en_US',
24 type: 'article',
25 publishedTime: post.publishedAt,
26 authors: [post.author.name],
27 },
28 twitter: {
29 card: 'summary_large_image',
30 title: post.title,
31 description: post.excerpt,
32 images: [post.coverImage],
33 },
34 robots: {
35 index: true,
36 follow: true,
37 googleBot: {
38 index: true,
39 follow: true,
40 'max-video-preview': -1,
41 'max-image-preview': 'large',
42 'max-snippet': -1,
43 },
44 },
45 alternates: {
46 canonical: `https://example.com/blog/${post.slug}`,
47 },
48 };
49}
50
51// Title guidelines:
52// Home: "Site Name — Tagline" (50-60 chars)
53// Page: "Page Title | Site Name" (50-60 chars)
54// Blog: "Post Title | Blog Name" (50-60 chars)
55
56// Description guidelines:
57// 150-160 characters, compelling summary
58// Include target keyword naturally
59// No quotes or special characters
A/B Testing Frameworks

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.

ab-testing.ts
TypeScript
1// A/B testing with Vercel Edge Middleware
2// middleware.ts
3import { NextResponse } from 'next/server';
4import type { NextRequest } from 'next/server';
5
6export 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)
33import { GrowthBook, GrowthBookProvider } from '@growthbook/growthbook-react';
34
35const gb = new GrowthBook({
36 apiHost: 'https://cdn.growthbook.io',
37 clientKey: 'sdk-xxx',
38 enableDevMode: true,
39});
40
41// Evaluate an experiment
42const { value } = gb.evaluateFeature('new-pricing-page');
43
44if (value) {
45 return <NewPricingPage />;
46}
47return <LegacyPricingPage />;
$Blueprint — Engineering Documentation·Section ID: OPS-SEO-01·Revision: 1.0