SEO Unpacked 0/12

Frontend engineeringSearch, speed and analytics12 modules, launch ready

SEO
Unpacked

Everything a frontend developer controls in search: how Googlebot crawls and renders your pages, the tags that shape your snippet, structured data, clean URLs, Core Web Vitals, PageSpeed Insights, and measuring it all with Google Analytics 4 and Search Console.

Google SearchGA4Lighthouse

Four pillars

Every frontend SEO task makes a page easier to find, easier to understand, faster, or easier to measure. Start with the pillar that is weakest.

DiscoverableBotSiteBot → Site, then HTML back

Google can find, fetch and render every page you want indexed.

UnderstandableGooglePagePage → Google, signal by signal

Your markup tells Google and social apps what each page is.

FastUserPageUser ⇄ Page, every tap

Real users get a quick, stable, responsive page.

MeasuredPagegtagGA4GSCPage → gtag → GA4 and GSC

You can see traffic, queries and conversions, and fix what is broken.

01

How search works

Google crawls a URL, renders it like a browser, indexes what it understood, and ranks it for queries. Every other module improves one of those steps.

crawlrenderindexFoundation
In one lineCrawl, render, index, rank
In simple words

Think of a librarian. First they fetch a copy of your book (crawl), read it properly with all the pictures (render), file it on the right shelf with a summary card (index), and later hand it to readers who ask about that topic (rank).

Under the hood

Googlebot fetches the HTML and queues it for the Web Rendering Service, an evergreen headless Chromium that runs your JavaScript, sometimes seconds or days later. Links found in the rendered DOM join the crawl queue; content, canonical, structured data and signals are indexed; ranking happens at query time. Pages need a 200 status, no noindex, crawlable a href links and content present in the rendered DOM. Google indexes the mobile version first.

Pick it forUnderstanding why a page is missing from Google before you change anything else.

Crawler
Googlebot Smartphone, mostly
Renderer
Evergreen Chromium (WRS)
Index
Mobile first
Needs
200, no noindex, real links

How a conversation looks

From URL to search result in four steps
Googlebot
Your server
Renderer
Index
GET /blog/seo-guide
200 · HTML
queue for rendering
fetch JS, CSS, API data
assets
rendered DOM, links, JSON-LD
ranked at query time

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
Raw HTML checkcurl -sA "…Googlebot…" URLShows what arrives before any JavaScript runsDebugging a page missing from Google
URL InspectionSearch Console, Test live URLShows Google's own rendered HTML and screenshotConfirming content and links survive rendering
site: searchsite:example.com/blogA rough view of what is indexedQuick spot checks, never exact counts
Crawlable links<a href="/pricing">Pricing</a>Google discovers pages through href linksEvery navigation element

Try it

as-googlebot.shBASH
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
curl -sA "$UA" https://example.com/blog/seo-guide | grep -o '<title>.*</title>'
curl -sI https://example.com/blog/seo-guide | grep -iE '^HTTP|x-robots-tag'
curl -s https://example.com/blog/seo-guide | grep -c '<a href='

Look at the raw HTML first. If the title and links are only added by JavaScript, they exist for Google only after rendering, which can lag. Server rendered HTML removes that wait.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
curl -sA "$UA" https://example.com/blog/seo-guide | grep -o '<title>.*</title>'
<title>SEO for Frontend Developers | Example</title>
curl -sI https://example.com/blog/seo-guide | grep -iE '^HTTP|x-robots-tag'
HTTP/2 200
curl -s https://example.com/blog/seo-guide | grep -c '<a href='
48

Key terms

TermSimple meaning
CrawlGoogle downloads the page
RenderGoogle runs the JavaScript and builds the final page
IndexGoogle stores what the page is about
Crawl budgetHow many URLs Google will fetch from your site; matters on huge sites
Mobile first indexingGoogle judges the mobile version of your page
02

Title and meta tags

The handful of tags in <head> that decide your blue link, your snippet, and whether the page is indexed at all.

<title>descriptionrobotsUnderstandable
In one lineThe page's label
In simple words

The title is the name on the book's spine and the meta description is the blurb on the back. Google usually shows them as the clickable link and the two lines under it, so write them for people, not for robots.

Under the hood

<title> is a strong relevance signal and usually the result headline; keep it unique per page, front load the topic and aim for roughly 50 to 60 characters before truncation. The meta description is not a ranking factor but shapes clicks; Google may rewrite it from page text, so keep it accurate, around 150 to 160 characters. meta robots and the X-Robots-Tag header control noindex, nofollow and snippet limits. meta keywords is ignored by Google. lang, charset and viewport are basics every page needs.

Pick it forEvery indexable page, unique per URL.

Title
≈ 50 to 60 characters, unique
Description
≈ 150 to 160 characters
Robots
index, follow by default
Keywords meta
Ignored by Google

How a conversation looks

