Gatsby SEO
How to optimize a Gatsby site for search — Gatsby's four rendering paths (SSG, DSG, SSR, client-only) and what each means for crawlability, the modern Head API vs legacy react-helmet, sitemaps, canonicals, image SEO, the React-bundle CWV cost, and the Netlify-era maintenance risk.
1 evidence signal on this page
- Related live toolCanonicalization Checker
Gatsby has four rendering options: SSG (the default — pages pre-rendered to static HTML at gatsby build), DSG (deferred static generation on first request), SSR (server-side rendering per request via Gatsby Functions), and client-only routes (rendered entirely in the browser). Most Gatsby sites lean on SSG, which gives crawlers fully rendered HTML on the first fetch with no rendering queue to wait on — a strong SEO baseline, far better than a pure client-side React app. But that baseline isn't universal: DSG and SSR pages generate HTML outside the build step and need their own production checks, and client-only routes shouldn't be assumed to expose route-specific content in the initial HTML at all. Whichever path serves a page, Gatsby still ships the full React runtime (~200KB+) and hydrates it client-side — a Core Web Vitals cost, not a crawlability one. The current way to manage metadata is the built-in Gatsby Head API (v4.19+), which works across all four rendering paths and replaces gatsby-plugin-react-helmet. The recurring pitfalls are double canonical tags, draft/orphan pages leaking into sitemaps, missing alt text, the production-only sitemap gotcha, DSG/SSR/client-only routes needing explicit sitemap and production-behavior checks, and a framework whose maintenance has slowed sharply since Netlify's 2023 acquisition.
TL;DR — By default, Gatsby builds your pages into finished HTML before anyone visits (this mode is called SSG), so when Google shows up, your content is already in the page — no waiting for JavaScript. That gives Gatsby a strong SEO head start. Gatsby also has a few other rendering options for pages that need to generate later or use per-request data — worth knowing if your site uses them. The catch: it still sends a big React bundle to the browser, which can slow your page down, and you still have to set up meta tags, sitemaps, canonicals, and image alt text yourself.
What Gatsby is
Gatsby is a website framework built on React. Most React apps build the
page in your browser after the JavaScript loads — which is a problem for SEO,
because a search engine fetching the page first sees a near-empty shell. Gatsby
flips that around by default. When you run gatsby build, it turns most pages into
a complete HTML file ahead of time. So when Google, Bing, or a reader requests one
of those pages, the content is right there in the HTML. Evidence for this claim Gatsby creates static HTML files during its production build. Scope: Gatsby static generation; client-side behavior can still be added. Confidence: high · Verified: Gatsby: Builds and deploys
That default mode makes Gatsby a static site generator (SSG), and it’s why Gatsby is generally good for SEO out of the box — much better than a plain React app. Gatsby also lets individual pages opt into three other rendering modes — generating on first request, generating per-request on a server, or rendering entirely in the browser — which the Advanced tab covers, since each one changes what a crawler actually sees.
Why people worry about Gatsby and SEO (and why they mostly shouldn’t)
The most common myth is “Gatsby is bad for SEO because it uses React.” That’s false. The React part runs after the page is already built and delivered — it just makes the page interactive. The text, links, and headings a search engine cares about are already in the HTML from the start.
So Gatsby clears the biggest hurdle automatically. What it doesn’t do is all of SEO for you.
What you still have to set up
A Gatsby site is not automatically optimized. You still need to:
- Add title tags and meta descriptions to every page (the modern way is the built-in Gatsby Head API). Evidence for this claim Gatsby's Head API lets pages export document-head metadata. Scope: Gatsby Head API. Confidence: high · Verified: Gatsby: Head API
- Generate a sitemap (the
gatsby-plugin-sitemapplugin). - Set canonical URLs so duplicate versions of a page don’t compete.
- Write alt text for images — Gatsby’s image tool resizes and optimizes images automatically, but it does not write alt text for you.
The one thing that surprises people
Gatsby still ships the full React runtime (~200KB+) to the browser on every page. It doesn’t hurt whether you get indexed — the HTML is already complete — but it can slow your page down, which affects Core Web Vitals (Google’s page-experience speed metrics). Lighter frameworks like Astro send almost no JavaScript by comparison.
One more thing to know if you’re choosing Gatsby today: Netlify acquired Gatsby in 2023, and active development has slowed a lot since. For an existing Gatsby site that’s fine; for a brand-new project, it’s worth weighing.
Want the technical version — the Head API vs the old react-helmet plugin, the sitemap and canonical gotchas, the React-bundle CWV trade-off, and the honest take on Gatsby’s future? Switch to the Advanced tab.
TL;DR — Gatsby has four rendering options — SSG (the default), DSG, SSR, and client-only routes — and they don’t all put content in the raw HTML the same way. SSG pre-renders to static HTML at build time (
gatsby build), so content is in the raw HTML before the first crawler request — no Web Rendering Service, no rendering queue. DSG defers generation to the first request; SSR generates per request on a server; client-only routes render nothing route-specific until the browser runs the JavaScript. Whichever path serves a page, Gatsby hydrates a full React bundle (~200KB+) on the client, which is a Core Web Vitals cost, not a crawlability one. The current metadata approach is the built-in Gatsby Head API (v4.19+), which works across all four rendering paths and replacesgatsby-plugin-react-helmet. The recurring failure modes are double canonical tags (Head API + react-helmet both firing), draft/orphan pages leaking into sitemaps, the production-only sitemap gotcha, missing alt text onGatsbyImage, trailing-slash inconsistency, and DSG/SSR/client-only routes that never got a production check. And the elephant in the room: since Netlify’s 2023 acquisition, Gatsby’s maintenance has slowed sharply.
Gatsby’s four rendering options — and why they matter for SEO
Google processes JavaScript pages in three phases — crawling, then rendering, then indexing — and rendering happens in a separate pass from a queue using headless Chromium. Google’s own guidance is blunt about why you shouldn’t lean on that: “Server-side or pre-rendering is still a great idea because it makes your website faster for users and crawlers, and not all bots can run JavaScript.”
A Gatsby site isn’t universally build-time static HTML — Gatsby supports four distinct rendering paths, chosen per page or template Evidence for this claim gatsby build writes production output, including generated HTML, to the public directory. Scope: Gatsby production builds. Confidence: high · Verified: Gatsby CLI: build :
- SSG (Static Site Generation) — the default.
gatsby buildemits fully rendered static HTML into/publicfor the page. Googlebot’s first-wave raw-HTML fetch already contains the complete content — text, links, metadata. This is the safe, low-risk path, and it’s what most Gatsby pages use. - DSG (Deferred Static Generation). Generation is deferred until the page’s first request rather than happening for every page at build time — useful for sites with huge numbers of low-traffic pages where a full build would be slow. The build alone does not tell you what a crawler sees; the page’s HTML doesn’t exist until something requests it, so first-request and cached behavior need their own check, not just a build log.
- SSR (Server-Side Rendering). The page is rendered per request, using request-time data, via Gatsby Functions. Because it runs at request time, SSR pages need production checks a build-time page doesn’t: response status, caching headers, timeout behavior, and what a crawler sees on an empty or error response — none of that is visible from a successful local build.
- Client-only routes. These render entirely in the browser and don’t get route-specific content in the initial HTML — the same profile as a plain client-side React app. Don’t assume Google (or any crawler) sees anything page-specific here until JavaScript runs; treat these as JS-dependent by design, which is fine for gated/authenticated content but wrong for anything you want indexed with content.
React hydration (ReactDOMClient.hydrateRoot()) happens client-side on top of
whichever of these produced the HTML, purely to add interactivity — that part is
the same regardless of rendering path, and is a separate concern from which path
generated the page (more on hydration cost below).
Contrast SSG/DSG/SSR with a pure client-side React app, which serves an empty
<div id="root"> and depends on the browser (or the renderer) to build the page.
Most Gatsby pages ship meaningful HTML by default — that’s the indexability win,
and it’s real, but it’s a per-page property, not a framework guarantee. Rendering
options and image tooling don’t guarantee Core Web Vitals, indexing, or rankings on
their own; verify the actual production output for whichever path a route uses.
The catch with SSG/DSG/SSR alike is a performance one, not a crawlability one: Gatsby ships the full React runtime to every page and re-hydrates it. More on that, and the Astro comparison, below.
Gatsby Head API vs gatsby-plugin-react-helmet
This is the single most important “are you doing it the modern way?” question in Gatsby SEO.
The legacy approach — gatsby-plugin-react-helmet. For years, the standard way
to set <title>, meta description, and other head tags was the react-helmet
library plus this plugin. The plugin’s job was to give react-helmet SSR
support — without it, your title/meta tags would only appear after JS execution,
not in the raw HTML, which defeats the point. It works, but it has known issues with
React Hooks and concurrent rendering, plus a background-tab title bug you patch with
defer={false}.
The modern approach — the Gatsby Head API (v4.19+). Gatsby now has a built-in
way to add head elements: a named Head export from any page or template file. Evidence for this claim Gatsby pages and templates can export a named Head function to add head elements. Scope: Gatsby Head API. Confidence: high · Verified: Gatsby: Head API
export const Head = () => (
<>
<title>Page Title</title>
<meta name="description" content="..." />
</>
)It receives useful props — location.pathname, params, data (from the page’s
GraphQL query), and pageContext — and it deduplicates tags that share an id
prop (last one wins), though running two different head-tag mechanisms at once
(Head API plus a leftover react-helmet call) can still conflict even with
deduplication. It works only in page files and templates, not in arbitrary
components. The advantages over react-helmet: no third-party bundle, no Provider
wrapper, deterministic tag order with React 18 streaming. Use the Head API for all
new projects, and plan a migration for existing ones.
The Head API works the same way across all four rendering paths — SSG, DSG, SSR,
and client-only routes all support a Head export. What differs is when its
output lands in HTML a crawler can fetch: on SSG it’s baked in at build time; on
DSG and SSR it’s generated at first-request or per-request time; on a client-only
route it isn’t in the initial HTML at all. Don’t assume “I added a Head export”
is equivalent to “this is in the raw HTML for every route” — check the actual
production output (view-source: or curl) for whichever path each route uses,
not just one representative page.
The “meta tags in DevTools but not in source” bug. A classic Gatsby SEO symptom:
your title/meta tags show up in Chrome DevTools but are missing from view-source:.
The cause is that DevTools shows the hydrated DOM (after JS runs), while view-source
shows the raw HTML. If your tags only appear in DevTools, your SEO component is
rendering client-side instead of being baked into Gatsby’s static output — usually
because it’s used as a regular component rather than as (or inside) the page’s Head
export. Always verify in raw HTML, not DevTools.
Wiring up an SEO component (the GraphQL data layer)
Gatsby’s data layer is GraphQL, and it’s how you feed metadata into your pages.
useStaticQuerypulls global defaults fromsiteMetadata(title, description,siteUrl) defined ingatsby-config.js.- Page-level GraphQL queries pass a
dataprop straight to theHeadexport — no extra wiring needed. - The standard pattern is prop || siteMetadata fallback: use the per-page value if it exists, otherwise the site default.
A Head export that takes page data looks like:
export const Head = ({ data }) => (
<>
<title>{data.post.title}</title>
<meta name="description" content={data.post.excerpt} />
</>
) Sitemaps: gatsby-plugin-sitemap (and its traps)
Install gatsby-plugin-sitemap and configure it in gatsby-config.js. A few things
trip people up:
- It generates
sitemap-index.xml, not/sitemap.xml. Submit the index URL in Google Search Console — don’t submit/sitemap.xmland expect it to resolve. - It only runs in production builds. It does nothing in
gatsby develop. To test it, rungatsby build && gatsby serve. People file “my sitemap is missing” reports that are really just “I never ran a production build.” createLinkInHead: trueby default adds the sitemap reference to the HTML head automatically.- It always excludes
/dev-404-page,/404, and/offline-plugin-app-shell-fallback. <priority>and<changefreq>are ignored by Google — the plugin docs say so directly. Focus on an accurate<lastmod>instead.entryLimitdefaults to 45,000 URLs per file.
Excluding drafts is your job. Gatsby builds everything it finds, so draft
content sails straight into the sitemap unless you filter it. Do that in
gatsby-node.js with a GraphQL filter (e.g. excluding entries without a publish
date), not by hiding it in a React component — by then it’s already built and
listed.
DSG, SSR, and client-only routes need an explicit sitemap decision.
gatsby-plugin-sitemap reflects what it can see at build time. An SSG page is
straightforward — it exists as a static file, so it’s naturally sitemap-eligible.
A DSG page’s HTML doesn’t exist yet at build time (it generates on first request),
an SSR page never has fixed HTML at all, and a client-only route has no
route-specific content to index in the first place. Don’t assume any of these are
in the sitemap (or should be) just because the route exists — decide, page by page,
whether it belongs, and verify the generated sitemap-index.xml actually reflects
that decision rather than a build-time guess.
robots.txt: gatsby-plugin-robots-txt
gatsby-plugin-robots-txt generates robots.txt at build time. The useful detail
for SEO is environment awareness: it reads process.env.GATSBY_ACTIVE_ENV then
process.env.NODE_ENV, so you can serve different rules per environment. The
classic use is blocking crawlers on Netlify preview/branch deploys so your
staging URLs don’t get accidentally indexed, while leaving production open.
Canonical URLs (and the double-canonical bug)
Two viable approaches:
gatsby-plugin-canonical-urlsadds a<link rel="canonical">to every page. SetstripQueryString: trueso/blog?tag=fooand/blogdon’t get treated as separate canonicalized pages — recommended for most sites.- The Head API, setting canonicals yourself from
location.pathname:
export const Head = ({ location }) => (
<link rel="canonical" href={`https://example.com${location.pathname}`} />
)The double canonical bug. This is a known, easy-to-hit issue: if you use
gatsby-plugin-canonical-urls and a react-helmet canonical tag at the same
time, you emit two <link rel="canonical"> tags. Pick one mechanism. (If you’re
on react-helmet, gatsby-plugin-react-helmet-canonical-urls is the helmet-aware
option; on the Head API, set the canonical there and drop the plugin.)
Trailing slashes. Gatsby pages can be reachable with and without a trailing
slash, and Gatsby’s <Link> component uses client-side History API routing — which
bypasses server-side 301 redirects you’d normally use to normalize trailing slashes.
Decide on one form, enforce it at the host/CDN level, and keep canonicals consistent
with it.
Image SEO: gatsby-plugin-image
gatsby-plugin-image is genuinely one of Gatsby’s strengths. Two components:
StaticImage— for images whose path is known and hardcoded at build time.GatsbyImage— for dynamic images coming from GraphQL.
What it does automatically: multiple sizes, WebP/AVIF formats, lazy loading, and breakpoints (750/1080/1366/1920px). It also generates placeholders (blurred, dominant color, or traced SVG) that reserve space and prevent Cumulative Layout Shift — a Core Web Vitals metric. Because dimensions are set, you avoid CLS, and modern formats plus lazy loading help LCP.
The one thing it does not do is write alt text. That’s on you, every time —
missing alt text on GatsbyImage is one of the most common Gatsby SEO oversights.
(Migrating from the old gatsby-image package? There’s a codemod:
npx gatsby-codemods gatsby-plugin-image.)
Structured data (JSON-LD)
Google’s preferred structured-data format is JSON-LD, and the clean way to add it in modern Gatsby is via the Head API with a script tag:
export const Head = ({ data }) => (
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": "Article",
"headline": data.post.title,
})}
</script>
)gatsby-plugin-next-seo offers pre-built JSON-LD components if you’d rather not
hand-roll them. One frequent confusion to clear up: gatsby-plugin-manifest is
not a structured-data plugin — it generates the PWA web app manifest (icons,
theme color), nothing to do with schema.
The React bundle and Core Web Vitals
Here’s Gatsby’s real weakness relative to the zero-JS generators.
- Full hydration (the Gatsby 1–4 default) hydrates the entire React tree and ships a 200KB+ React runtime to every page. The HTML is pre-rendered, so this doesn’t hurt crawlability — but it absolutely affects load speed and CWV.
- Partial hydration (Gatsby 5, experimental) hydrates only components marked
"use client"and leaves the rest as static HTML, cutting the JS shipped and directly improving TTI and CWV. The limitations are real: production builds only, still beta, and incompatible with emotion, styled-components, andgatsby-plugin-offline.
The takeaway: Gatsby’s JS payload is a performance problem, not an indexability one. Googlebot still renders JS to assess page-experience signals, so the bundle can cost you on CWV even though your content indexes fine.
Gatsby vs Astro for SEO
If you’re choosing a static framework today, this is the comparison that matters most for SEO.
| Dimension | Gatsby | Astro |
|---|---|---|
| JS shipped to browser | 200KB+ (full React runtime) | ~5KB (interactive islands only) |
| Rendering model | SSG → SPA (full hydration) | SSG → MPA (zero hydration by default) |
| Build speed (40 pages) | 2–3 minutes | Under 10 seconds |
| SEO plugin ecosystem | Mature (gatsby-plugin-*) | Growing |
| Crawl-budget impact | Higher (more JS for Google to render) | Lower |
| Framework future | Uncertain (Netlify ownership, slowed activity) | Active, growing |
Both pre-render indexable HTML — that’s a wash. The difference is the JS tax: Astro’s islands ship a fraction of the JavaScript, which a Vaihe comparison frames as “Reduced JavaScript execution conserves crawl budget and accelerates page scanning.” (One historical caveat on the other side: Astro’s image handling lacked automatic width/height at the time of that comparison, which produced Lighthouse warnings — check current Astro docs, as it may be resolved.)
For context on how the whole field compares, see the static site generators hub.
The honest part: Gatsby’s maintenance trajectory
I won’t sugarcoat this, and I won’t catastrophize it either.
Netlify acquired Gatsby Inc. in February 2023. Gatsby Cloud was sunset and customers were moved to Netlify; Netlify stated the acquisition would “not impact Gatsby JS.” Since then, activity has slowed markedly. A widely-read community GitHub discussion (#39062) argues Gatsby is effectively abandoned — minimal commits, no React 19 support, a 2024 roadmap that wasn’t delivered, and the telemetry service shut down. Maintainers have framed the current state as security fixes, limited dependency updates, and low-hanging-fruit bug fixes.
What that means for SEO teams. For an existing Gatsby site, none of this is
an emergency — it builds, it indexes, it works. The risk is ecosystem decay over
time: SEO depends on plugins (gatsby-plugin-sitemap, image, canonical-urls), and
aging plugins (e.g. gatsby-source-shopify facing API deprecation) can eventually
break in ways that quietly degrade indexing. For a new project, weigh that
seriously — the frameworks people are migrating to are Astro and Next.js.
Production checklist: verify every rendering path
A local gatsby develop session or even a clean gatsby build log doesn’t prove
what a crawler actually receives. Because SSG, DSG, SSR, and client-only routes
each generate HTML differently, check production behavior per path rather than
assuming one representative page covers all of them:
- Raw HTML content. For each rendering path in use, fetch a real production
URL with
curl -s <url>orview-source:— not DevTools — and confirm the content, title, and meta tags a crawler would see are actually there. - Metadata (Head export output). Confirm the
Headexport’s tags land in that raw HTML for the path in question. This is where DSG/SSR differ most from SSG: the tags exist in your source either way, but only a production fetch proves they made it into the response. - HTTP status. Check the response status code on production, especially for SSR and DSG routes — a page that renders fine locally can 500 in production on first request or under load in ways a build never surfaces.
- Caching behavior. SSG output is a static file with predictable caching. DSG caches after the first request — confirm the second request is fast and correct, not just the first. SSR responses depend on your caching headers and hosting layer; verify stale or per-request content isn’t served to the wrong visitor.
- Failure and empty-state behavior. For SSR and DSG pages backed by request-time or first-request data, check what a crawler sees if that data fetch fails or returns empty — an unhandled error state is not the same page you tested locally with good data.
- Sitemap generation and exclusions. Re-confirm this only happens on a
production build (
gatsby build && gatsby serve, nevergatsby develop), and that the exclusions you expect — drafts, client-only routes, anything you decided shouldn’t be listed — are actually absent from the generatedsitemap-index.xml, not just absent from your intent.
None of this is optional per rendering path — a working gatsby build proves SSG
output, not DSG, SSR, or client-only behavior.
Common Gatsby SEO mistakes
- Still using
gatsby-plugin-react-helmet— legacy; migrate to the Head API. - Meta tags in DevTools but not in page source — the SEO component is rendering
client-side; check
view-source:, not DevTools. - Draft content in sitemaps — filter drafts in
gatsby-node.jswith GraphQL, not in a React component. - Orphan pages from
src/pages— Gatsby auto-routes everything there; stale files build and land in the sitemap. - Double canonical tags —
gatsby-plugin-canonical-urls+ react-helmet both firing. - Trailing-slash inconsistency —
<Link>client-routing bypasses server-side trailing-slash redirects. - Not stripping query strings from canonicals — set
stripQueryString: true. - Alt text omitted on
GatsbyImage— it isn’t auto-generated. - Submitting
/sitemap.xml— the real file is/sitemap-index.xml. - Testing the sitemap in
gatsby develop— it only generates ongatsby build. - Noindex toggled by React state — Google may have already processed the raw
HTML; and when it sees
noindexin raw HTML it may skip rendering entirely. Keep noindex decisions in static HTML or server headers.
For the JavaScript-rendering fundamentals behind all of this, see the parent JavaScript SEO hub.
AI summary
A condensed take on the Advanced version:
- Gatsby has four rendering paths — SSG, DSG, SSR, and client-only routes.
SSG is the default:
gatsby buildpre-renders the page to static HTML, so crawlers get full content on the first fetch — no rendering queue, and the strongest indexability baseline. DSG defers generation to the first request; SSR generates per request; client-only routes render nothing route-specific until JavaScript runs in the browser (same profile as a plain client-side React app). Verify production HTML per path — a successfulgatsby buildonly proves SSG behavior. - The React-bundle cost is performance, not crawlability, and applies regardless of rendering path. Gatsby hydrates a full React runtime (~200KB+) on the client, which hits Core Web Vitals. The zero-JS generators (Hugo, Jekyll, Eleventy) don’t have this; Astro ships ~5KB via islands.
- Use the Gatsby Head API (v4.19+), a named
Headexport from page/template files, for titles/meta/canonical/JSON-LD. It works across all four rendering paths and replaces the legacygatsby-plugin-react-helmet. It works only in pages/templates and dedupes byid. - Production checklist per path: raw HTML content, Head-export metadata, HTTP status, caching behavior, and failure/empty-state handling — check all five for whichever rendering path a route actually uses, plus confirm sitemap generation and exclusions only on a production build.
- Sitemaps:
gatsby-plugin-sitemapgeneratessitemap-index.xml(submit that, not/sitemap.xml) and only runs ongatsby build, not in dev. Google ignores<priority>/<changefreq>— focus on<lastmod>. Filter drafts ingatsby-node.js. DSG, SSR, and client-only routes each need an explicit sitemap-inclusion decision — don’t assume they’re covered the same way an SSG page is. - Canonicals: Head API +
location.pathname, orgatsby-plugin-canonical-urlswithstripQueryString: true— never both (double canonical bug). - Images:
gatsby-plugin-imageauto-handles sizes/WebP/lazy-load and prevents CLS, but does not write alt text — that’s manual. - The “DevTools but not view-source” bug = SEO component rendering client-side
instead of via the
Headexport. - Maintenance risk: Netlify acquired Gatsby in 2023; activity has slowed (no React 19, undelivered roadmap). Fine for existing sites; for new projects weigh Astro/Next.js.
Official documentation
Primary-source documentation from Gatsby and from Google.
Gatsby
- Rendering options — the overview of Gatsby’s four rendering paths: SSG, DSG, SSR, and client-only routes.
- Using Deferred Static Generation — DSG’s first-request generation behavior.
- Using Server-Side Rendering — SSR’s request-time output via Gatsby Functions.
- Client-only routes and user authentication — why client-only routes don’t expose route-specific content in the initial HTML.
- Adding an SEO component — the recommended SEO-component pattern.
- Gatsby Head API reference — the modern, built-in way to manage head tags.
- Introducing the Gatsby Head API — why it replaced react-helmet.
- gatsby-plugin-sitemap — sitemap generation (production builds only).
- gatsby-plugin-image —
StaticImage/GatsbyImage, formats, CLS prevention. - gatsby-plugin-robots-txt — environment-aware robots.txt.
- gatsby-plugin-canonical-urls — site-wide canonical tags and
stripQueryString. - gatsby-plugin-react-helmet — the legacy metadata approach.
- gatsby-plugin-next-seo — pre-built JSON-LD/schema components.
- React hydration in Gatsby — how the static HTML becomes interactive.
- Understand the JavaScript SEO basics — the crawl → render → index phases, and why pre-rendering helps.
Quotes from the source
On-the-record statements that apply directly to Gatsby’s architecture. Gatsby is a JavaScript SSG, so the relevant primary sources are Google’s JavaScript-rendering guidance and Google reps on the same.
Google Search Central — JavaScript SEO basics
- “Server-side or pre-rendering is still a great idea because it makes your website faster for users and crawlers, and not all bots can run JavaScript.” — the case for exactly what Gatsby does at build time.
- “The page may stay on this queue for a few seconds, but it can take longer than that.” — on the rendering queue that pre-rendered HTML lets you skip.
Martin Splitt, Google (Developer Advocate)
- “Even though Googlebot can render JavaScript, we don’t want to rely on that.” — the principle behind preferring SSG/SSR. (Via SEJ / Botify coverage.)
- “A lot of people are still looking at view source. That is not what we use for indexing. We use the rendered HTML.” — the exact reason the “DevTools vs view-source” Gatsby bug confuses people. (Via SEJ.)
- “The median time in the render queue is only five seconds.” — note this is the median; tail latency can be far longer. (BrightonSEO, via SEJ.)
John Mueller, Google (Search Advocate)
- “Server-side rendering is not a requirement there. We can render JavaScript-based pages for the most part.” — context: SSR/SSG is best practice, not strictly required; Google will eventually render CSR content. (Via SEJ.)
Gatsby SEO checklist
A pass to confirm a Gatsby build is actually search-friendly:
- Primary content appears in View Source (raw HTML), not just in DevTools — confirms it’s pre-rendered, not client-side only.
- Metadata is set via the Gatsby Head API (
Headexport from pages/ templates), not legacy react-helmet on new work. - Every page has a unique, build-time
<title>and meta description, with asiteMetadatafallback. - Canonical tags are set one way only — Head API or
gatsby-plugin-canonical-urls, never both (avoids the double-canonical bug). -
stripQueryString: trueis set so query-string variants don’t fragment canonicals. -
gatsby-plugin-sitemapis installed; you verified the output withgatsby build && gatsby serve(it doesn’t run in dev). - You submitted
/sitemap-index.xml(not/sitemap.xml) in Search Console. - Draft content is filtered out in
gatsby-node.js(GraphQL), so it never enters the sitemap. - No stale/orphan files in
src/pagesthat auto-build into routes. -
robots.txtblocks Netlify preview/branch deploys but leaves production open. - Every
GatsbyImage/StaticImagehas explicit alt text. - JS/CSS assets aren’t blocked in
robots.txt. - Trailing-slash form is decided and enforced at the host/CDN (not just via
<Link>). -
noindexdecisions live in static HTML or server headers, not React state. - Core Web Vitals checked — the React bundle is the likely drag; consider partial hydration where viable.
- For every rendering path in use (SSG, DSG, SSR, client-only), you checked
production raw HTML, metadata, HTTP status, caching behavior, and
failure/empty-state handling — not just a local
gatsby buildlog. - DSG, SSR, and client-only routes each have an explicit, verified sitemap decision — none of them are covered by the same assumption as an SSG page.
The mental models
1. Crawlability vs performance are two separate scorecards. Gatsby aces crawlability (HTML is pre-rendered) but pays on performance (the React bundle). Don’t conflate them — “Gatsby is bad for SEO because React” mixes up the two. Your content indexes fine; your Core Web Vitals are where the JS tax shows up.
2. The earlier the HTML exists, the less can go wrong. SSG decides HTML at build time — the safest point. Gatsby’s default page is an SSG, so it’s on the safe end of the rendering spectrum (client-only → SSR → DSG → SSG, roughly riskiest to safest for indexability). But Gatsby is a multi-mode framework — a given page might be DSG, SSR, or client-only instead, each earning its own place on that spectrum. Know which mode each route actually uses before assuming it’s on the safe end.
3. Head API by default; react-helmet only by inheritance.
For anything new, the decision is made: Gatsby Head API. react-helmet is a thing you
migrate off of, not something you reach for. If a tag isn’t showing in raw HTML,
the first question is “is this in a Head export, or stuck in a component?”
4. Pick exactly one canonical mechanism.
Head API canonical or gatsby-plugin-canonical-urls — never both. Two mechanisms =
two canonical tags = the double-canonical bug. Same discipline for anything that
writes to <head>: one source of truth per tag.
5. Gatsby builds everything it finds — so filtering is your job.
Drafts, stale src/pages files, query-string variants: Gatsby doesn’t editorialize.
If you don’t exclude it (in gatsby-node.js, in canonical config, in sitemap
options), it ships and gets crawled.
6. New project vs existing site changes the maintenance calculus. Existing Gatsby site: keep it, it works. New project: the slowed maintenance and ecosystem-decay risk are real inputs — weigh Astro/Next.js before committing.
Gatsby SEO — cheat sheet
Metadata: which approach
| Approach | Status | Use it when |
|---|---|---|
Gatsby Head API (Head export) | Current (v4.19+) | All new work; pages/templates only |
gatsby-plugin-react-helmet | Legacy | Existing sites pending migration |
Plugins at a glance
| Plugin | Does | SEO gotcha |
|---|---|---|
gatsby-plugin-sitemap | Generates sitemap-index.xml | Production build only; submit the index URL |
gatsby-plugin-image | Sizes, WebP/AVIF, lazy-load, CLS-safe | No auto alt text |
gatsby-plugin-canonical-urls | Site-wide canonical tags | Don’t pair with react-helmet canonicals |
gatsby-plugin-robots-txt | Build-time robots.txt | Use env rules to block preview deploys |
gatsby-plugin-next-seo | Pre-built JSON-LD components | — |
gatsby-plugin-manifest | PWA manifest (icons/theme) | Not structured data |
Fast facts
- Sitemap file is
/sitemap-index.xml, not/sitemap.xml. - Sitemap doesn’t generate in
gatsby develop— usegatsby build && gatsby serve. - Google ignores
<priority>/<changefreq>— accurate<lastmod>only. - React bundle ≈ 200KB+ per page (full hydration); a CWV cost, not a crawl one.
- Astro ships ~5KB by comparison (islands only).
gatsby develop≠ production: sitemap, robots, and some optimizations differ.- Netlify acquired Gatsby Feb 2023; maintenance has slowed (no React 19 yet).
Two-line SEO Head export
export const Head = ({ data, location }) => (
<>
<title>{data.post.title}</title>
<link rel="canonical" href={`https://example.com${location.pathname}`} />
</>
) Check Gatsby’s production artifacts
Run this after gatsby build, not against gatsby develop:
find public -name '*.html' -type f | while IFS= read -r file; do
title_count=$(grep -Eio '<title>[^<]*</title>' "$file" | wc -l | tr -d ' ')
canonical_count=$(grep -Eio '<link[^>]+rel=["'"']canonical["'"'][^>]*>' "$file" | wc -l | tr -d ' ')
robots=$(grep -Eio '<meta[^>]+name=["'"']robots["'"'][^>]*>' "$file" | head -1)
if [ "$title_count" -ne 1 ] || [ "$canonical_count" -ne 1 ]; then
printf '%s\ttitles=%s\tcanonicals=%s\t%s\n' "$file" "$title_count" "$canonical_count" "$robots"
fi
doneThis catches missing or duplicate output from overlapping Head API/plugin mechanisms. Review the values and sitemap membership separately.
Patrick's relevant free tools
- SEO Incident Simulator — Practice thirty deterministic technical SEO incident investigations — indexability, crawl controls, redirects, sitemaps, markup, caching, DNS, bot verification, rendering, hreflang, and faceted navigation — with clearly labeled fixture evidence and Find → Fix → Verify handoffs.
- Raw vs. Rendered HTML Checker — See what's in your page's initial HTML versus after JavaScript runs — headless-Chrome rendering only when the page actually needs it, a rendering-strategy verdict (SSR / prerendered / CSR / hybrid), ~15 calibrated JavaScript-SEO checks (noindex, canonicals, robots.txt blocking, links, soft 404s), a side-by-side raw-vs-rendered diff, and shareable reports.
Tools for Gatsby SEO
- Gatsby Head API (built-in, v4.19+) — the no-dependency way to manage
<title>, meta, canonical, and JSON-LD per page/template. gatsby-plugin-sitemap— generatessitemap-index.xmlon production builds.gatsby-plugin-image—StaticImage/GatsbyImage, responsive sizes, WebP/AVIF, lazy loading, CLS-preventing placeholders.gatsby-plugin-canonical-urls— site-wide canonical tags withstripQueryString.gatsby-plugin-robots-txt— environment-aware robots.txt (block preview deploys).gatsby-plugin-next-seo— pre-built JSON-LD/schema components if you don’t want to hand-roll structured data.view-source:/ GSC URL Inspection — the ground truth for “is my content and metadata in the raw HTML?” (not DevTools, which shows the hydrated DOM).- Lighthouse / PageSpeed Insights — to see the Core Web Vitals cost of the React bundle and whether partial hydration is worth pursuing.
gatsby build && gatsby serve— the only way to test sitemap, robots.txt, and production-only optimizations locally.
What not to do on a Gatsby site
Concrete mistakes people actually make building and shipping Gatsby sites — prevention, not diagnosis.
Keeping gatsby-plugin-react-helmet on new work
The mistake: reaching for react-helmet by habit on new pages or a new project, because that’s what older Gatsby tutorials still show.
Why it’s wrong: react-helmet has known issues with React Hooks and concurrent rendering, plus a background-tab title bug you have to patch with defer={false}. It’s also a third-party bundle and Provider wrapper you don’t need anymore.
What to do instead: use the built-in Gatsby Head API (v4.19+) — a named Head export from the page or template file. Reserve react-helmet for existing code you haven’t migrated yet.
Running two canonical mechanisms at once
The mistake: setting a canonical via gatsby-plugin-canonical-urls and a react-helmet (or Head API) canonical tag on the same page.
Why it’s wrong: both fire, and the page ships two <link rel="canonical"> tags — a well-known Gatsby failure mode that muddies which URL you actually intended as canonical.
What to do instead: pick exactly one mechanism site-wide (the Head API, or the plugin with stripQueryString: true) and drop the other entirely.
Filtering drafts in a React component instead of at build time
The mistake: hiding unpublished content with a client-side check (e.g. if (!post.published) return null) and assuming that keeps it out of search.
Why it’s wrong: Gatsby builds everything it finds into static HTML before that component ever runs in the browser — the draft page is already built and already listed in the sitemap by the time your React check executes.
What to do instead: filter drafts in gatsby-node.js with a GraphQL query (e.g. excluding entries without a publish date) so they never get built or listed in the first place.
Shipping GatsbyImage without alt text
The mistake: trusting gatsby-plugin-image to handle image SEO end-to-end because it auto-generates sizes, formats, and placeholders.
Why it’s wrong: the plugin optimizes delivery — it does not write alt text. Missing alt text on GatsbyImage/StaticImage is one of the most common Gatsby SEO oversights precisely because everything else about the image looks handled.
What to do instead: treat alt text as a required field on every image component, every time — it’s the one piece of image SEO Gatsby leaves entirely to you.
Assuming gatsby develop shows you production SEO output
The mistake: checking the sitemap, robots.txt, or noindex behavior in gatsby develop and concluding it’s broken because nothing shows up.
Why it’s wrong: gatsby-plugin-sitemap and several other build-time behaviors simply don’t run in development mode — you’re not looking at a bug, you’re looking at dev mode doing what dev mode does.
What to do instead: run gatsby build && gatsby serve before drawing any conclusion about sitemap, robots.txt, or production-only optimizations.
Letting <Link> client-routing hide trailing-slash inconsistency
The mistake: assuming your server-side trailing-slash redirect rules apply everywhere, including in-app navigation.
Why it’s wrong: Gatsby’s <Link> component uses client-side History API routing, which bypasses the server-side 301 redirects you’d normally rely on to normalize trailing slashes — so internal links can quietly serve the “wrong” form without ever hitting your redirect rule.
What to do instead: decide on one trailing-slash form, enforce it at the host/CDN level, and make sure canonicals are consistent with it regardless of how a given page was reached.
Common Gatsby SEO issues
Symptom-first lookup for problems you’re actively seeing on a Gatsby site — start from what you observe.
Meta tags show up in DevTools but are missing from view-source
Symptom: you inspect the page in Chrome DevTools and the title/meta description look correct, but view-source: (or a raw curl) shows them missing or generic.
Likely cause: the SEO component is rendering client-side — it’s used as a regular component rather than as (or inside) the page’s Head export, so it only appears in the DOM after hydration.
Fix: move the tags into the page or template’s named Head export. Confirm with view-source: or curl -s <url> — not DevTools, which shows the hydrated DOM, not what a crawler’s raw fetch sees.
The sitemap is missing or empty
Symptom: you visit /sitemap-index.xml (or check Search Console) and get nothing, or an incomplete list.
Likely cause: almost always that you tested in gatsby develop, where gatsby-plugin-sitemap doesn’t run at all. Less commonly, the plugin isn’t installed/configured in gatsby-config.js.
Fix: run gatsby build && gatsby serve and check again. If it’s still missing, verify the plugin is present in gatsby-config.js. Confirm by requesting /sitemap-index.xml directly — remember it’s the index URL, not /sitemap.xml.
A page has two canonical tags
Symptom: viewing source (or running a schema/tag audit) shows two <link rel="canonical"> elements on the same page.
Likely cause: gatsby-plugin-canonical-urls and a react-helmet (or Head API) canonical are both firing for the same page.
Fix: remove one mechanism so only a single source sets the canonical. Confirm by viewing source again and checking with Patrick’s Canonical Tag Checker that exactly one canonical resolves.
Draft or orphan pages appear in the sitemap
Symptom: the sitemap (or Search Console coverage) lists URLs you never meant to publish — draft posts, or stale files under src/pages.
Likely cause: Gatsby auto-builds and auto-routes everything it finds — an unpublished GraphQL entry that isn’t filtered, or a leftover file in src/pages, gets built into a real static page and listed like any other.
Fix: filter drafts in gatsby-node.js with a GraphQL query (e.g. excluding entries without a publish date) so they’re never built. For orphan src/pages files, delete the stale file — filtering in a component is too late, since the page is already built by then.
Query-string variants of a page are getting indexed as duplicates
Symptom: Search Console shows near-duplicate URLs like /blog and /blog?tag=foo both indexed, or flagged as duplicate content.
Likely cause: gatsby-plugin-canonical-urls is running without stripQueryString: true, so query-string variants are canonicalized to themselves instead of the clean URL.
Fix: set stripQueryString: true in the plugin config, rebuild, and re-check the canonical tag on a query-string URL — it should now point to the clean path.
A page you intended to noindex is still showing up in search
Symptom: you set a page to noindex, but it’s still indexed weeks later, or Search Console shows it as indexed despite the tag.
Likely cause: the noindex directive was toggled via React state rather than baked into static HTML or server headers — Google may have already processed the raw HTML (without the noindex) and, when it does see noindex only in a client-rendered pass, can skip rendering the page again entirely.
Fix: move the noindex decision into static HTML (via the Head export at build time) or an HTTP header, not conditional React logic. Recheck with view-source: to confirm the tag is present in the raw response.
Test yourself: Gatsby SEO
Five quick questions on optimizing a Gatsby site for search. Pick an answer for each, then check.
Resources worth your time
My related writing
- JavaScript SEO: A Definitive Guide — rendering, DOM parity, and why static/prerendered output (like Gatsby’s) is the low-risk end of the spectrum.
- The Beginner’s Guide to Technical SEO — where rendering architecture fits in the bigger picture.
My speaking
- How Search Works (SlideShare) — my walkthrough of crawling, rendering, indexing, and ranking. (My standing disclaimer applies: “This is my understanding of systems… not going to be 100% complete or accurate.”)
From around the industry
- Gatsby Head API reference — the primary source for the modern metadata approach.
- Introducing the Gatsby Head API — Gatsby’s own explanation of why it replaced react-helmet.
- Google Search Central — JavaScript SEO basics — the crawl → render → index phases Gatsby’s build lets you skip for content.
- Gatsby is joining Netlify — the 2023 acquisition announcement, for context on maintenance.
- Netlify acquires frontend platform Gatsby (TechCrunch) — independent coverage of the acquisition.
- “Is GatsbyJS abandoned?” discussion #39062 — the community thread on Gatsby’s current maintenance state.
- SEO comparison of Gatsby vs Next vs Astro (Vaihe) — the JS-payload and crawl-budget comparison.
- Understanding partial hydration in Gatsby 5 (LogRocket) — the CWV-relevant hydration improvement and its limits.
Gatsby SEO
Gatsby SEO is the set of practices for getting a Gatsby site found and ranked. By default, Gatsby pre-renders HTML at build time (SSG), putting content in the raw HTML on the first fetch — but Gatsby also supports DSG, SSR, and client-only routes that don't all work that way, and it ships a full React bundle, so canonical tags, sitemaps, and Core Web Vitals still need deliberate work.
Related: Static Site Generator, JavaScript SEO
Gatsby SEO
Gatsby SEO covers the technical and on-page work involved in optimizing a Gatsby site — a React-based framework — for crawling, indexing, and performance. Gatsby supports four rendering paths per page or template: SSG (static site generation, the default), DSG (deferred static generation, generated on first request), SSR (server-side rendering, generated per request), and client-only routes (rendered entirely in the browser). SSG’s defining SEO trait is that it pre-renders to static HTML at build time (gatsby build), so a crawler gets fully rendered content on the first fetch with no rendering queue to wait on — a strong baseline compared to a pure client-side React app, which serves an empty shell. DSG, SSR, and client-only routes don’t share that guarantee and each need their own production check.
The catch is that Gatsby still ships the full React runtime (~200KB+) to the browser and hydrates the static HTML to add interactivity. That doesn’t hurt crawlability — the HTML is already complete — but it does carry a Core Web Vitals cost the zero-JS generators (Hugo, Jekyll, Eleventy) don’t have.
The current way to manage page metadata is the Gatsby Head API (v4.19+): a named Head export from each page or template that replaces the legacy gatsby-plugin-react-helmet. Sitemaps come from gatsby-plugin-sitemap (production builds only), images from gatsby-plugin-image, and canonicals from the Head API or gatsby-plugin-canonical-urls. The common pitfalls are double canonical tags, draft and orphan pages leaking into sitemaps, missing alt text, and a framework whose maintenance has slowed sharply since Netlify’s 2023 acquisition.
Related: Static Site Generator, JavaScript SEO
Build-time retrieval analysis plus live signals for this exact article. The automatic chunk report includes a deterministic readiness score and is ready without a model download.
Search Console
sampleGA4 traffic (28d)
sampleCloudflare traffic (7d)
sampledCrUX field data (28d, phone)
sampleGoogle NLP entities
localChangelog
Revision history
Compare the published article with an archived editorial snapshot. Added and removed words are shown only after you open a comparison.
Updated Jul 18, 2026.
Editorial summary and recorded change details.Summary
Corrected the article's universal build-time-HTML framing: Gatsby actually has four rendering paths — SSG, DSG, SSR, and client-only routes — and only SSG guarantees pre-rendered HTML at build time.
Change details
-
Added a rendering-options breakdown (SSG default, DSG first-request generation, SSR request-time rendering, client-only browser-only routes) with the SEO implications and production checks each one needs.
-
Added a Production checklist section covering raw HTML, Head-export metadata, HTTP status, caching, and failure/empty-state behavior to verify per rendering path.
-
Added sitemap guidance for DSG, SSR, and client-only routes, which don't get the same automatic sitemap treatment as SSG pages.