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.
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.
Google can find, fetch and render every page you want indexed.
Your markup tells Google and social apps what each page is.
You can see traffic, queries and conversions, and fix what is broken.
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.
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).
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
requestresponsepush or streamcontrol
Methods, usage, why and when
| Method | Usage | Why | When to use |
|---|---|---|---|
Raw HTML check | curl -sA "…Googlebot…" URL | Shows what arrives before any JavaScript runs | Debugging a page missing from Google |
URL Inspection | Search Console, Test live URL | Shows Google's own rendered HTML and screenshot | Confirming content and links survive rendering |
site: search | site:example.com/blog | A rough view of what is indexed | Quick spot checks, never exact counts |
Crawlable links | <a href="/pricing">Pricing</a> | Google discovers pages through href links | Every navigation element |
Try it
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.
# 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
| Term | Simple meaning |
|---|---|
Crawl | Google downloads the page |
Render | Google runs the JavaScript and builds the final page |
Index | Google stores what the page is about |
Crawl budget | How many URLs Google will fetch from your site; matters on huge sites |
Mobile first indexing | Google judges the mobile version of your page |
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.
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.
<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
requestresponsepush or streamcontrol
Methods, usage, why and when
| Method | Usage | Why | When to use |
|---|---|---|---|
<title> | <title>Topic: Detail | Brand</title> | Main relevance signal and usually the result headline | Every page, unique |
meta description | <meta name="description" content="…"> | Shapes the snippet and the click through rate | Every indexable page |
meta robots | <meta name="robots" content="noindex, follow"> | Keeps a page out of the index while following its links | Thank you pages, internal search, staging |
X-Robots-Tag | X-Robots-Tag: noindex | Robots rules for files that have no <head> | PDFs, images, API responses |
Snippet controls | max-snippet:160, max-image-preview:large | Controls how much Google may show | News, publishers, image heavy pages |
meta keywords | <meta name="keywords" content="…"> | Ignored by Google; harmless but unhelpful | Only if an internal search tool or another engine needs it |
Try it
<!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.
# 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
| Term | Simple meaning |
|---|---|
Title tag | The page's name in tabs and search results |
Meta description | The summary that may appear under the link |
noindex | Keep this page out of search results |
nofollow | Do not pass trust through these links |
Snippet | The text Google shows for a result |
X-Robots-Tag | Robots rules sent as an HTTP header, for PDFs and images |
Open Graph and social cards
Tags that turn a pasted link into a rich preview on LinkedIn, Slack, WhatsApp, X and Facebook.
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.
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
requestresponsepush or streamcontrol
Methods, usage, why and when
| Method | Usage | Why | When to use |
|---|---|---|---|
og:title + og:description | property="og:title" | Text on the card, can differ from the SEO title | Every shareable page |
og:image | 1200 × 630 absolute URL | The image drives most of the clicks | Every shareable page, per page |
twitter:card | summary_large_image | Large image cards on X | Pages shared on X |
Dynamic OG images | @vercel/og ImageResponse or a build script | A title in every image without manual design | Blogs and catalogues with many pages |
Re-scrape | LinkedIn Post Inspector, Facebook Sharing Debugger | Platforms cache the old card | After changing tags or images |
Try it
<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.
# 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
| Term | Simple meaning |
|---|---|
Open Graph | A tag standard for link previews |
og:image | The picture on the card |
twitter:card | Which card layout X should use |
Scraper | The bot that reads your tags |
Preview cache | Platforms keep the old card until you refresh it |
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.
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.
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
requestresponsepush or streamcontrol
Methods, usage, why and when
| Method | Usage | Why | When to use |
|---|---|---|---|
Article | "@type": "Article" | Article details such as headline, image and date | Blog posts, news |
Product + Offer | "offers": { "price": "2499", "priceCurrency": "INR" } | Price, stock and ratings in results | Product pages |
BreadcrumbList | "itemListElement": [ListItem…] | A readable path instead of a raw URL | Any site with hierarchy |
Organization | "logo", "sameAs": ["https://www.linkedin.com/company/…"] | Clarifies your brand, logo and profiles | Home or about page |
Rich Results Test | search.google.com/test/rich-results | Shows errors and which features are eligible | Before deploying markup |
FAQPage | "@type": "FAQPage" | Rich results now limited to authoritative government and health sites | Rarely; do not expect the dropdowns |
Try it
<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.
# 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
| Term | Simple meaning |
|---|---|
JSON-LD | Structured data written as JSON in a script tag |
schema.org | The shared dictionary of types and properties |
Rich result | A search result with extras such as stars or prices |
Required property | A field Google needs before the feature can show |
Manual action | A penalty applied by a human reviewer at Google |
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.
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.
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
requestresponsepush or streamcontrol
Methods, usage, why and when
| Method | Usage | Why | When to use |
|---|---|---|---|
One h1 | <h1>Main topic</h1> | States the page's subject clearly | Every page |
Landmarks | <header> <nav> <main> <footer> | Structure for bots and assistive tech | Every template |
Descriptive anchors | <a href="/pricing">See pricing</a> | Anchor text tells Google about the target | All internal links |
Responsive images | srcset, sizes, width, height, alt | Right size per screen, no layout shift, indexable | Every content image |
Lazy loading | loading="lazy" below the fold only | Saves bandwidth without delaying LCP | Images and iframes lower on the page |
Try it
<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.
# 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
| Term | Simple meaning |
|---|---|
Landmark | A labelled region such as nav or main |
Heading outline | The page's table of contents |
Anchor text | The clickable words of a link |
Alt text | A text version of an image |
Internal links | Links between your own pages, which spread importance |
URLs, canonicals and redirects
One clean, permanent URL per piece of content, with every duplicate pointing at it.
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.
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
requestresponsepush or streamcontrol
Methods, usage, why and when
| Method | Usage | Why | When to use |
|---|---|---|---|
301 / 308 | return 301 /new-path; | Moves the old URL's signals to the new one | Migrations, renamed slugs, https and host |
Self canonical | <link rel="canonical" href="https://example.com/p"> | Guards against parameter duplicates | Every indexable page |
Cross canonical | canonical → original article URL | Credits the source of syndicated content | Republished posts on Medium or partners |
hreflang | <link rel="alternate" hreflang="hi-IN" href="…"> | Sends each user to their language version | Multi language or multi region sites |
Clean slugs | /blog/frontend-seo-cheatsheet | Readable, stable and shareable | Every new route |
Try it
# 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.
# 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
| Term | Simple meaning |
|---|---|
Canonical | The preferred URL among duplicates |
301 / 308 | Moved permanently; passes signals |
302 / 307 | Moved temporarily; keep the old URL indexed |
Redirect chain | Several hops before the final page; slow and leaky |
hreflang | Which language or region each version is for |
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.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.
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
requestresponsepush or streamcontrol
Methods, usage, why and when
| Method | Usage | Why | When to use |
|---|---|---|---|
Disallow | Disallow: /admin/ | Saves crawl budget on useless paths | Admin, cart, infinite filters |
Allow | Allow: /api/og/ | Opens a path inside a disallowed folder | Exceptions such as OG image endpoints |
Sitemap line | Sitemap: https://example.com/sitemap.xml | Every crawler finds your list | Every robots.txt |
Sitemap index | <sitemapindex> of sitemap-1.xml… | Splits over 50,000 URLs | Large sites |
Generated at build | Next.js app/sitemap.ts, app/robots.ts | Never out of sync with routes | Framework sites |
Try it
# 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.
# 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
| Term | Simple meaning |
|---|---|
User-agent | Which crawler a group of rules is for |
Disallow | Do not crawl paths that start with this |
Sitemap index | A sitemap that lists other sitemaps |
lastmod | When the page content last really changed |
Orphan page | A page no internal link points to; sitemaps help, links help more |
Rendering: CSR, SSR, SSG
Where your HTML is built decides what crawlers see first and how fast users see content.
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.
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
requestresponsepush or streamcontrol
Methods, usage, why and when
| Method | Usage | Why | When to use |
|---|---|---|---|
SSG | generateStaticParams() | Fastest HTML from a CDN | Docs, blogs, marketing pages |
ISR | export const revalidate = 3600 | Static speed with fresh data | Catalogues, listings |
SSR | export const dynamic = "force-dynamic" | Always current, per request | Search results, personalised pages |
CSR | useEffect(() => fetch(…)) | Least server work | Dashboards behind login |
Metadata API | export async function generateMetadata() | Title, description, OG and canonical in server HTML | Every Next.js route |
Real status codes | notFound(), redirect() | Avoids soft 404s and stale pages | Missing or moved content |
Try it
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.
# 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
| Term | Simple meaning |
|---|---|
CSR | The browser builds the page from JavaScript |
SSR | The server builds HTML on each request |
SSG | HTML is built once, at deploy time |
ISR | Static pages that rebuild on a timer |
Hydration | JavaScript attaches to server HTML to make it interactive |
Soft 404 | An error page sent with status 200 |
Core Web Vitals
Three real user metrics Google uses as a page experience signal: loading (LCP), responsiveness (INP) and visual stability (CLS).
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.
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
requestresponsepush or streamcontrol
Methods, usage, why and when
| Method | Usage | Why | When to use |
|---|---|---|---|
Preload LCP image | <link rel="preload" as="image" fetchpriority="high"> | The browser fetches the hero first | Pages with a hero image |
Modern images | <picture> AVIF, WebP, srcset | Fewer bytes, faster LCP | Every image |
Size reservations | width/height or aspect-ratio | Stops content jumping, lowers CLS | Images, ads, embeds |
Yield to the main thread | await scheduler.yield() | Splits long tasks, lowers INP | Heavy click handlers |
Defer third parties | <script defer> or Partytown | Less blocking JavaScript | Chat widgets, tag managers |
Font loading | font-display: swap + size-adjust fallback | Text shows early without a big shift | Custom web fonts |
Try it
// <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.
# 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
| Metric | Good | Needs improvement | Poor |
|---|---|---|---|
LCP | ≤ 2.5 s | 2.5 to 4 s | > 4 s |
INP | ≤ 200 ms | 200 to 500 ms | > 500 ms |
CLS | ≤ 0.1 | 0.1 to 0.25 | > 0.25 |
TTFB (diagnostic) | ≤ 0.8 s | 0.8 to 1.8 s | > 1.8 s |
FCP (diagnostic) | ≤ 1.8 s | 1.8 to 3 s | > 3 s |
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.
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.
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
requestresponsepush or streamcontrol
Methods, usage, why and when
| Method | Usage | Why | When to use |
|---|---|---|---|
PSI web | pagespeed.web.dev | Quick field and lab view for one URL | Spot checks |
PSI API | runPagespeed?url=…&strategy=mobile | Scriptable results, JSON | Dashboards, scheduled checks |
Lighthouse CLI | npx lighthouse URL --form-factor=mobile | Local lab runs with full reports | Debugging before deploy |
Lighthouse CI | lhci autorun with budgets | Fails a pull request that regresses | Every repository with a frontend |
CrUX API | chromeuxreport.googleapis.com | Field data for any origin | Comparing with competitors |
Try it
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.
# 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 range | Colour | Meaning |
|---|---|---|
90 to 100 | Green | Good |
50 to 89 | Orange | Needs improvement |
0 to 49 | Red | Poor |
Field: FAST / AVERAGE / SLOW | Banner | CrUX overall Core Web Vitals verdict |
Google Analytics 4
Install GA4 with your measurement ID, track the events that matter, respect consent, and turn key events into conversions.
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.
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
requestresponsepush or streamcontrol
Methods, usage, why and when
| Method | Usage | Why | When to use |
|---|---|---|---|
gtag.js | gtag('config', 'G-XXXXXXXXXX') | Direct and lightweight | Simple sites with few tags |
Google Tag Manager | GTM-XXXXXXX container | Marketing can add tags without deploys | Teams with many tags and campaigns |
Custom events | gtag('event', 'sign_up', { method }) | Tracks the actions that matter | Sign ups, purchases, leads |
SPA page views | gtag('event', 'page_view', { page_path }) | Route changes do not reload the page | React, Vue, Next.js apps without history tracking |
Consent Mode v2 | gtag('consent', 'default', {…denied}) | Legal compliance with modelled data | EEA and UK visitors, and good practice everywhere |
Debug | Tag Assistant, ?debug_mode=true | Events appear live in DebugView | Before shipping any tracking change |
Try it
<!-- 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.
# 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
| Term | Simple meaning |
|---|---|
Measurement ID | G-XXXXXXXXXX: tells the tag which property to send to |
Data stream | One website or app feeding a property |
Event | Anything a visitor does, sent with parameters |
Key event | An event you mark as a success, such as sign_up |
Custom dimension | A parameter registered so it shows in reports |
DebugView | A live view of events from your test browser |
Search Console and verification
Prove you own the site, submit your sitemap, then watch indexing, queries and Core Web Vitals straight from Google.
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.
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
requestresponsepush or streamcontrol
Methods, usage, why and when
| Method | Usage | Why | When to use |
|---|---|---|---|
Domain property | TXT google-site-verification=… | Covers all protocols and subdomains | Whenever you control DNS |
URL prefix property | <meta name="google-site-verification"> | Quick without DNS access | Agencies, subfolders |
Submit sitemap | Sitemaps, Add a new sitemap | Faster discovery and a coverage count | Launch and structure changes |
Request indexing | URL Inspection, Request indexing | Asks Google to recrawl one URL | Important new or fixed pages |
GA4 link | GA4 Admin, Search Console links | Queries next to behaviour | Content and landing page analysis |
IndexNow | GET api.indexnow.org/indexnow?url=…&key=… | Instant notice to Bing and others | Frequently updated sites |
Try it
# 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.
# 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
| Report | Answers |
|---|---|
Performance | Which queries and pages get clicks and impressions? |
Page indexing | Which pages are indexed, and why not the others? |
URL Inspection | How did Google crawl and render this exact URL? |
Sitemaps | Was the sitemap read, and how many URLs were found? |
Core Web Vitals | Which 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.
| Check | Tool | Target | Why |
|---|---|---|---|
Unique title and description | View source, Lighthouse SEO | Every page | Your search snippet |
Self canonical, one host | curl -I, URL Inspection | One URL per page | No split signals |
Server HTML has content | curl, URL Inspection | Title, h1, links present | Bots that skip JavaScript |
robots.txt and sitemap | /robots.txt, Search Console | No blocked CSS or JS | Discovery |
Structured data valid | Rich Results Test | Zero errors | Rich result eligibility |
Open Graph tags | Post Inspector, Sharing Debugger | 1200 × 630 image | Clicks from social |
LCP | PageSpeed Insights | ≤ 2.5 s at p75 | Loading speed |
INP | PageSpeed Insights | ≤ 200 ms at p75 | Responsiveness |
CLS | PageSpeed Insights | ≤ 0.1 at p75 | Stability |
GA4 firing once | Tag Assistant, DebugView | One page_view per view | Accurate data |
Consent defaults | Tag Assistant consent tab | Denied until accepted | Compliance |
Search Console verified | Domain property | Sitemap read, no errors | Visibility into Google |