Your head tags become your search result
Searcher
Google
Your page
crawl the <head>
title, description, canonical, robots
query: frontend seo cheatsheet
link = <title> · snippet = description
click

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
<title><title>Topic: Detail | Brand</title>Main relevance signal and usually the result headlineEvery page, unique
meta description<meta name="description" content="…">Shapes the snippet and the click through rateEvery indexable page
meta robots<meta name="robots" content="noindex, follow">Keeps a page out of the index while following its linksThank you pages, internal search, staging
X-Robots-TagX-Robots-Tag: noindexRobots rules for files that have no <head>PDFs, images, API responses
Snippet controlsmax-snippet:160, max-image-preview:largeControls how much Google may showNews, publishers, image heavy pages
meta keywords<meta name="keywords" content="…">Ignored by Google; harmless but unhelpfulOnly if an internal search tool or another engine needs it

Try it

head.htmlHTML
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>SEO for Frontend Developers: Cheatsheet | Example</title>
  <meta name="description" content="Meta tags, JSON-LD, Core Web Vitals, GA4 and PageSpeed Insights for frontend developers, with copy and paste examples.">
  <link rel="canonical" href="https://example.com/blog/seo-guide">
  <meta name="robots" content="index, follow, max-image-preview:large">
  <meta name="keywords" content="frontend seo, meta tags, core web vitals"> <!-- ignored by Google -->
  <meta name="theme-color" content="#FF4D6D">
  <link rel="icon" href="/favicon.svg" type="image/svg+xml">
</head>

Write the title for the searcher. Lead with what the page answers, then the brand. Stuffing keywords makes Google rewrite the title and lowers clicks.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
curl -s https://example.com/blog/seo-guide | grep -oE '<title>.*</title>|name="(description|robots)" content="[^"]*"'
<title>SEO for Frontend Developers: Cheatsheet | Example</title>
name="description" content="Meta tags, JSON-LD, Core Web Vitals, GA4 and PageSpeed Insights for frontend developers, with copy and paste examples."
name="robots" content="index, follow, max-image-preview:large"

Key terms

TermSimple meaning
Title tagThe page's name in tabs and search results
Meta descriptionThe summary that may appear under the link
noindexKeep this page out of search results
nofollowDo not pass trust through these links
SnippetThe text Google shows for a result
X-Robots-TagRobots rules sent as an HTTP header, for PDFs and images
03

Open Graph and social cards

Tags that turn a pasted link into a rich preview on LinkedIn, Slack, WhatsApp, X and Facebook.

og:titleog:imagetwitter:cardUnderstandable
In one lineLink previews
In simple words

When someone pastes your link into a chat, the app sends a little robot to read your page and build a card: a title, a short line and a picture. Open Graph tags tell that robot exactly what to show.

Under the hood

Open Graph (ogp.me) uses meta property tags: og:title, og:description, og:image, og:url, og:type and og:site_name. X reads twitter:card (summary_large_image) and falls back to OG for the rest. Preview bots such as facebookexternalhit, Slackbot, LinkedInBot and WhatsApp do not run JavaScript, so tags must be in the server HTML. Use an absolute HTTPS image URL, ideally 1200 by 630, under about 5 MB, with og:image:alt. Platforms cache previews; re-scrape with their debug tools after changes.

Pick it forAny page people will share: posts, products, landing pages.

Image
1200 × 630, absolute HTTPS URL
X card
summary_large_image
JavaScript
Not run by preview bots
Cache
Refresh in each platform's debugger

How a conversation looks

One request from a bot builds the card everyone sees
Person
Chat app bot
Your page
pastes https://example.com/blog/seo-guide
GET as Slackbot or LinkedInBot
og:title · og:description · og:image
no JavaScript runs here
rich preview card

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
og:title + og:descriptionproperty="og:title"Text on the card, can differ from the SEO titleEvery shareable page
og:image1200 × 630 absolute URLThe image drives most of the clicksEvery shareable page, per page
twitter:cardsummary_large_imageLarge image cards on XPages shared on X
Dynamic OG images@vercel/og ImageResponse or a build scriptA title in every image without manual designBlogs and catalogues with many pages
Re-scrapeLinkedIn Post Inspector, Facebook Sharing DebuggerPlatforms cache the old cardAfter changing tags or images

Try it

social.htmlHTML
<meta property="og:type" content="article">
<meta property="og:site_name" content="Example">
<meta property="og:title" content="SEO for Frontend Developers: Cheatsheet">
<meta property="og:description" content="Meta tags, JSON-LD, Core Web Vitals and GA4 in one place.">
<meta property="og:url" content="https://example.com/blog/seo-guide">
<meta property="og:image" content="https://example.com/og/seo-guide.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="SEO Unpacked cheatsheet cover">
<meta name="twitter:card" content="summary_large_image">

Generate OG images per page. A title rendered into each image, for example with @vercel/og or a build script, gets far more clicks than one shared logo.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
curl -sA "Slackbot-LinkExpanding 1.0" https://example.com/blog/seo-guide | grep -oE 'property="og:(title|image)" content="[^"]*"'
property="og:title" content="SEO for Frontend Developers: Cheatsheet"
property="og:image" content="https://example.com/og/seo-guide.png"
curl -sI https://example.com/og/seo-guide.png | grep -iE '^HTTP|content-type|content-length'
HTTP/2 200
content-type: image/png
content-length: 184320

Key terms

TermSimple meaning
Open GraphA tag standard for link previews
og:imageThe picture on the card
twitter:cardWhich card layout X should use
ScraperThe bot that reads your tags
Preview cachePlatforms keep the old card until you refresh it
04

Structured data with JSON-LD

A block of JSON that states facts about the page in schema.org vocabulary, making it eligible for rich results.

JSON-LDschema.orgrich resultsUnderstandable
In one lineMachine readable facts
In simple words

Your page says it in words; JSON-LD says it as a filled in form: this is an article, this is the author, this is the date, this is the price. Google reads the form and may show stars, prices or breadcrumbs in the result.

Under the hood

Add <script type="application/ld+json"> anywhere in the HTML, server rendered or injected by JavaScript. Use schema.org types Google supports (Article, Product with Offer and AggregateRating, BreadcrumbList, Organization, LocalBusiness, Event, VideoObject, JobPosting) with their required properties, and only mark up content visible on the page. Valid markup makes a page eligible, not guaranteed. FAQ rich results are limited to well known authoritative sites and HowTo rich results were retired, so do not rely on them. Validate with the Rich Results Test and watch Search Console enhancement reports.

Pick it forArticles, products, recipes, events, jobs, local businesses and site breadcrumbs.

Format
JSON-LD, recommended
Vocabulary
schema.org
Placement
Any <script> in the page
Validate
Rich Results Test

How a conversation looks

Valid markup earns eligibility for richer results
Your page
Googlebot
Results page
fetch and render
JSON-LD: Article, BreadcrumbList
check required fields and visible content
eligible: breadcrumb trail, article details

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
Article"@type": "Article"Article details such as headline, image and dateBlog posts, news
Product + Offer"offers": { "price": "2499", "priceCurrency": "INR" }Price, stock and ratings in resultsProduct pages
BreadcrumbList"itemListElement": [ListItem…]A readable path instead of a raw URLAny site with hierarchy
Organization"logo", "sameAs": ["https://www.linkedin.com/company/…"]Clarifies your brand, logo and profilesHome or about page
Rich Results Testsearch.google.com/test/rich-resultsShows errors and which features are eligibleBefore deploying markup
FAQPage"@type": "FAQPage"Rich results now limited to authoritative government and health sitesRarely; do not expect the dropdowns

Try it

jsonld.htmlHTML
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "SEO for Frontend Developers: Cheatsheet",
  "image": ["https://example.com/og/seo-guide.png"],
  "datePublished": "2026-10-05",
  "dateModified": "2026-10-05",
  "author": [{ "@type": "Person", "name": "Shree Kumar Sharma", "url": "https://example.com/about" }],
  "publisher": { "@type": "Organization", "name": "Example", "logo": "https://example.com/logo.png" }
}
</script>

Mark up only what users can see. Prices, ratings or authors that appear only in JSON-LD break Google's guidelines and can lead to a manual action.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
curl -s https://example.com/blog/seo-guide | grep -oE '"@type": ?"[A-Za-z]+"' | sort | uniq -c
      1 "@type": "Article"
      1 "@type": "BreadcrumbList"
      3 "@type": "ListItem"
      1 "@type": "Organization"
      1 "@type": "Person"

Key terms

TermSimple meaning
JSON-LDStructured data written as JSON in a script tag
schema.orgThe shared dictionary of types and properties
Rich resultA search result with extras such as stars or prices
Required propertyA field Google needs before the feature can show
Manual actionA penalty applied by a human reviewer at Google
05

Semantic HTML and accessibility

Real headings, landmarks, links and alt text help Google understand the page and help every user use it. Good SEO and good accessibility are mostly the same work.

<h1><main>altUnderstandable
In one lineStructure that machines can read
In simple words

Headings are the table of contents, links are the roads between pages, and alt text describes pictures for anyone who cannot see them. Crawlers and screen readers both rely on these to make sense of the page.

Under the hood

Use one descriptive <h1> and a logical h2 to h6 outline; landmarks (header, nav, main, article, aside, footer) frame the content. Google follows <a href> links only, not click handlers on divs, and uses anchor text as a signal, so avoid "click here". Images need meaningful alt text, explicit width and height (also prevents layout shift), and modern formats with srcset. Use <button> for actions and <a> for navigation. Lighthouse SEO and accessibility audits plus axe catch most mistakes.

Pick it forEvery template: blog posts, product pages, docs, landing pages.

Headings
One h1, then a logical outline
Links
<a href> with clear text
Images
alt, width, height, srcset
Check with
Lighthouse, axe

How a conversation looks

One structure serves search engines and assistive technology
Crawler
Your DOM
Screen reader
read <h1>, <main>, <a href>
topic, outline, link graph
jump between headings and landmarks
the same outline, spoken

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
One h1<h1>Main topic</h1>States the page's subject clearlyEvery page
Landmarks<header> <nav> <main> <footer>Structure for bots and assistive techEvery template
Descriptive anchors<a href="/pricing">See pricing</a>Anchor text tells Google about the targetAll internal links
Responsive imagessrcset, sizes, width, height, altRight size per screen, no layout shift, indexableEvery content image
Lazy loadingloading="lazy" below the fold onlySaves bandwidth without delaying LCPImages and iframes lower on the page

Try it

article.htmlHTML
<header><nav aria-label="Main"><a href="/blog">Blog</a></nav></header>
<main>
  <article>
    <h1>SEO for Frontend Developers</h1>
    <p>Published <time datetime="2026-10-05">5 October 2026</time></p>
    <h2>Core Web Vitals</h2>
    <img src="/img/lcp.avif" alt="Waterfall chart with the hero image loading first"
         width="1200" height="675" loading="lazy" decoding="async">
    <p>Read the <a href="/blog/core-web-vitals">Core Web Vitals guide</a> next.</p>
  </article>
</main>
<footer>…</footer>

Links must be real links. A div with an onClick that calls router.push is invisible to crawlers. Frameworks' Link components render , so use them.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
curl -s https://example.com/blog/seo-guide | grep -oE '<h[1-3][^>]*>[^<]+'
<h1>SEO for Frontend Developers
<h2>Core Web Vitals
<h2>Structured data
<h3>Article markup
curl -s https://example.com/blog/seo-guide | grep -c '<img [^>]*alt=""'
0

Key terms

TermSimple meaning
LandmarkA labelled region such as nav or main
Heading outlineThe page's table of contents
Anchor textThe clickable words of a link
Alt textA text version of an image
Internal linksLinks between your own pages, which spread importance
06

URLs, canonicals and redirects

One clean, permanent URL per piece of content, with every duplicate pointing at it.

canonical301hreflangDiscoverable
In one lineOne page, one address
In simple words

If the same page lives at five addresses, Google has to guess which is the real one and splits the credit between them. A canonical tag says "this is the original", and a redirect forwards visitors from an old address to the new one.

Under the hood

Choose one version (https, one host, consistent trailing slash, lowercase) and 301 or 308 everything else to it in a single hop. rel=canonical is a strong hint for duplicates such as tracking parameters, sort orders and syndicated copies; make it absolute and self referencing on the original. Avoid redirect chains and 302s for permanent moves. Keep URLs short, readable and hyphenated. For languages or regions, link alternates with hreflang, reciprocal on every version, plus x-default. Hash fragments are not separate URLs to Google.

Pick it forMigrations, filters and query parameters, multi language sites, and any duplicate content.

Permanent move
301 or 308, one hop
Duplicates
rel=canonical, absolute URL
Languages
hreflang + x-default
Style
lowercase-with-hyphens

How a conversation looks

Redirect once, then a self referencing canonical
Browser or bot
Old URL
New URL
GET /Blog/SEO-Guide?utm_source=x
301 · Location: /blog/seo-guide
GET /blog/seo-guide
200 · canonical = itself
links and signals consolidate here

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
301 / 308return 301 /new-path;Moves the old URL's signals to the new oneMigrations, renamed slugs, https and host
Self canonical<link rel="canonical" href="https://example.com/p">Guards against parameter duplicatesEvery indexable page
Cross canonicalcanonical → original article URLCredits the source of syndicated contentRepublished posts on Medium or partners
hreflang<link rel="alternate" hreflang="hi-IN" href="…">Sends each user to their language versionMulti language or multi region sites
Clean slugs/blog/frontend-seo-cheatsheetReadable, stable and shareableEvery new route

Try it

redirects.confNGINX
# force https and the bare domain in one hop
server {
  listen 80;
  server_name example.com www.example.com;
  return 301 https://example.com$request_uri;
}

server {
  listen 443 ssl;
  server_name example.com;
  # a moved page
  location = /blog/old-seo-post { return 301 /blog/seo-guide; }
  # drop the trailing slash on everything except /
  rewrite ^/(.+)/$ /$1 permanent;
}

Canonical is a hint, redirects are a rule. Use a redirect when the old URL should never be seen again, and a canonical when both URLs must keep working.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
curl -sIL http://www.example.com/blog/old-seo-post | grep -iE '^HTTP|^location'
HTTP/1.1 301 Moved Permanently
Location: https://example.com/blog/old-seo-post
HTTP/2 301
location: /blog/seo-guide
HTTP/2 200
# two hops: fold the http and old path rules into one redirect where you can

Key terms

TermSimple meaning
CanonicalThe preferred URL among duplicates
301 / 308Moved permanently; passes signals
302 / 307Moved temporarily; keep the old URL indexed
Redirect chainSeveral hops before the final page; slow and leaky
hreflangWhich language or region each version is for
07

robots.txt and sitemap.xml

robots.txt says where crawlers may go; the sitemap lists the URLs you want found. Neither one keeps a page out of search on its own.

robots.txtsitemap.xmllastmodDiscoverable
In one lineCrawl rules and a URL list
In simple words

robots.txt is the sign on the door saying which rooms visitors may enter. The sitemap is the floor plan listing every room worth visiting, with the date each was last redecorated.

Under the hood

robots.txt lives at the root of each host and protocol and controls crawling, not indexing: a blocked URL can still be indexed from links, without content. To keep a page out, allow crawling and serve noindex. Never block CSS or JS the renderer needs. Sitemaps are XML (up to 50,000 URLs or 50 MB uncompressed per file, combined with a sitemap index) listing canonical, indexable 200 URLs with accurate lastmod; Google ignores priority and changefreq. Reference the sitemap in robots.txt and submit it in Search Console.

Pick it forEvery site. Generate both at build time so they never drift from your routes.

Location
/robots.txt at the host root
Controls
Crawling, not indexing
Sitemap limit
50,000 URLs or 50 MB per file
Useful field
lastmod; priority is ignored

How a conversation looks

Rules first, then the list, then the pages
Googlebot
Your server
GET /robots.txt
Disallow: /admin · Sitemap: …
GET /sitemap.xml
120 URLs with lastmod
GET only allowed, changed pages

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
DisallowDisallow: /admin/Saves crawl budget on useless pathsAdmin, cart, infinite filters
AllowAllow: /api/og/Opens a path inside a disallowed folderExceptions such as OG image endpoints
Sitemap lineSitemap: https://example.com/sitemap.xmlEvery crawler finds your listEvery robots.txt
Sitemap index<sitemapindex> of sitemap-1.xml…Splits over 50,000 URLsLarge sites
Generated at buildNext.js app/sitemap.ts, app/robots.tsNever out of sync with routesFramework sites

Try it

robots.txt + sitemap.xmlTXT
# robots.txt
User-agent: *
Disallow: /admin/
Disallow: /cart
Disallow: /*?sort=

Sitemap: https://example.com/sitemap.xml

<!-- sitemap.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/blog/seo-guide</loc><lastmod>2026-10-05</lastmod></url>
  <url><loc>https://example.com/blog/core-web-vitals</loc><lastmod>2026-09-28</lastmod></url>
</urlset>

Disallow does not mean deindex. To remove a page from Google, let it be crawled and add noindex. Blocking it in robots.txt hides the noindex tag from Google.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
curl -s https://example.com/robots.txt
User-agent: *
Disallow: /admin/
Disallow: /cart
Disallow: /*?sort=
Sitemap: https://example.com/sitemap.xml
curl -s https://example.com/sitemap.xml | grep -c '<loc>'
120

Key terms

TermSimple meaning
User-agentWhich crawler a group of rules is for
DisallowDo not crawl paths that start with this
Sitemap indexA sitemap that lists other sitemaps
lastmodWhen the page content last really changed
Orphan pageA page no internal link points to; sitemaps help, links help more
08

Rendering: CSR, SSR, SSG

Where your HTML is built decides what crawlers see first and how fast users see content.

SSRSSGISRDiscoverable
In one lineWhere the HTML is made
In simple words

With client side rendering the browser receives an empty box and a set of instructions to build the page. With server rendering or static generation it receives the finished page. Finished pages are faster to show and easier for every bot to read.

Under the hood

CSR ships a near empty HTML shell; content, title and links appear only after JavaScript runs, which Google can render with a delay and most social and AI crawlers cannot. SSR renders HTML per request; SSG renders at build time; ISR (or stale while revalidate) regenerates static pages on a schedule. Hydration then makes the page interactive. For SPAs, give each view its own URL via the History API, return real 404 status codes, and set metadata on the server. Next.js App Router exposes metadata and generateMetadata for this.

Pick it forSSG or ISR for content and marketing pages, SSR for personalised or fast changing pages, CSR only behind login.

Best for SEO
SSG, ISR, SSR
Risky
CSR for public content
Preview bots
Need server HTML
Next.js
metadata, generateMetadata

How a conversation looks

Same page, two very different first responses
Bot or browser
Server
API
CSR
GET /products/42
<div id="root"></div> + bundle.js
JS fetches /api/products/42
JSON, then the page appears
SSR or SSG
GET /products/42
full HTML with title, content, links

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
SSGgenerateStaticParams()Fastest HTML from a CDNDocs, blogs, marketing pages
ISRexport const revalidate = 3600Static speed with fresh dataCatalogues, listings
SSRexport const dynamic = "force-dynamic"Always current, per requestSearch results, personalised pages
CSRuseEffect(() => fetch(…))Least server workDashboards behind login
Metadata APIexport async function generateMetadata()Title, description, OG and canonical in server HTMLEvery Next.js route
Real status codesnotFound(), redirect()Avoids soft 404s and stale pagesMissing or moved content

Try it

app/products/[id]/page.tsxTSX
import type { Metadata } from "next";

export const revalidate = 3600; // ISR: rebuild at most once an hour

export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
  const { id } = await params;
  const p = await getProduct(id);
  return {
    title: `${p.name} | Example Store`,
    description: p.summary,
    alternates: { canonical: `/products/${id}` },
    openGraph: { images: [p.image] },
  };
}

export default async function Page({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const p = await getProduct(id);
  if (!p) notFound(); // a real 404, not a soft 404
  return <h1>{p.name}</h1>;
}

Avoid soft 404s. A missing product that renders "Not found" with status 200 wastes crawl budget and can stay indexed. Return a real 404.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
curl -s localhost:3000/products/42 | grep -oE '<title>.*</title>|<h1>[^<]+'
<title>Trail Runner 3 | Example Store</title>
<h1>Trail Runner 3
# the same route as a client rendered SPA
curl -s localhost:5173/products/42 | grep -oE '<title>.*</title>|<div id="root">[^<]*'
<title>Vite App</title>
<div id="root">
curl -s -o /dev/null -w "%{http_code}\n" localhost:3000/products/does-not-exist
404

Key terms

TermSimple meaning
CSRThe browser builds the page from JavaScript
SSRThe server builds HTML on each request
SSGHTML is built once, at deploy time
ISRStatic pages that rebuild on a timer
HydrationJavaScript attaches to server HTML to make it interactive
Soft 404An error page sent with status 200
09

Core Web Vitals

Three real user metrics Google uses as a page experience signal: loading (LCP), responsiveness (INP) and visual stability (CLS).

LCPINPCLSFast
In one lineSpeed as users feel it
In simple words

LCP asks how long until the main thing appears. INP asks how quickly the page reacts when you tap. CLS asks whether things jump around while you are reading. Pass all three and the page feels fast.

Under the hood

Measured at the 75th percentile of real Chrome users over 28 days (CrUX), split by mobile and desktop. LCP good is 2.5 s or less: preload or fetchpriority=high the hero image, serve AVIF or WebP, use a CDN and cut server time. INP (which replaced FID in March 2024) good is 200 ms or less: break up long tasks, yield with scheduler.yield or setTimeout, defer third party scripts. CLS good is 0.1 or less: set width and height or aspect-ratio on media, reserve space for ads and embeds, use font-display with matched fallbacks. They are a ranking signal, but relevance still dominates.

Pick it forEvery public page; mobile first, because that is where most pages fail.

LCP good
≤ 2.5 s
INP good
≤ 200 ms
CLS good
≤ 0.1
Measured at
p75 of real users, 28 days

How a conversation looks

Where each vital is measured during a visit
Browser
CDN
User
GET / (HTML)
HTML with preload for hero.avif
GET hero.avif · fetchpriority=high
image
LCP 1.9 s
taps "Add to cart"
next paint
INP 120 ms · CLS 0.02

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
Preload LCP image<link rel="preload" as="image" fetchpriority="high">The browser fetches the hero firstPages with a hero image
Modern images<picture> AVIF, WebP, srcsetFewer bytes, faster LCPEvery image
Size reservationswidth/height or aspect-ratioStops content jumping, lowers CLSImages, ads, embeds
Yield to the main threadawait scheduler.yield()Splits long tasks, lowers INPHeavy click handlers
Defer third parties<script defer> or PartytownLess blocking JavaScriptChat widgets, tag managers
Font loadingfont-display: swap + size-adjust fallbackText shows early without a big shiftCustom web fonts

Try it

vitals.jsJS
// <link rel="preload" as="image" href="/hero.avif" fetchpriority="high"> in <head>
import { onLCP, onINP, onCLS } from "web-vitals";

function send({ name, value, rating, id }) {
  gtag("event", name, {
    value: Math.round(name === "CLS" ? value * 1000 : value),
    metric_rating: rating,
    metric_id: id,
    non_interaction: true,
  });
}
onLCP(send);
onINP(send);
onCLS(send);

Field data beats lab data. Lighthouse runs once on a simulated phone. Sending web-vitals to GA4 shows what your real visitors experience, page by page.

DevTools consoleChrome
# a recorded session, replayed when you press Run
# DevTools console with the web-vitals library logging
LCP { value: 1890, rating: 'good', element: 'img.hero' }
CLS { value: 0.02, rating: 'good' }
INP { value: 120, rating: 'good', target: 'button.add-to-cart' }

Key terms

MetricGoodNeeds improvementPoor
LCP≤ 2.5 s2.5 to 4 s> 4 s
INP≤ 200 ms200 to 500 ms> 500 ms
CLS≤ 0.10.1 to 0.25> 0.25
TTFB (diagnostic)≤ 0.8 s0.8 to 1.8 s> 1.8 s
FCP (diagnostic)≤ 1.8 s1.8 to 3 s> 3 s
10

PageSpeed Insights and Lighthouse

One URL in, two answers out: real user field data from CrUX and a lab audit from Lighthouse, with a list of fixes.

PSILighthouseCrUXFast
In one lineField data plus a lab test
In simple words

PageSpeed Insights is a health check for one page. The top half says how real visitors experienced it over the last month. The bottom half is a test run on a simulated slow phone, with a to do list of what to fix.

Under the hood

PSI shows CrUX field data (p75 LCP, INP, CLS, plus FCP and TTFB) at URL or origin level when there is enough traffic, then a Lighthouse lab run with simulated throttling on a mid range mobile device. The Performance score (0 to 100) weights lab metrics such as LCP, TBT, CLS, FCP and Speed Index; TBT stands in for INP in the lab. Pass or fail for Core Web Vitals comes from field data only. Automate with the PageSpeed Insights API v5 (an API key raises quotas), the Lighthouse CLI, or Lighthouse CI on every pull request.

Pick it forBefore launch, after every big change, and continuously in CI.

Field data
CrUX, 28 days, p75
Lab data
Lighthouse, throttled mobile
Scores
Performance, Accessibility, Best Practices, SEO
Automate
PSI API v5, Lighthouse CI

How a conversation looks

One request combines lab and field results
You
PSI API
Lighthouse
CrUX
GET runPagespeed?url=…&strategy=mobile
lab run, simulated slow 4G
field data for this URL
scores, audits, opportunities
p75 LCP, INP, CLS
one JSON report

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
PSI webpagespeed.web.devQuick field and lab view for one URLSpot checks
PSI APIrunPagespeed?url=…&strategy=mobileScriptable results, JSONDashboards, scheduled checks
Lighthouse CLInpx lighthouse URL --form-factor=mobileLocal lab runs with full reportsDebugging before deploy
Lighthouse CIlhci autorun with budgetsFails a pull request that regressesEvery repository with a frontend
CrUX APIchromeuxreport.googleapis.comField data for any originComparing with competitors

Try it

psi.shBASH
URL="https://example.com/blog/seo-guide"
curl -s "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=$URL&strategy=mobile&category=performance&category=seo&key=$PSI_KEY" \
  | jq '{perf: .lighthouseResult.categories.performance.score,
         seo: .lighthouseResult.categories.seo.score,
         lcp_p75: .loadingExperience.metrics.LARGEST_CONTENTFUL_PAINT_MS.percentile,
         inp_p75: .loadingExperience.metrics.INTERACTION_TO_NEXT_PAINT.percentile,
         cwv: .loadingExperience.overall_category}'

npx lighthouse "$URL" --only-categories=performance,seo --form-factor=mobile \
  --output=html --output-path=./lighthouse.html --chrome-flags="--headless=new"

A 100 score is not the goal. Passing Core Web Vitals in the field is. Lab scores swing between runs; compare medians and fix the listed opportunities.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
bash psi.sh
{
  "perf": 0.91,
  "seo": 1,
  "lcp_p75": 2100,
  "inp_p75": 140,
  "cwv": "FAST"
}
Printer html output written to /home/shree/lighthouse.html

Key terms

Score rangeColourMeaning
90 to 100GreenGood
50 to 89OrangeNeeds improvement
0 to 49RedPoor
Field: FAST / AVERAGE / SLOWBannerCrUX overall Core Web Vitals verdict
11

Google Analytics 4

Install GA4 with your measurement ID, track the events that matter, respect consent, and turn key events into conversions.

G-XXXXXXXXXXgtag.jseventsMeasured
In one lineMeasure what visitors do
In simple words

GA4 is a visitor log for your site. A small script sends a note every time someone views a page or does something useful, like signing up, and GA4 turns those notes into reports about where people came from and what they did.

Under the hood

Create a property and a web data stream to get a measurement ID (G- followed by 10 characters); Google Tag Manager containers use GTM-XXXXXXX instead. gtag.js sends events to the collect endpoint; page_view and enhanced measurement (scrolls, outbound clicks, site search, file downloads) are automatic. Send custom events with gtag('event', name, params), mark the important ones as key events, register custom dimensions for params you want in reports, and use DebugView while testing. In the EEA and UK, Consent Mode v2 (ad_storage, analytics_storage, ad_user_data, ad_personalization) must default to denied until the user agrees. SPAs must send page_view on route changes if enhanced measurement history events are off.

Pick it forEvery production site; pair it with Search Console for search queries.

ID format
G-XXXXXXXXXX (GTM: GTM-XXXXXXX)
Auto events
page_view, scroll, click, search
Custom
gtag('event', …)
Privacy
Consent Mode v2

How a conversation looks

Consent first, then events flow to your property
Visitor's browser
gtag.js
GA4
load gtag/js?id=G-XXXXXXXXXX
consent default: denied
page_view (cookieless ping)
consent update: granted
gtag('event', 'sign_up')
sign_up · key event
204 No Content

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
gtag.jsgtag('config', 'G-XXXXXXXXXX')Direct and lightweightSimple sites with few tags
Google Tag ManagerGTM-XXXXXXX containerMarketing can add tags without deploysTeams with many tags and campaigns
Custom eventsgtag('event', 'sign_up', { method })Tracks the actions that matterSign ups, purchases, leads
SPA page viewsgtag('event', 'page_view', { page_path })Route changes do not reload the pageReact, Vue, Next.js apps without history tracking
Consent Mode v2gtag('consent', 'default', {…denied})Legal compliance with modelled dataEEA and UK visitors, and good practice everywhere
DebugTag Assistant, ?debug_mode=trueEvents appear live in DebugViewBefore shipping any tracking change

Try it

ga4.htmlHTML
<!-- 1. Consent defaults, before the tag loads -->
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('consent', 'default', { ad_storage: 'denied', ad_user_data: 'denied', ad_personalization: 'denied', analytics_storage: 'denied' });
</script>
<!-- 2. The Google tag with your measurement ID -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXXXXX');
</script>
<!-- 3. When the visitor accepts, and when something useful happens -->
<script>
  function onAccept(){ gtag('consent', 'update', { analytics_storage: 'granted' }); }
  function onSignUp(){ gtag('event', 'sign_up', { method: 'Google' }); }
</script>

Load the tag once, high in <head>. Two copies of gtag, or gtag plus a GTM tag for the same ID, double count every page view.

DevToolsChrome
# a recorded session, replayed when you press Run
# DevTools console after a sign up
dataLayer.map(a => a[0] + ' ' + a[1])
["consent default", "js Mon Oct 05 2026", "config G-XXXXXXXXXX", "consent update", "event sign_up"]
# Network tab, filtered by collect
POST https://region1.google-analytics.com/g/collect?v=2&tid=G-XXXXXXXXXX&en=page_view  204
POST https://region1.google-analytics.com/g/collect?v=2&tid=G-XXXXXXXXXX&en=sign_up  204

Key terms

TermSimple meaning
Measurement IDG-XXXXXXXXXX: tells the tag which property to send to
Data streamOne website or app feeding a property
EventAnything a visitor does, sent with parameters
Key eventAn event you mark as a success, such as sign_up
Custom dimensionA parameter registered so it shows in reports
DebugViewA live view of events from your test browser
12

Search Console and verification

Prove you own the site, submit your sitemap, then watch indexing, queries and Core Web Vitals straight from Google.

verificationURL InspectionPerformanceMeasured
In one lineGoogle's report card for your site
In simple words

Search Console is where Google tells you how it sees your site: which pages are in the index and why others are not, which searches show you, how many people click, and which pages are slow.

Under the hood

Add a Domain property (DNS TXT record, covers every subdomain and protocol) or a URL prefix property (HTML meta tag, HTML file, GA or GTM). Submit sitemaps under Sitemaps; Google retired the sitemap ping endpoint in 2023. URL Inspection shows the indexed and live rendered HTML, canonical choice and crawl status, and can request indexing. Page indexing explains exclusions such as noindex, duplicates and soft 404s. Performance gives clicks, impressions, CTR and position by query and page for 16 months. Link GA4 to see queries alongside behaviour. Bing Webmaster Tools and IndexNow cover Bing and others.

Pick it forOn day one of every site, and after migrations or traffic drops.

Domain property
DNS TXT record
URL prefix
meta tag, file, GA or GTM
Data kept
16 months of performance
Bing
Webmaster Tools + IndexNow

How a conversation looks

Verify once, then Google reports back continuously
You
Search Console
Your site
Googlebot
add property example.com
look for the TXT record or meta tag
google-site-verification=…
submit /sitemap.xml
schedule crawling
indexing, queries, CWV reports

requestresponsepush or streamcontrol

Methods, usage, why and when

MethodUsageWhyWhen to use
Domain propertyTXT google-site-verification=…Covers all protocols and subdomainsWhenever you control DNS
URL prefix property<meta name="google-site-verification">Quick without DNS accessAgencies, subfolders
Submit sitemapSitemaps, Add a new sitemapFaster discovery and a coverage countLaunch and structure changes
Request indexingURL Inspection, Request indexingAsks Google to recrawl one URLImportant new or fixed pages
GA4 linkGA4 Admin, Search Console linksQueries next to behaviourContent and landing page analysis
IndexNowGET api.indexnow.org/indexnow?url=…&key=…Instant notice to Bing and othersFrequently updated sites

Try it

verify.shBASH
# Domain property: add the TXT record Search Console gives you, then check it
dig +short TXT example.com | grep google-site-verification

# URL prefix property: the meta tag option, in <head>
# <meta name="google-site-verification" content="your-token">
curl -s https://example.com | grep -o 'google-site-verification" content="[^"]*'

# Bing and other IndexNow engines: notify a changed URL
curl -s -o /dev/null -w "%{http_code}\n" "https://api.indexnow.org/indexnow?url=https://example.com/blog/seo-guide&key=$INDEXNOW_KEY"

Prefer the Domain property. It covers http, https, www and every subdomain in one place, so nothing falls outside your reports.

shree@devbox: ~Ubuntu 24.04
# a recorded session, replayed when you press Run
dig +short TXT example.com | grep google-site-verification
"google-site-verification=3fQx9kT2bLr7mWp0aZcVnE4hYs1uJdG8oPiXwq6tRvM"
curl -s -o /dev/null -w "%{http_code}\n" "https://api.indexnow.org/indexnow?url=https://example.com/blog/seo-guide&key=$INDEXNOW_KEY"
200

Key terms

ReportAnswers
PerformanceWhich queries and pages get clicks and impressions?
Page indexingWhich pages are indexed, and why not the others?
URL InspectionHow did Google crawl and render this exact URL?
SitemapsWas the sitemap read, and how many URLs were found?
Core Web VitalsWhich groups of pages fail in the field?

Launch checklist

Run down this list before every launch and after every redesign. Each line points to the module that explains it.

CheckToolTargetWhy
Unique title and descriptionView source, Lighthouse SEOEvery pageYour search snippet
Self canonical, one hostcurl -I, URL InspectionOne URL per pageNo split signals
Server HTML has contentcurl, URL InspectionTitle, h1, links presentBots that skip JavaScript
robots.txt and sitemap/robots.txt, Search ConsoleNo blocked CSS or JSDiscovery
Structured data validRich Results TestZero errorsRich result eligibility
Open Graph tagsPost Inspector, Sharing Debugger1200 × 630 imageClicks from social
LCPPageSpeed Insights≤ 2.5 s at p75Loading speed
INPPageSpeed Insights≤ 200 ms at p75Responsiveness
CLSPageSpeed Insights≤ 0.1 at p75Stability
GA4 firing onceTag Assistant, DebugViewOne page_view per viewAccurate data
Consent defaultsTag Assistant consent tabDenied until acceptedCompliance
Search Console verifiedDomain propertySitemap read, no errorsVisibility into Google