Modern websites, ranked in AI searchCited by ChatGPT, Perplexity & Google AI Overviews32% of our own traffic comes from AI searchModern websites, ranked in AI searchCited by ChatGPT, Perplexity & Google AI Overviews32% of our own traffic comes from AI searchModern websites, ranked in AI searchCited by ChatGPT, Perplexity & Google AI Overviews32% of our own traffic comes from AI searchModern websites, ranked in AI searchCited by ChatGPT, Perplexity & Google AI Overviews32% of our own traffic comes from AI search
AEO & AI Search

JavaScript SEO: what Googlebot actually renders

Googlebot renders JavaScript in a second pass with an evergreen Chromium. Where SPAs still break, and how to ship a JS site that gets indexed

Mahmoud Halat·May 26, 2026·7 min read
JavaScript SEOrenderingSPAGooglebot
Mahmoud Halat·May 26, 2026·7 min read

Key Takeaway

Googlebot renders JavaScript using an evergreen Chromium, but in a second pass after the initial crawl. Pages with empty initial HTML, content gated behind user interaction, or JavaScript-injected canonicals that conflict with the source HTML are the failure modes most likely to break indexing.

Googlebot does render your JavaScript, just in a second pass, on its own schedule, with a stripped-down Chromium that carries no cookies and waits only so long before moving on. That second pass is where most JavaScript SEO problems live.

I have audited dozens of React and Vue and Next.js sites for clients who were convinced "Google can't see JavaScript." That hasn't been true since 2019, when Google switched to an evergreen Googlebot running modern Chromium. The actual problem is more boring and more fixable: the site ships an empty initial HTML, then asks Googlebot to wait. Sometimes Googlebot waits, sometimes it doesn't.

This post is what the Google Search Central docs and recent Search Off the Record episodes actually say about rendering, and what to fix on a JS-heavy site.

The two-pass rendering model

Per Google's JavaScript SEO basics, Googlebot processes a JS-powered page in three phases: crawl, render, index. Crawl and render are not the same step.

In phase one, Googlebot fetches the URL and reads the response, parsing any HTML it gets, looking for href attributes to add to its crawl queue, and checking the robots meta tag. If the initial HTML is empty (just a

and a script tag) there is nothing here to index yet.

In phase two, every 200-response page goes into a render queue where a headless Chromium picks it up, executes the JavaScript, and produces the rendered HTML. Google parses that HTML for links and content, then indexes it.

> Googlebot queues every page with a 200 status code for rendering, regardless of whether the page actually uses JavaScript. The page may sit in that queue for seconds, or longer.

The render queue is the bottleneck nobody talks about. If your page only has content after JavaScript runs, you are betting on that queue. For a small site, that bet usually wins. For a fast-changing site (listings, real-time inventory, news) that bet costs you indexing latency.

Why the "Google can't see my SPA" panic is mostly wrong

The old fear came from a real era. Per Martin Splitt and Edu Pereda on Search Off the Record episode 58, the original "Google can't render JS" sentiment dates back to when rendering was slow and unreliable. Since the evergreen switch, the rendering engine is current Chromium with the full set of standard browser APIs.

What it does not support, per Google's troubleshooting guide: user-permission features (camera, geolocation), state across page loads (localStorage and cookies are wiped per fetch), and non-HTTP transports (WebSockets, WebRTC). If your content depends on any of those, the rendered HTML will be empty.

That is the real failure mode. The problem isn't "JavaScript broken," it's "content gated behind something Googlebot literally cannot do."

Three patterns that quietly kill indexing:

  • Content that only loads after a click ("Load more," "Show details," tabbed interfaces with no DOM-level content)
  • Content that requires a cookie set on the previous page
  • Content fetched from an API that fails the second request because the session is gone

If a real human can see the content on page load with no interaction, Googlebot probably can too. If they have to click first, Googlebot does not.

How browsers (and Googlebot) actually parse HTML

Per Gary Illyes and Martin Splitt on SOTR episode 105 ("How Browsers Really Parse HTML"), there is a parsing rule most developers do not know: a non-metadata element appearing in the implicitly closes the head and opens the body. Anything after that point (your canonical, your hreflang, your robots meta) ends up inside the body and is silently ignored.

The pattern that triggers this in the wild: an inline script in the head that injects an iframe or a paragraph during render. Gary cited a real example where hreflang tags appeared correct in source but were ignored because an injected iframe had already closed the head.

> Metadata elements (, , </code>) only count when they are inside the head. A stray <code><p></code> or <code><iframe></code> in the head terminates the head context, and any subsequent metadata is treated as body content and discarded.</p><p>This is one of the strongest arguments for putting your canonical, hreflang, and robots meta into the server-rendered HTML rather than injecting them later via JavaScript.</p><h2>Use the History API, not URL fragments</h2><p>A single-page app that routes by URL fragment (<code>example.com/#/products</code>) is a dead end for indexing. The <a href="https://developers.google.com/search/blog/2015/10/deprecating-our-ajax-crawling-scheme">AJAX crawling scheme was deprecated in 2015</a>; Googlebot does not resolve fragment URLs to distinct pages.</p><p>The fix is the History API. Real URLs in <code>href</code> attributes, intercepted in JavaScript, with <code>window.history.pushState</code> updating the visible URL. The Google docs example is short and worth copying.</p><p>The same rule applies to lazy-loaded content. Use <code>loading="lazy"</code> for images and iframes. For content blocks, use the IntersectionObserver API to load when the element enters the viewport. Do not rely on scroll or click handlers, because <a href="https://developers.google.com/search/docs/crawling-indexing/javascript/lazy-loading">per Google's lazy-loading guidance</a>, Search does not interact with your page. It doesn't scroll or click.</p><p>For paginated content under infinite scroll: give each chunk a real, persistent URL (<code>?page=12</code>, not <code>?date=yesterday</code>), link to those URLs sequentially, and update the visible URL via History API as new chunks become primary. That is the only way a 200th-position result becomes individually crawlable.</p><h2>The canonical-via-JavaScript trap</h2><p>This is the most common JS SEO bug we see when we audit client sites. The HTML ships with one canonical. JavaScript runs and injects a different canonical. Now Google has two signals pointing in opposite directions.</p><p>Per Martin Splitt on SOTR 105: "If you have a canonical that is there at the first time we fetch the HTML from the server and then the JavaScript changes it, we actually advise against doing that." The fix is simple to state and easy to skip: set the canonical in the server HTML, or set it only in JavaScript, never both and never conflicting.</p><p>If your CMS or React framework supports server-side rendering of the canonical link tag, use it. If it does not, ship empty in HTML and inject from JS only.</p><h2>SPA-heavy verticals where this matters most</h2><p>Two industries we work with hit JS SEO walls constantly. <a href="/for/real-estate">Real estate sites</a> lean on map components and filter UIs that load listings client-side after a user interaction, so Googlebot sees an empty grid. <a href="/for/av-installers">AV installers</a> ship portfolio galleries built as carousels with content lazy-loaded behind clicks, then wonder why their case studies do not rank.</p><p>For both, the fix is usually the same: server-render the content card body. Keep the fancy interaction client-side. The HTML response should answer the question "what's on this page" before any JavaScript runs.</p><h2>The soft-404 problem in SPAs</h2><p>Client-side routing breaks HTTP status codes. A user navigates to <code>/product/this-was-deleted</code>, the SPA loads, the API returns "not found," and the page renders an error state, but the HTTP status is still 200. Google indexes it as a real page. Now you have ten thousand thin error pages in the index.</p><p>Per Google's docs, two fixes work: either JavaScript-redirect to a URL that returns a real 404 from the server (<code>window.location.href = '/not-found'</code>), or dynamically inject a <code><meta name="robots" content="noindex"></code> when the API says the resource is gone. Either one stops the bleed, so pick whichever your stack makes easier.</p><h2>What to check before you call it done</h2><p>Use the <a href="https://support.google.com/webmasters/answer/9012289">URL Inspection Tool in Search Console</a> or the <a href="https://search.google.com/test/rich-results">Rich Results Test</a>. Both run the page through Google's actual rendering pipeline and show the post-render HTML. If your content is not in that rendered HTML, Google will not index it. No amount of theorizing fixes this, so open the tool and look at what came back.</p><p>While you are there: check the JavaScript console output the tool exposes. If something threw an error during render, that is where you find out. Logging <code>window.onerror</code> to your analytics is the move; per Google, that catches most runtime issues. Parse errors will not show up, but the rest will.</p><p>JavaScript SEO comes down to two things you can verify in the URL Inspection tool: the initial HTML gave Google enough to crawl with, and the rendered HTML gave Google everything it needed to index. If either one comes back short, you have your fix list.</p><h2>Sources</h2><ul><li><a href="https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics">Understand JavaScript SEO Basics</a> (Google Search Central, March 2026)</li><li><a href="https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript">Fix Search-Related JavaScript Problems</a> (Google Search Central, December 2025)</li><li><a href="https://developers.google.com/search/docs/crawling-indexing/javascript/lazy-loading">Fix lazy-loaded content</a> (Google Search Central)</li><li><a href="https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering">Dynamic Rendering as a Workaround</a> (Google Search Central, deprecated workaround, kept for reference)</li><li><a href="https://developers.google.com/search/blog/2019/05/the-new-evergreen-googlebot">The new evergreen Googlebot</a> (Google Search Central Blog)</li><li><em>How Browsers Really Parse HTML</em>, Search Off the Record episode 105 (Gary Illyes and Martin Splitt, February 2026)</li><li><em>JavaScript at Google</em>, Search Off the Record episode 58 (Martin Splitt, Edu Pereda, Pascal Birchler)</li></ul></div><div class="not-prose my-12 flex flex-col sm:flex-row gap-4"><a href="/blog/core-web-vitals-2026-actually" class="flex-1 rounded-xl border border-border bg-muted/30 p-5 hover:bg-muted/60 transition-colors group"><p class="text-xs text-muted-foreground mb-1 flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left"><path d="m12 19-7-7 7-7"></path><path d="M19 12H5"></path></svg> Previous in series</p><p class="text-sm font-medium text-foreground group-hover:text-primary transition-colors">Core Web Vitals in 2026: what actually changes your score</p></a><a href="/blog/canonicalization-stop-fighting-google" class="flex-1 rounded-xl border border-border bg-muted/30 p-5 hover:bg-muted/60 transition-colors group text-right"><p class="text-xs text-muted-foreground mb-1 flex items-center gap-1 justify-end">Next in series <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></p><p class="text-sm font-medium text-foreground group-hover:text-primary transition-colors">Canonicalization: stop fighting Google's canonical picker</p></a></div><div class="not-prose my-12 rounded-xl border border-border bg-muted/50 p-6 text-center"><p class="text-lg font-semibold mb-2">Is your site invisible to AI search?</p><p class="text-sm text-muted-foreground mb-4">Get a free AEO infrastructure audit and find out what your competitors are doing that you're not.</p><a href="/audit" class="inline-flex items-center gap-2 rounded-lg bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors group">Get Your Free Audit <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right w-4 h-4 group-hover:translate-x-1 transition-transform"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></a></div><div class="not-prose"><section class="my-12"><h2 class="text-sm font-bold uppercase tracking-widest mb-4 text-muted-foreground">Further Reading</h2><div class="space-y-2"><a href="/blog/structured-data-guide-ai-search-json-ld" class="flex items-center justify-between gap-3 rounded-lg border px-4 py-3 text-sm transition-colors group border-border bg-card hover:bg-accent/50"><div class="min-w-0"><span class="text-xs font-bold uppercase tracking-widest text-primary mr-2">AEO & AI Search</span><span class="font-medium group-hover:text-primary transition-colors text-foreground">The Complete Guide to Structured Data for AI Search: JSON-LD Schemas That Drive Citations</span></div><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right text-muted-foreground shrink-0 group-hover:text-primary group-hover:translate-x-1 transition-all"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></a><a href="/blog/what-is-an-ai-agent" class="flex items-center justify-between gap-3 rounded-lg border px-4 py-3 text-sm transition-colors group border-border bg-card hover:bg-accent/50"><div class="min-w-0"><span class="text-xs font-bold uppercase tracking-widest text-primary mr-2">Agentic AI</span><span class="font-medium group-hover:text-primary transition-colors text-foreground">What Makes an AI System an Agent? The Foundation of Agentic Design</span></div><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right text-muted-foreground shrink-0 group-hover:text-primary group-hover:translate-x-1 transition-all"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></a></div></section></div><div class="not-prose"><section class="my-12"><div class="flex items-end justify-between gap-4 mb-6 flex-wrap"><div><span class="annotation-purple text-xl block mb-2">Want to verify?</span><h2 class="text-2xl md:text-3xl font-bold tracking-tight text-[#0a0a0a]">Industry sources we cite.</h2></div><p class="text-xs font-bold uppercase tracking-widest text-[#0a0a0a]/40">6<!-- --> link<!-- -->s<!-- --> · External</p></div><ul class="grid grid-cols-1 md:grid-cols-2 gap-3 list-none p-0 m-0"><li><a href="https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics" target="_blank" rel="noopener noreferrer" class="group flex items-start gap-3 rounded-xl bg-white ring-1 ring-[#0a0a0a]/10 hover:ring-[#0a0a0a]/25 hover:bg-[#0a0a0a]/[0.02] p-4 transition-all"><span class="shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-lg bg-violet/10 text-violet group-hover:bg-violet group-hover:text-white transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-open h-4 w-4"><path d="M12 7v14"></path><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"></path></svg></span><div class="min-w-0 flex-1"><p class="text-[10px] font-bold uppercase tracking-widest text-[#0a0a0a]/40 mb-1 truncate">Google Search Central</p><p class="text-sm font-semibold text-[#0a0a0a] group-hover:text-violet transition-colors leading-snug">Understand JavaScript SEO Basics</p></div><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right shrink-0 text-[#0a0a0a]/30 group-hover:text-violet group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-all" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a></li><li><a href="https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript" target="_blank" rel="noopener noreferrer" class="group flex items-start gap-3 rounded-xl bg-white ring-1 ring-[#0a0a0a]/10 hover:ring-[#0a0a0a]/25 hover:bg-[#0a0a0a]/[0.02] p-4 transition-all"><span class="shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-lg bg-violet/10 text-violet group-hover:bg-violet group-hover:text-white transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-open h-4 w-4"><path d="M12 7v14"></path><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"></path></svg></span><div class="min-w-0 flex-1"><p class="text-[10px] font-bold uppercase tracking-widest text-[#0a0a0a]/40 mb-1 truncate">Google Search Central</p><p class="text-sm font-semibold text-[#0a0a0a] group-hover:text-violet transition-colors leading-snug">Fix Search-Related JavaScript Problems</p></div><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right shrink-0 text-[#0a0a0a]/30 group-hover:text-violet group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-all" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a></li><li><a href="https://developers.google.com/search/docs/crawling-indexing/javascript/lazy-loading" target="_blank" rel="noopener noreferrer" class="group flex items-start gap-3 rounded-xl bg-white ring-1 ring-[#0a0a0a]/10 hover:ring-[#0a0a0a]/25 hover:bg-[#0a0a0a]/[0.02] p-4 transition-all"><span class="shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-lg bg-violet/10 text-violet group-hover:bg-violet group-hover:text-white transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-open h-4 w-4"><path d="M12 7v14"></path><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"></path></svg></span><div class="min-w-0 flex-1"><p class="text-[10px] font-bold uppercase tracking-widest text-[#0a0a0a]/40 mb-1 truncate">Google Search Central</p><p class="text-sm font-semibold text-[#0a0a0a] group-hover:text-violet transition-colors leading-snug">Fix lazy-loaded content</p></div><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right shrink-0 text-[#0a0a0a]/30 group-hover:text-violet group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-all" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a></li><li><a href="https://developers.google.com/search/blog/2019/05/the-new-evergreen-googlebot" target="_blank" rel="noopener noreferrer" class="group flex items-start gap-3 rounded-xl bg-white ring-1 ring-[#0a0a0a]/10 hover:ring-[#0a0a0a]/25 hover:bg-[#0a0a0a]/[0.02] p-4 transition-all"><span class="shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-lg bg-violet/10 text-violet group-hover:bg-violet group-hover:text-white transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-open h-4 w-4"><path d="M12 7v14"></path><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"></path></svg></span><div class="min-w-0 flex-1"><p class="text-[10px] font-bold uppercase tracking-widest text-[#0a0a0a]/40 mb-1 truncate">Google Search Central Blog</p><p class="text-sm font-semibold text-[#0a0a0a] group-hover:text-violet transition-colors leading-snug">The new evergreen Googlebot</p></div><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right shrink-0 text-[#0a0a0a]/30 group-hover:text-violet group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-all" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a></li><li><a href="https://search-off-the-record.libsyn.com/how-browsers-really-parse-html-and-what-that-means-for-seo-transcript" target="_blank" rel="noopener noreferrer" class="group flex items-start gap-3 rounded-xl bg-white ring-1 ring-[#0a0a0a]/10 hover:ring-[#0a0a0a]/25 hover:bg-[#0a0a0a]/[0.02] p-4 transition-all"><span class="shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-lg bg-violet/10 text-violet group-hover:bg-violet group-hover:text-white transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-open h-4 w-4"><path d="M12 7v14"></path><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"></path></svg></span><div class="min-w-0 flex-1"><p class="text-[10px] font-bold uppercase tracking-widest text-[#0a0a0a]/40 mb-1 truncate">Search Off the Record</p><p class="text-sm font-semibold text-[#0a0a0a] group-hover:text-violet transition-colors leading-snug">How Browsers Really Parse HTML (SOTR 105)</p></div><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right shrink-0 text-[#0a0a0a]/30 group-hover:text-violet group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-all" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a></li><li><a href="https://developers.google.com/search/blog/2015/10/deprecating-our-ajax-crawling-scheme" target="_blank" rel="noopener noreferrer" class="group flex items-start gap-3 rounded-xl bg-white ring-1 ring-[#0a0a0a]/10 hover:ring-[#0a0a0a]/25 hover:bg-[#0a0a0a]/[0.02] p-4 transition-all"><span class="shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-lg bg-violet/10 text-violet group-hover:bg-violet group-hover:text-white transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-open h-4 w-4"><path d="M12 7v14"></path><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"></path></svg></span><div class="min-w-0 flex-1"><p class="text-[10px] font-bold uppercase tracking-widest text-[#0a0a0a]/40 mb-1 truncate">Google Search Central Blog</p><p class="text-sm font-semibold text-[#0a0a0a] group-hover:text-violet transition-colors leading-snug">Deprecating AJAX crawling scheme</p></div><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right shrink-0 text-[#0a0a0a]/30 group-hover:text-violet group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-all" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a></li></ul></section></div><section class="not-prose mt-16 pt-12 border-t border-[#0a0a0a]/10" aria-label="Frequently Asked Questions"><span class="annotation-purple text-xl block mb-3">Quick answers</span><h2 class="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tighter leading-[1] text-[#0a0a0a] mb-8">Frequently asked.</h2><div class="w-full space-y-3" data-orientation="vertical"><div data-state="closed" data-orientation="vertical" class="rounded-xl bg-white ring-1 ring-[#0a0a0a]/10 hover:ring-[#0a0a0a]/20 transition-all border-b-0 overflow-hidden"><h3 data-orientation="vertical" data-state="closed" class="flex"><button type="button" aria-controls="radix-_R_4vj6uaq_" aria-expanded="false" data-state="closed" data-orientation="vertical" id="radix-_R_vj6uaq_" class="flex flex-1 items-center justify-between transition-all [&[data-state=open]>svg]:rotate-180 px-5 py-4 text-left text-base md:text-lg font-semibold text-[#0a0a0a] hover:no-underline hover:bg-[#0a0a0a]/[0.02]" data-radix-collection-item="">Does Googlebot render JavaScript in 2026?<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down h-4 w-4 shrink-0 transition-transform duration-200"><path d="m6 9 6 6 6-6"></path></svg></button></h3><div data-state="closed" id="radix-_R_4vj6uaq_" hidden="" role="region" aria-labelledby="radix-_R_vj6uaq_" data-orientation="vertical" class="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" style="--radix-accordion-content-height:var(--radix-collapsible-content-height);--radix-accordion-content-width:var(--radix-collapsible-content-width)"></div></div><div data-state="closed" data-orientation="vertical" class="rounded-xl bg-white ring-1 ring-[#0a0a0a]/10 hover:ring-[#0a0a0a]/20 transition-all border-b-0 overflow-hidden"><h3 data-orientation="vertical" data-state="closed" class="flex"><button type="button" aria-controls="radix-_R_5fj6uaq_" aria-expanded="false" data-state="closed" data-orientation="vertical" id="radix-_R_1fj6uaq_" class="flex flex-1 items-center justify-between transition-all [&[data-state=open]>svg]:rotate-180 px-5 py-4 text-left text-base md:text-lg font-semibold text-[#0a0a0a] hover:no-underline hover:bg-[#0a0a0a]/[0.02]" data-radix-collection-item="">Can Google index a single-page app?<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down h-4 w-4 shrink-0 transition-transform duration-200"><path d="m6 9 6 6 6-6"></path></svg></button></h3><div data-state="closed" id="radix-_R_5fj6uaq_" hidden="" role="region" aria-labelledby="radix-_R_1fj6uaq_" data-orientation="vertical" class="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" style="--radix-accordion-content-height:var(--radix-collapsible-content-height);--radix-accordion-content-width:var(--radix-collapsible-content-width)"></div></div><div data-state="closed" data-orientation="vertical" class="rounded-xl bg-white ring-1 ring-[#0a0a0a]/10 hover:ring-[#0a0a0a]/20 transition-all border-b-0 overflow-hidden"><h3 data-orientation="vertical" data-state="closed" class="flex"><button type="button" aria-controls="radix-_R_5vj6uaq_" aria-expanded="false" data-state="closed" data-orientation="vertical" id="radix-_R_1vj6uaq_" class="flex flex-1 items-center justify-between transition-all [&[data-state=open]>svg]:rotate-180 px-5 py-4 text-left text-base md:text-lg font-semibold text-[#0a0a0a] hover:no-underline hover:bg-[#0a0a0a]/[0.02]" data-radix-collection-item="">Should I inject a canonical tag via JavaScript?<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down h-4 w-4 shrink-0 transition-transform duration-200"><path d="m6 9 6 6 6-6"></path></svg></button></h3><div data-state="closed" id="radix-_R_5vj6uaq_" hidden="" role="region" aria-labelledby="radix-_R_1vj6uaq_" data-orientation="vertical" class="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" style="--radix-accordion-content-height:var(--radix-collapsible-content-height);--radix-accordion-content-width:var(--radix-collapsible-content-width)"></div></div><div data-state="closed" data-orientation="vertical" class="rounded-xl bg-white ring-1 ring-[#0a0a0a]/10 hover:ring-[#0a0a0a]/20 transition-all border-b-0 overflow-hidden"><h3 data-orientation="vertical" data-state="closed" class="flex"><button type="button" aria-controls="radix-_R_6fj6uaq_" aria-expanded="false" data-state="closed" data-orientation="vertical" id="radix-_R_2fj6uaq_" class="flex flex-1 items-center justify-between transition-all [&[data-state=open]>svg]:rotate-180 px-5 py-4 text-left text-base md:text-lg font-semibold text-[#0a0a0a] hover:no-underline hover:bg-[#0a0a0a]/[0.02]" data-radix-collection-item="">Does Googlebot scroll or click?<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down h-4 w-4 shrink-0 transition-transform duration-200"><path d="m6 9 6 6 6-6"></path></svg></button></h3><div data-state="closed" id="radix-_R_6fj6uaq_" hidden="" role="region" aria-labelledby="radix-_R_2fj6uaq_" data-orientation="vertical" class="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" style="--radix-accordion-content-height:var(--radix-collapsible-content-height);--radix-accordion-content-width:var(--radix-collapsible-content-width)"></div></div><div data-state="closed" data-orientation="vertical" class="rounded-xl bg-white ring-1 ring-[#0a0a0a]/10 hover:ring-[#0a0a0a]/20 transition-all border-b-0 overflow-hidden"><h3 data-orientation="vertical" data-state="closed" class="flex"><button type="button" aria-controls="radix-_R_6vj6uaq_" aria-expanded="false" data-state="closed" data-orientation="vertical" id="radix-_R_2vj6uaq_" class="flex flex-1 items-center justify-between transition-all [&[data-state=open]>svg]:rotate-180 px-5 py-4 text-left text-base md:text-lg font-semibold text-[#0a0a0a] hover:no-underline hover:bg-[#0a0a0a]/[0.02]" data-radix-collection-item="">What is the two-pass rendering model?<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down h-4 w-4 shrink-0 transition-transform duration-200"><path d="m6 9 6 6 6-6"></path></svg></button></h3><div data-state="closed" id="radix-_R_6vj6uaq_" hidden="" role="region" aria-labelledby="radix-_R_2vj6uaq_" data-orientation="vertical" class="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down" style="--radix-accordion-content-height:var(--radix-collapsible-content-height);--radix-accordion-content-width:var(--radix-collapsible-content-width)"></div></div></div></section></article><div class="mt-16 pt-12 border-t border-[#0a0a0a]/10"><section aria-label="Related articles"><span class="annotation-purple text-xl block mb-3">Keep reading</span><h2 class="text-3xl md:text-4xl lg:text-5xl font-bold tracking-tighter leading-[1] text-[#0a0a0a] mb-8">Continue with.</h2><div class="grid grid-cols-1 md:grid-cols-3 gap-4"><a href="/blog/googlebot-2mb-byte-limit" style="transition-delay:0ms" class="group flex flex-col h-full bg-white rounded-xl p-6 ring-1 ring-[#0a0a0a]/10 hover:ring-[#0a0a0a]/25 hover:bg-[#0a0a0a]/[0.02] transition-all"><p class="text-[10px] font-bold uppercase tracking-widest text-violet mb-3">AEO & AI Search</p><h3 class="text-base md:text-lg font-bold tracking-tight text-[#0a0a0a] group-hover:text-violet transition-colors leading-snug mb-2">The 2MB rule: how Googlebot truncates your page</h3><p class="text-sm text-[#0a0a0a]/55 leading-relaxed line-clamp-3 mb-4">Googlebot fetches up to 2 MB of HTML per URL for Search. Anything past that is silently truncated, and most sites have no idea</p><div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-[#0a0a0a]/45 pt-4 border-t border-[#0a0a0a]/10 mt-auto"><span class="flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar h-3 w-3"><path d="M8 2v4"></path><path d="M16 2v4"></path><rect width="18" height="18" x="3" y="4" rx="2"></rect><path d="M3 10h18"></path></svg>May 26, 2026</span><span class="text-[#0a0a0a]/20">·</span><span class="flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock h-3 w-3"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>6<!-- -->m</span><span class="ml-auto inline-flex items-center gap-1 text-xs font-semibold text-violet group-hover:translate-x-0.5 transition-transform">Read <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right h-3 w-3"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></span></div></a><a href="/blog/core-web-vitals-2026-actually" style="transition-delay:60ms" class="group flex flex-col h-full bg-white rounded-xl p-6 ring-1 ring-[#0a0a0a]/10 hover:ring-[#0a0a0a]/25 hover:bg-[#0a0a0a]/[0.02] transition-all"><p class="text-[10px] font-bold uppercase tracking-widest text-violet mb-3">AEO & AI Search</p><h3 class="text-base md:text-lg font-bold tracking-tight text-[#0a0a0a] group-hover:text-violet transition-colors leading-snug mb-2">Core Web Vitals in 2026: what actually changes your score</h3><p class="text-sm text-[#0a0a0a]/55 leading-relaxed line-clamp-3 mb-4">Three metrics. Three thresholds. One 75th-percentile rule most teams misread. Here's what actually changes Core Web Vitals on a real site</p><div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-[#0a0a0a]/45 pt-4 border-t border-[#0a0a0a]/10 mt-auto"><span class="flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar h-3 w-3"><path d="M8 2v4"></path><path d="M16 2v4"></path><rect width="18" height="18" x="3" y="4" rx="2"></rect><path d="M3 10h18"></path></svg>May 26, 2026</span><span class="text-[#0a0a0a]/20">·</span><span class="flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock h-3 w-3"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>7<!-- -->m</span><span class="ml-auto inline-flex items-center gap-1 text-xs font-semibold text-violet group-hover:translate-x-0.5 transition-transform">Read <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right h-3 w-3"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></span></div></a><a href="/blog/canonicalization-stop-fighting-google" style="transition-delay:120ms" class="group flex flex-col h-full bg-white rounded-xl p-6 ring-1 ring-[#0a0a0a]/10 hover:ring-[#0a0a0a]/25 hover:bg-[#0a0a0a]/[0.02] transition-all"><p class="text-[10px] font-bold uppercase tracking-widest text-violet mb-3">AEO & AI Search</p><h3 class="text-base md:text-lg font-bold tracking-tight text-[#0a0a0a] group-hover:text-violet transition-colors leading-snug mb-2">Canonicalization: stop fighting Google's canonical picker</h3><p class="text-sm text-[#0a0a0a]/55 leading-relaxed line-clamp-3 mb-4">rel=canonical is a hint, not a directive. How Google actually picks the canonical URL, and the three signals that reliably move the decision</p><div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-[#0a0a0a]/45 pt-4 border-t border-[#0a0a0a]/10 mt-auto"><span class="flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar h-3 w-3"><path d="M8 2v4"></path><path d="M16 2v4"></path><rect width="18" height="18" x="3" y="4" rx="2"></rect><path d="M3 10h18"></path></svg>May 26, 2026</span><span class="text-[#0a0a0a]/20">·</span><span class="flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock h-3 w-3"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>7<!-- -->m</span><span class="ml-auto inline-flex items-center gap-1 text-xs font-semibold text-violet group-hover:translate-x-0.5 transition-transform">Read <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right h-3 w-3"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></span></div></a></div></section></div></div></article></div></div></main><footer class="bg-warm text-warm-dark pt-16 md:pt-24 pb-0 overflow-hidden border-t border-[#0a0a0a]/10"><div class="container mx-auto px-4 md:px-8 max-w-6xl"><div class="grid grid-cols-1 lg:grid-cols-12 gap-10 mb-16"><div class="lg:col-span-5"><a href="/" class="inline-flex items-center gap-2 text-[#0a0a0a] font-extrabold text-xl tracking-tight uppercase mb-4"><svg viewBox="0 0 100 100" class="w-6 h-6 text-[#0a0a0a]" fill="none" role="img" aria-label="Space & Story mark"><path d="M1800.4 1185.66L1646.22 1167.75L1742.34 1046.27H1939.77L1800.4 1185.66ZM1800.4 1430.33L1646.22 1448.25L1742.34 1569.73H1939.77L1800.4 1430.33ZM1167.75 1646.25L1046.27 1742.37V1939.86L1185.7 1800.47L1167.75 1646.25V1646.25ZM1728.35 1728.39V2098.54L1757.94 2128.14L1935.9 2107.42L2089.47 2089.58L2107.32 1935.94L2128.05 1757.99L2098.47 1728.39H1728.35L1728.35 1728.39ZM887.593 2098.54V1728.39H517.496L487.912 1757.99L508.623 1935.93L526.48 2089.58L680.065 2107.41L858.026 2128.14L887.593 2098.54V2098.54ZM969.747 1448.25L815.588 1430.33L676.174 1569.73H873.609L969.749 1448.25H969.747ZM1569.71 2164.28L1481.89 2076.44L1307.98 1902.56L1134.04 2076.44L1046.27 2164.28V2206.35L1186.45 2317.33L1307.98 2413.51L1429.54 2317.33L1569.71 2206.35V2164.28H1569.71ZM1448.23 1646.25L1430.28 1800.47L1569.71 1939.86V1742.38L1448.23 1646.26V1646.25ZM2615 1308L2576.19 1357.02L2441.23 1527.57L2306.24 1698.07L2292.53 1715.39L2289.9 1737.93L2264.76 1953.92L2239.63 2169.95L2232.36 2232.44L2169.92 2239.7L1953.89 2264.85L1737.91 2290.01L1715.29 2292.62L1697.97 2306.35L1527.46 2441.3L1356.94 2576.3L1307.98 2615L1259.02 2576.3L1088.49 2441.3L917.989 2306.35L900.67 2292.62L878.07 2290.01L662.058 2264.85L446.047 2239.7L383.584 2232.44L376.34 2169.95L351.183 1953.92L326.063 1737.93L323.416 1715.39L309.709 1698.07L174.722 1527.57L39.7908 1357.02L1 1308L39.7908 1258.98L174.722 1088.43L309.709 917.934L323.416 900.614L326.063 878.07L351.183 662.077L376.34 446.046L383.584 383.564L446.047 376.302L662.058 351.146L878.07 325.991L900.67 323.379L917.989 309.652L1088.49 174.703L1259.02 39.6981L1307.98 1L1356.94 39.6981L1527.46 174.703L1697.97 309.652L1715.29 323.379L1737.91 325.991L1953.89 351.146L2169.92 376.302L2232.36 383.564L2239.63 446.046L2264.76 662.077L2289.9 878.07L2292.53 900.614L2306.24 917.934L2441.23 1088.43L2576.19 1258.98L2615 1308ZM202.491 1308L298.687 1429.56L409.648 1569.73H451.791L539.523 1481.96L713.502 1308L539.523 1134.04L451.791 1046.27H409.648L298.687 1186.44L202.491 1308ZM943.776 1308L1099.7 1394.32L1050.44 1565.62L1221.71 1516.34L1307.98 1672.26L1394.29 1516.34L1565.55 1565.62L1516.27 1394.32L1672.24 1308L1516.27 1221.68L1565.55 1050.38L1394.29 1099.66L1307.98 943.738L1221.71 1099.66L1050.44 1050.38L1099.7 1221.68L943.776 1308ZM1902.48 1308L2076.42 1481.96L2164.15 1569.73H2206.34L2317.24 1429.56L2413.47 1308L2317.24 1186.44L2206.34 1046.27H2164.15L2076.42 1134.04L1902.48 1308ZM1167.75 969.746L1046.27 873.625V676.138L1185.7 815.533L1167.75 969.747V969.746ZM1728.35 887.61V517.458L1757.94 487.856L1935.9 508.583L2089.47 526.422L2107.32 680.063L2128.05 858.007L2098.47 887.609H1728.35V887.61ZM887.593 517.458V887.61H517.496L487.912 858.008L508.621 680.064L526.478 526.423L680.063 508.584L858.026 487.857L887.593 517.459V517.458ZM969.747 1167.75L815.588 1185.66L676.174 1046.27H873.607L969.747 1167.75ZM1569.71 451.714L1481.89 539.556L1307.98 713.443L1134.04 539.556L1046.27 451.714V409.645L1186.44 298.665L1307.98 202.488L1429.54 298.665L1569.71 409.645V451.714ZM1448.23 969.745L1430.28 815.531L1569.71 676.135V873.623L1448.23 969.744V969.745Z" transform="scale(0.038226)" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>Space<span class="text-violet">And</span>Story.Co</a><p class="text-[#0a0a0a]/60 text-base leading-relaxed max-w-md mb-5">$100K Lovable Grand Prize winner. We rebuild small business websites to look better, load faster, and get cited by ChatGPT, Perplexity, and Google AI Overviews, so the right customers find you first.</p><a href="mailto:info@spaceandstory.co" class="inline-block text-sm font-semibold text-violet hover:text-[#0a0a0a] transition-colors">info@spaceandstory.co</a></div><div class="lg:col-span-7 grid grid-cols-2 sm:grid-cols-4 gap-6"><div><a href="/pricing" class="group inline-flex items-center gap-1.5 mb-3"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-briefcase w-3 h-3 text-violet group-hover:text-[#0a0a0a] transition-colors"><path d="M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path><rect width="20" height="14" x="2" y="6" rx="2"></rect></svg><span class="text-[10px] font-bold uppercase tracking-widest text-[#0a0a0a]/40 group-hover:text-[#0a0a0a] transition-colors">Build</span></a><ul class="space-y-2.5"><li><a href="/solutions" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Site — $2,500</a></li><li><a href="/solutions" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Story — $5,000</a></li><li><a href="/solutions" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Engine — $9,000</a></li><li><a href="/solutions" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Studio — $15,000+</a></li></ul></div><div><a href="/pricing" class="group inline-flex items-center gap-1.5 mb-3"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wallet w-3 h-3 text-violet group-hover:text-[#0a0a0a] transition-colors"><path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1"></path><path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4"></path></svg><span class="text-[10px] font-bold uppercase tracking-widest text-[#0a0a0a]/40 group-hover:text-[#0a0a0a] transition-colors">Retainers</span></a><ul class="space-y-2.5"><li><a href="/solutions" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Care — $60/mo</a></li><li><a href="/solutions" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Local — $300/mo</a></li><li><a href="/solutions" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Growth — $700/mo</a></li><li><a href="/solutions" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Authority — $2,000+/mo</a></li></ul></div><div><a href="/for/dentists" class="group inline-flex items-center gap-1.5 mb-3"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-users w-3 h-3 text-violet group-hover:text-[#0a0a0a] transition-colors"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M22 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg><span class="text-[10px] font-bold uppercase tracking-widest text-[#0a0a0a]/40 group-hover:text-[#0a0a0a] transition-colors">Industries</span></a><ul class="space-y-1.5"><li><a href="/for/dentists" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Dentists</a></li><li><a href="/for/lawyers" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Lawyers</a></li><li><a href="/for/contractors" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Contractors</a></li><li><a href="/for/real-estate" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Real Estate</a></li><li><a href="/for/home-builders" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Home Builders</a></li><li><a href="/for/av-installers" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">AV Install</a></li><li><a href="/for/accountants" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Accountants</a></li><li><a href="/for/fitness-studios" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Fitness Studios</a></li><li><a href="/for/vets" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Vets</a></li><li><a href="/for/boutique-retail" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Boutique Retail</a></li><li><a href="/for/auto-shops" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Auto Shops</a></li><li><a href="/for/restaurants" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Restaurants</a></li><li><a href="/for/schools" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Schools</a></li></ul></div><div><a href="/pricing" class="group inline-flex items-center gap-1.5 mb-3"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-open w-3 h-3 text-violet group-hover:text-[#0a0a0a] transition-colors"><path d="M12 7v14"></path><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"></path></svg><span class="text-[10px] font-bold uppercase tracking-widest text-[#0a0a0a]/40 group-hover:text-[#0a0a0a] transition-colors">Pricing & guides</span></a><ul class="space-y-2.5"><li><a href="/pricing" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">All pricing</a></li><li><a href="/compare/website-cost" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Website cost</a></li><li><a href="/compare/seo-cost" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">SEO cost</a></li><li><a href="/compare/aeo-cost" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">AEO cost</a></li><li><a href="/work" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">Case Studies</a></li><li><a class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors active" href="/blog" data-status="active" aria-current="page">Blog</a></li><li><a href="/faq" class="text-sm text-[#0a0a0a]/60 hover:text-[#0a0a0a] transition-colors">FAQ</a></li></ul></div></div></div><p class="text-xs text-[#0a0a0a]/40 mb-12">Build and retainer prices shown exclude applicable taxes.</p><div class="grid grid-cols-1 md:grid-cols-3 gap-6 pb-12 mb-12 border-b border-[#0a0a0a]/10"><div><p class="text-[10px] font-bold uppercase tracking-widest text-[#0a0a0a]/40 mb-3">Built for</p><p class="text-sm text-[#0a0a0a]/60 leading-relaxed">Local service businesses tired of paying their SEO agency for reports nobody reads. Dentists, lawyers, contractors, home builders, clinics, fitness studios, accountants, med spas.</p></div><div><p class="text-[10px] font-bold uppercase tracking-widest text-[#0a0a0a]/40 mb-3">What makes us different</p><p class="text-sm text-[#0a0a0a]/60 leading-relaxed">AEO is included at every retainer tier. Hosting floor is $60 a month. Rule of the house: we always quote below what you're currently paying. The 24-page audit is free.</p></div><div><p class="text-[10px] font-bold uppercase tracking-widest text-[#0a0a0a]/40 mb-3">Recognition</p><p class="text-sm text-[#0a0a0a]/60 leading-relaxed">Winner of the $100K Lovable Shipped Season 1 Grand Prize against 5,800 builders. Patent co-author on AI orchestration (WO2025147762A1). Toronto-built.</p></div></div><div class="flex flex-col md:flex-row items-start md:items-center justify-between gap-8 pb-12 mb-12 border-b border-[#0a0a0a]/10"><div class="flex flex-wrap items-center gap-2"><a href="/solutions" class="text-[10px] font-bold uppercase tracking-wider px-3 py-1.5 rounded-lg ring-1 ring-[#0a0a0a]/10 text-[#0a0a0a]/60 hover:text-[#0a0a0a] hover:ring-[#0a0a0a]/20 transition-all">All Solutions</a><a href="/pricing" class="text-[10px] font-bold uppercase tracking-wider px-3 py-1.5 rounded-lg ring-1 ring-[#0a0a0a]/10 text-[#0a0a0a]/60 hover:text-[#0a0a0a] hover:ring-[#0a0a0a]/20 transition-all">Pricing</a><a href="/for/dentists" class="text-[10px] font-bold uppercase tracking-wider px-3 py-1.5 rounded-lg ring-1 ring-[#0a0a0a]/10 text-[#0a0a0a]/60 hover:text-[#0a0a0a] hover:ring-[#0a0a0a]/20 transition-all">Industries</a><a href="/audit" class="text-[10px] font-bold uppercase tracking-wider px-3 py-1.5 rounded-lg ring-1 ring-[#0a0a0a]/10 text-[#0a0a0a]/60 hover:text-[#0a0a0a] hover:ring-[#0a0a0a]/20 transition-all">Audit My Site</a><a href="/work" class="text-[10px] font-bold uppercase tracking-wider px-3 py-1.5 rounded-lg ring-1 ring-[#0a0a0a]/10 text-[#0a0a0a]/60 hover:text-[#0a0a0a] hover:ring-[#0a0a0a]/20 transition-all">Case Studies</a><a href="/faq" class="text-[10px] font-bold uppercase tracking-wider px-3 py-1.5 rounded-lg ring-1 ring-[#0a0a0a]/10 text-[#0a0a0a]/60 hover:text-[#0a0a0a] hover:ring-[#0a0a0a]/20 transition-all">FAQ</a><a href="https://s1-showcase.lovable.app/" target="_blank" rel="noopener noreferrer" class="text-[10px] font-bold uppercase tracking-wider px-3 py-1.5 rounded-lg bg-violet/10 text-violet hover:bg-violet/20 transition-colors inline-flex items-center gap-1">Lovable Shipped #1<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-external-link w-2.5 h-2.5"><path d="M15 3h6v6"></path><path d="M10 14 21 3"></path><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path></svg></a><a href="https://www.youtube.com/live/XrCdc9fjKpM?t=10986s" target="_blank" rel="noopener noreferrer" class="text-[10px] font-bold uppercase tracking-wider px-3 py-1.5 rounded-lg bg-violet/10 text-violet hover:bg-violet/20 transition-colors inline-flex items-center gap-1">Startup Creasphere<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-external-link w-2.5 h-2.5"><path d="M15 3h6v6"></path><path d="M10 14 21 3"></path><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path></svg></a><a href="https://www.youtube.com/watch?v=oTpAf67XBLk" target="_blank" rel="noopener noreferrer" class="text-[10px] font-bold uppercase tracking-wider px-3 py-1.5 rounded-lg bg-violet/10 text-violet hover:bg-violet/20 transition-colors inline-flex items-center gap-1">Podcast Feature<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-external-link w-2.5 h-2.5"><path d="M15 3h6v6"></path><path d="M10 14 21 3"></path><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path></svg></a><a href="https://www.linkedin.com/posts/torontob2bmarketingcommunity_meet-mahmoud-halat-director-of-product-activity-7399180021476667392-N9E0/" target="_blank" rel="noopener noreferrer" class="text-[10px] font-bold uppercase tracking-wider px-3 py-1.5 rounded-lg bg-violet/10 text-violet hover:bg-violet/20 transition-colors inline-flex items-center gap-1">B2B Marketing<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-external-link w-2.5 h-2.5"><path d="M15 3h6v6"></path><path d="M10 14 21 3"></path><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path></svg></a></div><div class="flex items-center gap-2"><a href="https://www.linkedin.com/in/mahmoud-halat/" target="_blank" rel="noopener noreferrer" aria-label="LinkedIn" class="w-10 h-10 rounded-lg ring-1 ring-[#0a0a0a]/10 hover:ring-[#0a0a0a]/30 flex items-center justify-center text-[#0a0a0a]/50 hover:text-[#0a0a0a] transition-all"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"></path></svg></a><a href="https://www.instagram.com/spaceandstory.co/" target="_blank" rel="noopener noreferrer" aria-label="Instagram" class="w-10 h-10 rounded-lg ring-1 ring-[#0a0a0a]/10 hover:ring-[#0a0a0a]/30 flex items-center justify-center text-[#0a0a0a]/50 hover:text-[#0a0a0a] transition-all"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"></path></svg></a></div></div><div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6 pb-16"><div class="flex items-center gap-4"><svg viewBox="0 0 100 100" class="hero-mark-spin w-12 h-12 shrink-0 text-violet" fill="none" role="img" aria-label="Space & Story mark"><path d="M1800.4 1185.66L1646.22 1167.75L1742.34 1046.27H1939.77L1800.4 1185.66ZM1800.4 1430.33L1646.22 1448.25L1742.34 1569.73H1939.77L1800.4 1430.33ZM1167.75 1646.25L1046.27 1742.37V1939.86L1185.7 1800.47L1167.75 1646.25V1646.25ZM1728.35 1728.39V2098.54L1757.94 2128.14L1935.9 2107.42L2089.47 2089.58L2107.32 1935.94L2128.05 1757.99L2098.47 1728.39H1728.35L1728.35 1728.39ZM887.593 2098.54V1728.39H517.496L487.912 1757.99L508.623 1935.93L526.48 2089.58L680.065 2107.41L858.026 2128.14L887.593 2098.54V2098.54ZM969.747 1448.25L815.588 1430.33L676.174 1569.73H873.609L969.749 1448.25H969.747ZM1569.71 2164.28L1481.89 2076.44L1307.98 1902.56L1134.04 2076.44L1046.27 2164.28V2206.35L1186.45 2317.33L1307.98 2413.51L1429.54 2317.33L1569.71 2206.35V2164.28H1569.71ZM1448.23 1646.25L1430.28 1800.47L1569.71 1939.86V1742.38L1448.23 1646.26V1646.25ZM2615 1308L2576.19 1357.02L2441.23 1527.57L2306.24 1698.07L2292.53 1715.39L2289.9 1737.93L2264.76 1953.92L2239.63 2169.95L2232.36 2232.44L2169.92 2239.7L1953.89 2264.85L1737.91 2290.01L1715.29 2292.62L1697.97 2306.35L1527.46 2441.3L1356.94 2576.3L1307.98 2615L1259.02 2576.3L1088.49 2441.3L917.989 2306.35L900.67 2292.62L878.07 2290.01L662.058 2264.85L446.047 2239.7L383.584 2232.44L376.34 2169.95L351.183 1953.92L326.063 1737.93L323.416 1715.39L309.709 1698.07L174.722 1527.57L39.7908 1357.02L1 1308L39.7908 1258.98L174.722 1088.43L309.709 917.934L323.416 900.614L326.063 878.07L351.183 662.077L376.34 446.046L383.584 383.564L446.047 376.302L662.058 351.146L878.07 325.991L900.67 323.379L917.989 309.652L1088.49 174.703L1259.02 39.6981L1307.98 1L1356.94 39.6981L1527.46 174.703L1697.97 309.652L1715.29 323.379L1737.91 325.991L1953.89 351.146L2169.92 376.302L2232.36 383.564L2239.63 446.046L2264.76 662.077L2289.9 878.07L2292.53 900.614L2306.24 917.934L2441.23 1088.43L2576.19 1258.98L2615 1308ZM202.491 1308L298.687 1429.56L409.648 1569.73H451.791L539.523 1481.96L713.502 1308L539.523 1134.04L451.791 1046.27H409.648L298.687 1186.44L202.491 1308ZM943.776 1308L1099.7 1394.32L1050.44 1565.62L1221.71 1516.34L1307.98 1672.26L1394.29 1516.34L1565.55 1565.62L1516.27 1394.32L1672.24 1308L1516.27 1221.68L1565.55 1050.38L1394.29 1099.66L1307.98 943.738L1221.71 1099.66L1050.44 1050.38L1099.7 1221.68L943.776 1308ZM1902.48 1308L2076.42 1481.96L2164.15 1569.73H2206.34L2317.24 1429.56L2413.47 1308L2317.24 1186.44L2206.34 1046.27H2164.15L2076.42 1134.04L1902.48 1308ZM1167.75 969.746L1046.27 873.625V676.138L1185.7 815.533L1167.75 969.747V969.746ZM1728.35 887.61V517.458L1757.94 487.856L1935.9 508.583L2089.47 526.422L2107.32 680.063L2128.05 858.007L2098.47 887.609H1728.35V887.61ZM887.593 517.458V887.61H517.496L487.912 858.008L508.621 680.064L526.478 526.423L680.063 508.584L858.026 487.857L887.593 517.459V517.458ZM969.747 1167.75L815.588 1185.66L676.174 1046.27H873.607L969.747 1167.75ZM1569.71 451.714L1481.89 539.556L1307.98 713.443L1134.04 539.556L1046.27 451.714V409.645L1186.44 298.665L1307.98 202.488L1429.54 298.665L1569.71 409.645V451.714ZM1448.23 969.745L1430.28 815.531L1569.71 676.135V873.623L1448.23 969.744V969.745Z" transform="scale(0.038226)" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg><div><p class="text-xl md:text-2xl font-bold tracking-tight text-[#0a0a0a]">Ready to be the answer AI gives?</p><p class="text-sm text-[#0a0a0a]/55 mt-1">Free 24-page AEO audit. No signup, no card. We just email it to you.</p></div></div><a href="/audit" class="inline-flex shrink-0 items-center gap-2 px-7 py-3.5 rounded-lg bg-[#0a0a0a] text-white text-sm font-semibold hover:bg-[#0a0a0a]/90 transition-colors">Get a free audit <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right h-4 w-4"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></a></div></div><div class="relative overflow-hidden"><svg viewBox="0 0 1600 200" class="w-full h-auto text-[#0a0a0a]" fill="currentColor" aria-hidden="true"><text x="50%" y="78%" dominant-baseline="middle" text-anchor="middle" style="font-size:210px;font-family:Inter, system-ui, sans-serif;font-weight:700;letter-spacing:-0.05em"><tspan>SPACE</tspan><tspan fill="#7C5CFC">AND</tspan><tspan>STORY</tspan></text></svg></div><div class="bg-warm border-t border-[#0a0a0a]/10"><div class="container mx-auto max-w-6xl px-4 md:px-8 py-5 flex flex-col sm:flex-row items-center justify-between gap-4"><div class="flex items-center gap-2 flex-wrap"><a href="/privacy" class="text-[10px] font-bold uppercase tracking-wider px-3 py-1 rounded-lg ring-1 ring-[#0a0a0a]/10 text-[#0a0a0a]/40 hover:text-[#0a0a0a]/70 hover:ring-[#0a0a0a]/20 transition-all">Privacy</a><a href="/terms" class="text-[10px] font-bold uppercase tracking-wider px-3 py-1 rounded-lg ring-1 ring-[#0a0a0a]/10 text-[#0a0a0a]/40 hover:text-[#0a0a0a]/70 hover:ring-[#0a0a0a]/20 transition-all">Terms</a><a href="/ai-info" class="text-[10px] font-bold uppercase tracking-wider px-3 py-1 rounded-lg ring-1 ring-[#0a0a0a]/10 text-[#0a0a0a]/40 hover:text-[#0a0a0a]/70 hover:ring-[#0a0a0a]/20 transition-all">For AI agents</a></div><div class="text-xs text-[#0a0a0a]/40">© <!-- -->2026<!-- --> Space & Story · Toronto<span class="text-[10px] text-[#0a0a0a]/30 ml-3 cursor-help" title="Build dc2949f · main · 2026-07-06T20:29:19.259Z">v2026.07.06</span></div><div class="flex items-center gap-1.5 flex-wrap text-xs text-[#0a0a0a]/40"><span>AEO with</span><a href="https://cite-met.com" target="_blank" rel="noopener noreferrer" class="text-[10px] font-bold uppercase tracking-wider px-3 py-1 rounded-lg bg-lime/15 text-[#0a0a0a] hover:bg-lime/25 transition-colors">cite-met.com</a><span>· built with</span><a href="https://givefeedback.dev" target="_blank" rel="noopener noreferrer" class="text-[10px] font-bold uppercase tracking-wider px-3 py-1 rounded-lg bg-violet/10 text-violet hover:bg-violet/20 transition-colors">givefeedback.dev</a></div></div></div></footer></div><!--/$--><script class="$tsr" id="$tsr-stream-barrier">(self.$R=self.$R||{})["tsr"]=[];self.$_TSR={h(){this.hydrated=!0,this.c()},e(){this.streamEnded=!0,this.c()},c(){this.hydrated&&this.streamEnded&&(delete self.$_TSR,delete self.$R.tsr)},p(e){this.initialized?e():this.buffer.push(e)},buffer:[]};$_TSR.router=($R=>$R[0]={manifest:$R[1]={routes:$R[2]={__root__:$R[3]={preloads:$R[4]=["/assets/index-ChUjxb3O.js","/assets/react-vendor-Wq0efpRG.js","/assets/motion-vendor-BJz8KFM-.js"],assets:$R[5]=[$R[6]={tag:"script",attrs:$R[7]={type:"module",async:!0},children:"import(\"/assets/index-ChUjxb3O.js\")"}]},"/_layout":$R[8]={preloads:$R[9]=["/assets/_layout-BFu5uQ42.js","/assets/Navbar-DBBNV0GO.js","/assets/Logo-DcrIVrcC.js","/assets/external-link-DTBhSyxB.js","/assets/book-open-U-rYaVwv.js","/assets/ThemeProvider-CGy458QE.js","/assets/x-Dt3l_fYB.js","/assets/menu-Dw_oq2fL.js"]},"/_layout/blog_/$slug":$R[10]={preloads:$R[11]=["/assets/blog_._slug-BUGr8stp.js","/assets/musingsHelpers-ZEacJdM2.js","/assets/index-B08Cj_N_.js","/assets/htmlSanitizer-BYvwLYSF.js","/assets/musings-Do_Mqo7J.js","/assets/ExternalResourcesSection-CM3fDNaA.js","/assets/musings-index-fQENA_Jd.js","/assets/chevron-right-q0sSaOnp.js","/assets/arrow-left-Ch1L63Sp.js","/assets/clock-vb97P9kR.js","/assets/arrow-up-right-Cwfy7m9Q.js","/assets/blog_._slug-B_rCYFSW.js","/assets/not-found-DcudLBSv.js"]}}},matches:$R[12]=[$R[13]={i:"__root__�",u:1784918594011,s:"success",ssr:!0},$R[14]={i:"�_layout�",u:1784918594011,s:"success",ssr:!0},$R[15]={i:"�_layout�blog_�$slug�blog�javascript-seo-what-googlebot-renders",u:1784918594011,s:"success",l:$R[16]={post:$R[17]={id:"b7e4f2a8-3d6c-4f91-9e85-7c2d8a1f6b04",title:"JavaScript SEO: what Googlebot actually renders",slug:"javascript-seo-what-googlebot-renders",short_description:"Googlebot renders JavaScript in a second pass with an evergreen Chromium. Where SPAs still break, and how to ship a JS site that gets indexed",content:"Googlebot does render your JavaScript, just in a second pass, on its own schedule, with a stripped-down Chromium that carries no cookies and waits only so long before moving on. That second pass is where most JavaScript SEO problems live.\n\nI have audited dozens of React and Vue and Next.js sites for clients who were convinced \"Google can't see JavaScript.\" That hasn't been true since 2019, when Google switched to an [evergreen Googlebot running modern Chromium](https://developers.google.com/search/blog/2019/05/the-new-evergreen-googlebot). The actual problem is more boring and more fixable: the site ships an empty initial HTML, then asks Googlebot to wait. Sometimes Googlebot waits, sometimes it doesn't.\n\nThis post is what the Google Search Central docs and recent Search Off the Record episodes actually say about rendering, and what to fix on a JS-heavy site.\n\n## The two-pass rendering model\n\n[Per Google's JavaScript SEO basics](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics), Googlebot processes a JS-powered page in three phases: crawl, render, index. Crawl and render are not the same step.\n\nIn phase one, Googlebot fetches the URL and reads the response, parsing any HTML it gets, looking for `href` attributes to add to its crawl queue, and checking the robots meta tag. If the initial HTML is empty (just a `\x3Cdiv id=\"root\">` and a script tag) there is nothing here to index yet.\n\nIn phase two, every 200-response page goes into a render queue where a headless Chromium picks it up, executes the JavaScript, and produces the rendered HTML. Google parses *that* HTML for links and content, then indexes it.\n\n> Googlebot queues every page with a 200 status code for rendering, regardless of whether the page actually uses JavaScript. The page may sit in that queue for seconds, or longer.\n\nThe render queue is the bottleneck nobody talks about. If your page only has content after JavaScript runs, you are betting on that queue. For a small site, that bet usually wins. For a fast-changing site (listings, real-time inventory, news) that bet costs you indexing latency.\n\n## Why the \"Google can't see my SPA\" panic is mostly wrong\n\nThe old fear came from a real era. Per Martin Splitt and Edu Pereda on Search Off the Record episode 58, the original \"Google can't render JS\" sentiment dates back to when rendering was slow and unreliable. Since the evergreen switch, the rendering engine is current Chromium with the full set of standard browser APIs.\n\nWhat it does not support, per [Google's troubleshooting guide](https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript): user-permission features (camera, geolocation), state across page loads (localStorage and cookies are wiped per fetch), and non-HTTP transports (WebSockets, WebRTC). If your content depends on any of those, the rendered HTML will be empty.\n\nThat is the real failure mode. The problem isn't \"JavaScript broken,\" it's \"content gated behind something Googlebot literally cannot do.\"\n\nThree patterns that quietly kill indexing:\n\n- Content that only loads after a click (\"Load more,\" \"Show details,\" tabbed interfaces with no DOM-level content)\n- Content that requires a cookie set on the previous page\n- Content fetched from an API that fails the second request because the session is gone\n\nIf a real human can see the content on page load with no interaction, Googlebot probably can too. If they have to click first, Googlebot does not.\n\n## How browsers (and Googlebot) actually parse HTML\n\nPer Gary Illyes and Martin Splitt on SOTR episode 105 (\"How Browsers Really Parse HTML\"), there is a parsing rule most developers do not know: a non-metadata element appearing in the `\x3Chead>` implicitly closes the head and opens the body. Anything after that point (your canonical, your hreflang, your robots meta) ends up inside the body and is silently ignored.\n\nThe pattern that triggers this in the wild: an inline script in the head that injects an iframe or a paragraph during render. Gary cited a real example where hreflang tags appeared correct in source but were ignored because an injected iframe had already closed the head.\n\n> Metadata elements (`\x3Clink>`, `\x3Cmeta>`, `\x3Ctitle>`) only count when they are inside the head. A stray `\x3Cp>` or `\x3Ciframe>` in the head terminates the head context, and any subsequent metadata is treated as body content and discarded.\n\nThis is one of the strongest arguments for putting your canonical, hreflang, and robots meta into the server-rendered HTML rather than injecting them later via JavaScript.\n\n## Use the History API, not URL fragments\n\nA single-page app that routes by URL fragment (`example.com/#/products`) is a dead end for indexing. The [AJAX crawling scheme was deprecated in 2015](https://developers.google.com/search/blog/2015/10/deprecating-our-ajax-crawling-scheme); Googlebot does not resolve fragment URLs to distinct pages.\n\nThe fix is the History API. Real URLs in `href` attributes, intercepted in JavaScript, with `window.history.pushState` updating the visible URL. The Google docs example is short and worth copying.\n\nThe same rule applies to lazy-loaded content. Use `loading=\"lazy\"` for images and iframes. For content blocks, use the IntersectionObserver API to load when the element enters the viewport. Do not rely on scroll or click handlers, because [per Google's lazy-loading guidance](https://developers.google.com/search/docs/crawling-indexing/javascript/lazy-loading), Search does not interact with your page. It doesn't scroll or click.\n\nFor paginated content under infinite scroll: give each chunk a real, persistent URL (`?page=12`, not `?date=yesterday`), link to those URLs sequentially, and update the visible URL via History API as new chunks become primary. That is the only way a 200th-position result becomes individually crawlable.\n\n## The canonical-via-JavaScript trap\n\nThis is the most common JS SEO bug we see when we audit client sites. The HTML ships with one canonical. JavaScript runs and injects a different canonical. Now Google has two signals pointing in opposite directions.\n\nPer Martin Splitt on SOTR 105: \"If you have a canonical that is there at the first time we fetch the HTML from the server and then the JavaScript changes it, we actually advise against doing that.\" The fix is simple to state and easy to skip: set the canonical in the server HTML, or set it only in JavaScript, never both and never conflicting.\n\nIf your CMS or React framework supports server-side rendering of the canonical link tag, use it. If it does not, ship empty in HTML and inject from JS only.\n\n## SPA-heavy verticals where this matters most\n\nTwo industries we work with hit JS SEO walls constantly. [Real estate sites](/for/real-estate) lean on map components and filter UIs that load listings client-side after a user interaction, so Googlebot sees an empty grid. [AV installers](/for/av-installers) ship portfolio galleries built as carousels with content lazy-loaded behind clicks, then wonder why their case studies do not rank.\n\nFor both, the fix is usually the same: server-render the content card body. Keep the fancy interaction client-side. The HTML response should answer the question \"what's on this page\" before any JavaScript runs.\n\n## The soft-404 problem in SPAs\n\nClient-side routing breaks HTTP status codes. A user navigates to `/product/this-was-deleted`, the SPA loads, the API returns \"not found,\" and the page renders an error state, but the HTTP status is still 200. Google indexes it as a real page. Now you have ten thousand thin error pages in the index.\n\nPer Google's docs, two fixes work: either JavaScript-redirect to a URL that returns a real 404 from the server (`window.location.href = '/not-found'`), or dynamically inject a `\x3Cmeta name=\"robots\" content=\"noindex\">` when the API says the resource is gone. Either one stops the bleed, so pick whichever your stack makes easier.\n\n## What to check before you call it done\n\nUse the [URL Inspection Tool in Search Console](https://support.google.com/webmasters/answer/9012289) or the [Rich Results Test](https://search.google.com/test/rich-results). Both run the page through Google's actual rendering pipeline and show the post-render HTML. If your content is not in that rendered HTML, Google will not index it. No amount of theorizing fixes this, so open the tool and look at what came back.\n\nWhile you are there: check the JavaScript console output the tool exposes. If something threw an error during render, that is where you find out. Logging `window.onerror` to your analytics is the move; per Google, that catches most runtime issues. Parse errors will not show up, but the rest will.\n\nJavaScript SEO comes down to two things you can verify in the URL Inspection tool: the initial HTML gave Google enough to crawl with, and the rendered HTML gave Google everything it needed to index. If either one comes back short, you have your fix list.\n\n## Sources\n\n- [Understand JavaScript SEO Basics](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) (Google Search Central, March 2026)\n- [Fix Search-Related JavaScript Problems](https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript) (Google Search Central, December 2025)\n- [Fix lazy-loaded content](https://developers.google.com/search/docs/crawling-indexing/javascript/lazy-loading) (Google Search Central)\n- [Dynamic Rendering as a Workaround](https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering) (Google Search Central, deprecated workaround, kept for reference)\n- [The new evergreen Googlebot](https://developers.google.com/search/blog/2019/05/the-new-evergreen-googlebot) (Google Search Central Blog)\n- *How Browsers Really Parse HTML*, Search Off the Record episode 105 (Gary Illyes and Martin Splitt, February 2026)\n- *JavaScript at Google*, Search Off the Record episode 58 (Martin Splitt, Edu Pereda, Pascal Birchler)",image_url:null,author:"Mahmoud Halat",category:"AEO & AI Search",tags:$R[18]=["JavaScript SEO","rendering","SPA","Googlebot"],published_date:"2026-05-26",date_modified:"2026-05-26",featured:!1,read_time:7,lens:"development",parent_slug:"the-2026-google-seo-reality",series_order:6,series_section:"google-2026-bible",schema_type:"TechArticle",keywords:$R[19]=["javascript seo","googlebot rendering","spa seo","client side rendering google","does googlebot render javascript","react seo","history api spa"],tldr:"Googlebot renders JavaScript using an evergreen Chromium, but in a second pass after the initial crawl. Pages with empty initial HTML, content gated behind user interaction, or JavaScript-injected canonicals that conflict with the source HTML are the failure modes most likely to break indexing.",faqs:$R[20]=[$R[21]={question:"Does Googlebot render JavaScript in 2026?",answer:"Yes, it does. Googlebot runs an evergreen version of Chromium and renders JavaScript on every page that returns a 200 status code. Rendering happens in a second pass after the initial crawl, so JS-loaded content may take longer to be indexed than server-rendered HTML."},$R[22]={question:"Can Google index a single-page app?",answer:"Google can index SPAs that ship meaningful content in the initial HTML or load content without requiring user interaction. SPAs that use URL fragments for routing, gate content behind clicks, or depend on browser features Googlebot does not support (cookies across loads, geolocation) will not index correctly."},$R[23]={question:"Should I inject a canonical tag via JavaScript?",answer:"Avoid it when possible. If the server HTML already has a canonical, do not change it with JavaScript, because that creates conflicting signals. The safest pattern is to set the canonical in the server-rendered HTML and never modify it client-side. If JS is your only option, ship no canonical in HTML."},$R[24]={question:"Does Googlebot scroll or click?",answer:"No. Googlebot does not interact with your page. It does not scroll, click, hover, or trigger any user-action events. Any content loaded only on scroll or click will not be seen. Use the IntersectionObserver API and native loading=lazy for images, and ensure all content is reachable without interaction."},$R[25]={question:"What is the two-pass rendering model?",answer:"Google fetches your page once and parses the initial HTML, then queues the page for a second pass where headless Chromium executes the JavaScript and produces the rendered HTML. Only after that second pass does the page enter the index with its JS-generated content."}],external_links:$R[26]=[$R[27]={title:"Understand JavaScript SEO Basics",url:"https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics",source:"Google Search Central"},$R[28]={title:"Fix Search-Related JavaScript Problems",url:"https://developers.google.com/search/docs/crawling-indexing/javascript/fix-search-javascript",source:"Google Search Central"},$R[29]={title:"Fix lazy-loaded content",url:"https://developers.google.com/search/docs/crawling-indexing/javascript/lazy-loading",source:"Google Search Central"},$R[30]={title:"The new evergreen Googlebot",url:"https://developers.google.com/search/blog/2019/05/the-new-evergreen-googlebot",source:"Google Search Central Blog"},$R[31]={title:"How Browsers Really Parse HTML (SOTR 105)",url:"https://search-off-the-record.libsyn.com/how-browsers-really-parse-html-and-what-that-means-for-seo-transcript",source:"Search Off the Record"},$R[32]={title:"Deprecating AJAX crawling scheme",url:"https://developers.google.com/search/blog/2015/10/deprecating-our-ajax-crawling-scheme",source:"Google Search Central Blog"}],related_slugs:$R[33]=["googlebot-2mb-byte-limit","core-web-vitals-2026-actually","canonicalization-stop-fighting-google"],further_reading_slugs:$R[34]=["structured-data-guide-ai-search-json-ld","what-is-an-ai-agent"]},relatedPosts:$R[35]=[$R[36]={id:"bd3d08dd-90ed-49e9-99a4-e752513d9cae",title:"The 2MB rule: how Googlebot truncates your page",slug:"googlebot-2mb-byte-limit",short_description:"Googlebot fetches up to 2 MB of HTML per URL for Search. Anything past that is silently truncated, and most sites have no idea",content:"Googlebot fetches up to **2 MB of HTML per URL** for Search indexing. Anything past 2 MB is silently truncated, which means it never gets rendered and never reaches the index. The cap includes HTTP response headers.\n\nMost sites get away with bloated pages because their critical content (title, meta tags, canonical, structured data, hero text) lands inside the first 2 MB. The pages that don't get away with it are JavaScript-heavy sites with massive inline scripts or base64-encoded images that push the meaningful HTML past the cutoff.\n\nGary Illyes confirmed the number explicitly in his March 31, 2026 [Inside Googlebot blog post](https://developers.google.com/search/blog/2026/03/crawler-blog-post). The 2 MB limit isn't new, since it's been in effect for years, but the formal documentation in 2026 means there's no more debating it.\n\nThis post is the practical guide to the limit: what it is, why it matters more in 2026 than ever, how to test if your pages are over, and the quick wins that bring you back under.\n\n## The actual numbers\n\nThere isn't one byte limit. There are several, depending on the crawler and content type. Per Google's [Overview of Google crawlers and fetchers](https://developers.google.com/crawling/docs/crawlers-fetchers/overview-google-crawlers) and the March 2026 Inside Googlebot post:\n\n- **Googlebot (HTML)**: 2 MB per URL, including HTTP response headers\n- **Googlebot (PDF)**: 64 MB per URL\n- **Default for other Google crawlers**: 15 MB per URL\n- **Image and video crawlers**: vary by purpose (favicons are tiny, Image Search higher)\n\nPer Gary Illyes, \"Googlebot currently fetches up to 2MB for any individual URL (excluding PDFs).\" Per the same post: \"For PDF files, the limit is 64MB\" and for crawlers without specific limits, \"the default is 15MB regardless of content type.\"\n\nThe 2 MB cap is what matters for nearly every web page audit, and it's tighter than most engineers assume. The truncation behavior is brutal: Googlebot does not reject your page if it's over 2 MB, it cuts at exactly 2 MB. Whatever HTML is past byte 2,097,152 (2 × 1024 × 1024) silently disappears from Google's processing pipeline.\n\n> Googlebot's per-URL fetch limit for Search rendering is 2 MB of HTML, including HTTP response headers. Anything past 2 MB is silently truncated. Per Gary Illyes, Google Search Central blog, March 31, 2026.\n\nEmbedded resources (external CSS files, JavaScript bundles, images, XHR endpoints fetched during render) each have their own per-URL byte counter. The 2 MB on the HTML response is the constraint; external assets get 15 MB each by default. Moving inline content into external files is one of the quickest ways to get back under the 2 MB HTML cap (more on the tradeoff below).\n\n## Why 2 MB matters more in 2026 than ever\n\nThe cap isn't new. What's new is the cost of hitting it.\n\n**1. AI Mode and AI Overviews extract from rendered HTML.** When AI Mode generates an answer, it pulls signals from the rendered DOM Google indexed. If your structured data, canonical, or key content landed past the 2 MB cut, it's not in the DOM Google indexed, and it's not available for AI extraction. The pages most likely to be cited in AI Overviews are the pages with clean, machine-readable HTML inside the byte limit.\n\n**2. Information Agents do recurring fetches.** Per the I/O 2026 announcements, Information Agents (the persistent agent-based search surface) re-fetch pages on schedules. Each fetch is subject to the same 2 MB limit. A page that occasionally landed under the cap because of a smaller JS payload yesterday may land over the cap today, which makes the truncation hard to predict.\n\n**3. JavaScript fan-out is up.** Per Search Off the Record episode 106, the median mobile homepage went from 845 KB in 2015 to 2.3 MB in July 2025, roughly 3x growth in a decade. Most of that growth is JavaScript and inline assets, not actual content. Sites that were fine on page weight five years ago are now sitting right at the edge of the cap.\n\n**4. Render-blocking issues compound.** Google's Web Rendering Service (WRS), documented in its [JavaScript SEO Basics](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics), runs JavaScript and applies CSS. Per Gary Illyes's March 2026 post, WRS is stateless, so local and session storage are cleared between requests, and it skips images and videos while still processing JavaScript, CSS, and any XHR calls the page fires during render. If your page can't render its critical content within the 2 MB byte budget, WRS won't either.\n\nThe sites most at risk are heavy SPAs that ship the full app bundle in the initial HTML, sites that inline large base64 images directly in HTML, sites with massive inline CSS, and sites that try to be \"fast\" by inlining everything and accidentally push real content past the cap.\n\nWe see this pattern in heavy-SPA verticals like [real estate sites](/for/real-estate) and [AV installer portfolios](/for/av-installers), where map embeds and image carousels and a pile of inline scripts add up fast.\n\n## How to test if your page is over\n\nYou don't need expensive tools. Two methods.\n\n**Method 1: curl from your terminal.**\n\n```\ncurl -s -o /dev/null -w \"%{size_download}\n\" \\\n -H \"User-Agent: Googlebot/2.1 (+http://www.google.com/bot.html)\" \\\n https://yoursite.com/your-page\n```\n\nThat prints the byte count of the HTML response. If it's over 2,097,152 (2 MB in binary) you have a problem. Run it on every key page: homepage, top blog posts, top landing pages.\n\n**Method 2: Chrome DevTools.**\n\nOpen the page in Chrome, open DevTools, go to the Network tab, and reload. Click the very first row (the HTML document, not the assets) and read the \"Size\" column. If it's red and over 2 MB transferred, you're over the cap.\n\nA nuance: the 2 MB is the **uncompressed** HTML the server sends. Compression (gzip, Brotli) reduces transfer bytes but doesn't change what counts against the limit. The relevant number is what Googlebot sees after decompression. Most sites compress, so transferred bytes look small while the actual HTML is two to three times larger. Check the \"Content-Length\" header after decompression, not the over-the-wire transferred bytes.\n\n**Method 3: URL Inspection in Search Console.**\n\nIn Google Search Console, use the URL Inspection tool. Click \"View Crawled Page\" then \"HTML.\" If the HTML shown is truncated mid-tag or mid-element, you're over. This is the slowest method but it tells you exactly what Google saw.\n\n## Quick wins that bring you under\n\nHere's the order I run on client audits, fastest first:\n\n**1. Remove inline base64 images.** A single base64-encoded hero image can be 500 KB to 2 MB of HTML. Move it to an external file. Use a `\x3Cpicture>` element with `srcset` and `loading=\"lazy\"`. Per Martin Splitt on SOTR 106: don't ship a \"five megabyte image\" to a smartphone. Google itself enforces a 1 MB-per-image rule internally for its docs site submissions.\n\n**2. Move inline CSS to external files.** If you have 200 KB of CSS in a `\x3Cstyle>` block, move it to a `\x3Clink rel=\"stylesheet\">` reference. The CSS still loads (and counts against the rendering budget under its own URL's limit), but your HTML response gets back under the cap. Inline only the critical above-the-fold CSS if you must.\n\n**3. Move inline JavaScript to external files.** Same logic. A `\x3Cscript>` block with 500 KB of bundled JS is a 500 KB hit on your HTML response. External `\x3Cscript src=\"\">` references shift the bytes to a separate per-URL counter.\n\n**4. Kill unused JavaScript.** Run Chrome DevTools' Coverage panel (Cmd+Shift+P, then \"Show Coverage\"). It tells you what percentage of your loaded JS is actually used on this page. The result is usually appalling, somewhere between 60% and 90% unused. Tree-shake. Split your bundles. Lazy-load anything that's not above the fold.\n\n**5. Defer non-critical assets.** `\x3Cscript defer>` and `\x3Cscript async>` don't change HTML bytes, but they signal to the renderer to delay execution. This is more relevant for Core Web Vitals than the 2 MB cap, but worth doing in the same pass.\n\n**6. Audit your CMS for hidden bloat.** WordPress themes regularly inject hundreds of KB of inline CSS variables and tracking scripts. Squarespace and Wix templates have similar patterns. Inspect the actual HTML response, not the visual page. Plugins that \"optimize\" pages often inline more than they save.\n\n**7. Push critical content above the cut.** Even if you can't get under 2 MB this week, you can re-order the HTML so that `\x3Ctitle>`, `\x3Cmeta>`, canonical, structured data (`\x3Cscript type=\"application/ld+json\">`), main heading, and key body content all appear in the first 1 MB. Per Gary Illyes: \"Place critical elements early in the HTML to ensure they fall within the 2MB cut.\" The footer can sit on the wrong side of the cut, but the structured data can't.\n\n## The JavaScript-heavy site problem\n\nHeavy SPAs (single-page applications) typically ship a \"shell\" of HTML with most content rendered client-side by JavaScript. Per Google's [JavaScript SEO Basics](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics): \"Some JavaScript sites may use the app shell model where the initial HTML does not contain the actual content and Google needs to execute JavaScript before being able to see the actual page content.\"\n\nThe trap: if your JS bundle is inlined in the initial HTML to \"be fast,\" it can blow past 2 MB before any meaningful content renders. Googlebot truncates the HTML at 2 MB, the truncated content might not include the closing `\x3C/script>` tag, JS parsing fails, the render skips, and your content never gets indexed.\n\nThe fix is one of:\n\n- **Server-side render** (Next.js, Nuxt, SvelteKit, Remix), so the HTML response carries the rendered content before any JS executes.\n- **Static site generation** (Astro, Eleventy, Gatsby, Hugo), which renders the full HTML at build time and ships it ready to index.\n- **Hybrid pre-rendering** for crawlers, where dynamic rendering is allowed but Google recommends SSR or static instead.\n- **Code-splitting** so the initial JS bundle stays small and the extra chunks load on demand.\n\nFor our deeper take, see [JavaScript SEO: what Googlebot actually renders](/blog/javascript-seo-what-googlebot-renders). The 2 MB cap is one constraint among several that JS-heavy sites need to design around.\n\n## What the cap is not\n\nA few clarifications I get asked about constantly.\n\nThe 2 MB cap is **not a ranking signal**. Google doesn't demote you for being close to the limit. It just stops processing past it.\n\nIt is **not the same as Core Web Vitals**. CWV measures perceived performance for users (LCP, INP, CLS). The 2 MB cap measures what Googlebot fetches. A page can hit 100/100 on CWV and still be over 2 MB HTML, and a page can be under 2 MB and have terrible CWV.\n\nIt is **not about transferred bytes after compression**. It's about the decompressed HTML Google processes. Compression helps users; it doesn't help you against this cap.\n\nIt is **subject to change**. Per Gary Illyes's 2026 post, the limits are \"subject to change as the web evolves.\" Don't engineer to 1,999,000 bytes and call it good. Aim for under 1 MB if you can, which gives you headroom.\n\nFinally, the cap **doesn't mean you should split into more pages**. A 4 MB page should probably be one or two MB by removing bloat, not by chopping content into thinner sub-pages.\n\n## Sources\n\n- [Inside Googlebot: demystifying crawling, fetching, and the bytes we process](https://developers.google.com/search/blog/2026/03/crawler-blog-post) (Google Search Central blog, Gary Illyes, March 31, 2026)\n- [Overview of Google crawlers and fetchers](https://developers.google.com/crawling/docs/crawlers-fetchers/overview-google-crawlers) (Google Search Central)\n- [Understand JavaScript SEO Basics](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) (Google Search Central)\n- [Are websites getting fat? Page weight, HTML size and Googlebot limits explained](https://search-off-the-record.libsyn.com/are-websites-getting-fat-page-weight-html-size-googlebot-limits-explained-transcript) (Search Off the Record episode 106, Gary Illyes & Martin Splitt)",image_url:null,author:"Mahmoud Halat",category:"AEO & AI Search",tags:$R[37]=["Googlebot","crawling","page weight","AEO"],published_date:"2026-05-26",date_modified:"2026-05-26",featured:!1,read_time:6,lens:"development",parent_slug:"the-2026-google-seo-reality",series_order:2,series_section:"google-2026-bible",schema_type:"TechArticle",keywords:$R[38]=["googlebot 2mb limit","googlebot byte limit 2026","page weight seo","html size googlebot","googlebot truncation","googlebot html limit","how much html does google crawl"],tldr:"Googlebot fetches up to 2 MB of HTML per URL for Search indexing, including response headers. Anything past 2 MB is silently truncated, not rendered, not indexed. PDFs get 64 MB. Other Google crawlers default to 15 MB. Put structured data early in the HTML.",faqs:$R[39]=[$R[40]={question:"What is Googlebot's byte limit for HTML?",answer:"Googlebot fetches up to 2 MB of HTML per URL for Search indexing, including HTTP response headers. PDFs get a 64 MB limit. Other Google crawlers default to 15 MB per URL. Content past the cap is silently truncated. Googlebot does not reject the page, it just stops processing at the byte limit."},$R[41]={question:"What happens if my page is over 2 MB?",answer:"Googlebot cuts the HTML at 2 MB and ignores the rest. The truncated content is not rendered by the Web Rendering Service and not indexed. If your structured data, canonical tag, or key content lands past the cut, Google never sees it. Critical elements should appear within the first 1 MB to be safe."},$R[42]={question:"How do I check if my page is over Googlebot's HTML limit?",answer:"Run curl with a Googlebot user-agent and check the byte size. Or use Chrome DevTools' Network tab to see the uncompressed HTML response size. Or use Search Console's URL Inspection tool and view the crawled HTML, and if it's cut off mid-tag, you're over the limit. Compression doesn't change what counts."},$R[43]={question:"Does the 2 MB Googlebot limit include images and CSS?",answer:"No. The 2 MB applies only to the HTML response. External assets like CSS files, JavaScript bundles, and images each have their own per-URL byte counter, typically 15 MB for other Google crawlers. Moving inline assets to external files is one of the fastest ways to get your HTML response under the 2 MB cap."},$R[44]={question:"Why does the Googlebot 2 MB limit matter more in 2026?",answer:"AI Mode and AI Overviews extract answers from the rendered DOM Google indexed. If structured data lands past the 2 MB cut, it's not available for AI extraction. Information Agents also re-fetch pages on schedules, so truncation can be unpredictable. JavaScript-heavy sites with large inline bundles are most at risk."}],external_links:$R[45]=[$R[46]={title:"Inside Googlebot: demystifying crawling, fetching, and the bytes we process",url:"https://developers.google.com/search/blog/2026/03/crawler-blog-post",source:"Google Search Central blog"},$R[47]={title:"Overview of Google crawlers and fetchers",url:"https://developers.google.com/crawling/docs/crawlers-fetchers/overview-google-crawlers",source:"Google Search Central"},$R[48]={title:"Understand JavaScript SEO Basics",url:"https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics",source:"Google Search Central"},$R[49]={title:"Search Off the Record: Are websites getting fat? Page weight and Googlebot limits explained",url:"https://search-off-the-record.libsyn.com/are-websites-getting-fat-page-weight-html-size-googlebot-limits-explained-transcript",source:"Search Off the Record podcast episode 106"}],related_slugs:$R[50]=["robots-txt-in-2026-what-it-does","javascript-seo-what-googlebot-renders","core-web-vitals-2026-actually","crawl-budget-myth-small-sites"],further_reading_slugs:$R[51]=["the-2026-google-seo-reality","structured-data-guide-ai-search-json-ld"]},$R[52]={id:"22dab11b-7c37-4ca2-a9fd-8431cd69f3f8",title:"Core Web Vitals in 2026: what actually changes your score",slug:"core-web-vitals-2026-actually",short_description:"Three metrics. Three thresholds. One 75th-percentile rule most teams misread. Here's what actually changes Core Web Vitals on a real site",content:"Core Web Vitals in 2026 are three metrics: Largest Contentful Paint under 2.5 seconds (loading), Interaction to Next Paint under 200 milliseconds (responsiveness), and Cumulative Layout Shift under 0.1 (visual stability). INP replaced First Input Delay in March 2024. The thresholds are pass-fail at the 75th percentile of real user data, not lab data. Google confirms CWV is used in ranking, but won't say how much, and an empty page that loads fast still ranks badly because content quality dominates.\n\nThat's the whole shape of it, and everything else is implementation. Let me walk through what actually matters when you ship.\n\n## The three metrics, with the thresholds Google publishes\n\nPer [Google's Core Web Vitals documentation](https://developers.google.com/search/docs/appearance/core-web-vitals):\n\n**LCP, Largest Contentful Paint.** When did the largest visible content element finish rendering? Good is under 2.5 seconds, needs-improvement is 2.5 to 4 seconds, poor is over 4 seconds. The \"largest\" is usually a hero image, video thumbnail, or large H1 block. Time-to-first-byte is a sub-metric.\n\n**INP, Interaction to Next Paint.** How fast does the page paint a response to a user's interaction (click, tap, key press)? Good is under 200ms, needs-improvement is 200 to 500ms, poor is over 500ms. INP replaced FID in March 2024. The critical difference is that INP samples *all* interactions on the page and reports the slowest, where FID only measured the first.\n\n**CLS, Cumulative Layout Shift.** How much does content shift around as the page loads? The score is unitless, lower is better: good is under 0.1, needs-improvement is 0.1 to 0.25, poor is over 0.25. The classic CLS killer is an ad slot or image without explicit dimensions pushing text down after the user starts reading.\n\n## Why INP replaced FID, and what changed\n\nRick Viscomi, Chrome's web performance DevRel, said it plainly on [SOTR 071](https://search-off-the-record.libsyn.com/transcript-for-deciphering-inp-and-core-web-vitals): FID was \"overly permissive.\" Roughly 99.9% of desktop sites and 95% of mobile sites passed FID. A metric almost everyone passes isn't measuring anything.\n\nINP fixes that. It looks at every interaction on the page (every click, every keypress, every tap) and surfaces the slowest. The number gets worse than your old FID score, but it's not a regression, it's the new metric exposing slowness FID was hiding.\n\n> Per Mueller on SOTR 071: \"We do use these in our ranking systems or in Search systems. The details we tend not to go into. We don't go into thresholds.\"\n\nSo yes, CWV affects ranking. But the magnitude is undisclosed, and an empty page that loads in 200ms still won't rank because content quality is a separately weighted set of systems.\n\n## Field data vs lab data: the single most expensive mistake\n\nThis is where most teams burn weeks of engineering. Lab data and field data are different things that measure different things, and disagreeing between them is the normal case.\n\n**Lab data** comes from synthetic tools like Lighthouse. The browser is throttled to simulate a worst-case slow mobile device on a slow connection, and the page loads in a controlled emulator. It's useful for debugging and reproducible regression tests, but not for measuring real users.\n\n**Field data** comes from the [Chrome User Experience Report](https://developer.chrome.com/docs/crux/) (CrUX), which aggregates anonymized real-user metrics from opted-in Chrome users. It's available as origin-level data for any site that has enough traffic, and sometimes page-level data if traffic is high enough.\n\nThe 75th-percentile rule applies to field data. \"Good\" doesn't mean median. It means three quarters of real-user experiences hit the threshold. If your INP is 280ms at the 75th percentile, you're in \"needs improvement,\" not \"good,\" even if your median is 150ms.\n\nLighthouse can't measure INP at all in a meaningful way because Lighthouse doesn't actually click anything. It uses Total Blocking Time as a lab proxy. That's why a Lighthouse score of \"40-something red\" can coexist with a real-user CWV pass, and vice versa. Don't pick one and ignore the other: use Lighthouse to find what to fix, and use CrUX to know if real users are still struggling.\n\nPageSpeed Insights blends both, showing CrUX field data at the top of the report and Lighthouse lab data at the bottom, and mixing the two is the cause of 90% of \"but my CWV passes!\" arguments.\n\n## What \"Core Web Vitals failures\" usually mean\n\nAfter enough audits, a pattern: most CWV failures on small business sites are JavaScript bloat. Specifically:\n\n- **Multi-megabyte JavaScript bundles** that ship analytics, A/B testing, heatmaps, chat widgets, consent managers, and a CRM tracker on every page. Each delays main-thread work, which delays INP and pushes LCP back.\n- **Render-blocking third-party scripts** in the head (fonts, analytics, tag managers) that block the browser from painting until they resolve.\n- **Layout shift from un-dimensioned media**: images, videos, and ads without explicit width/height attributes, plus late-loading web fonts swapping in.\n- **Single Page App route transitions** that throw away the page and rebuild it client-side, costing a fresh LCP each navigation.\n\n> Most CWV failures are not server speed. They're the script tags you added one at a time, without ever removing one. The fix is rarely a CDN. The fix is an audit of what's on the page.\n\nWordPress sites in particular accumulate this fast. Every plugin ships scripts. Bundle them and you hit INP cliffs you wouldn't predict from code review.\n\n## Page experience is one signal, not THE signal\n\nThe page-experience signal exists. CWV is part of it. So is HTTPS, mobile-friendliness, and the absence of [intrusive interstitials](https://developers.google.com/search/docs/appearance/avoid-intrusive-interstitials). But [Google's own page-experience FAQ](https://developers.google.com/search/docs/appearance/page-experience) is explicit: \"There is no single signal. Our core ranking systems look at a variety of signals.\"\n\nThe trap is treating CWV as a binary pass/fail that decides ranking, which it isn't. A site with great content and \"needs improvement\" CWV will outrank a site with terrible content and perfect CWV. Mueller on SOTR 071: \"I have Core Web Vitals check. I will now rank well, that's a misunderstanding.\"\n\nThe right mental model: CWV is a hygiene minimum. Pass the thresholds at the 75th percentile of field data, then forget the score and spend your engineering time on what differentiates the page.\n\n## Intrusive interstitials are a separate penalty\n\nThe interstitial check sits next to CWV in Google's page-experience guidance, not inside it. The rule: pop-ups that cover the main content on mobile shortly after the user arrives can demote the page in mobile search.\n\nExemptions exist for legally required interstitials (age gates, cookie consent banners that don't block content) and for login walls on private content. The pattern Google penalises is the marketing modal that interrupts the user the moment they arrive on the page.\n\nFor full context on what else feeds into the broader picture, see the [2026 SEO reality pillar](/blog/the-2026-google-seo-reality), the [structured data spoke](/blog/structured-data-types-google-2026) that pairs technical eligibility with content quality, and the [JavaScript SEO breakdown](/blog/javascript-seo-what-googlebot-renders) for what Googlebot actually executes when it visits.\n\n## A four-step audit that resolves most CWV failures\n\nWhen the studio takes on a CWV-blocked site, the loop is consistent:\n\n1. **Pull CrUX field data.** Search Console's Core Web Vitals report or [PageSpeed Insights](https://pagespeed.web.dev/) gives you the 75th-percentile values. This is the only number that matters for ranking impact.\n2. **Add instrumentation.** Install [web-vitals.js](https://github.com/GoogleChrome/web-vitals) and pipe metrics to your analytics. Lab data tells you potential issues; field data tells you what real users are hitting, and without instrumentation you're guessing.\n3. **Audit your JavaScript.** Open the Network tab, sort by transfer size, and count the third-party scripts. Most sites can remove 30 to 50% of their JS payload by killing dead tag-manager triggers and unused analytics tools.\n4. **Fix the highest-impact metric first.** INP failures usually come from a small number of long tasks, and the [Long Animation Frames API](https://developer.chrome.com/docs/web-platform/long-animation-frames) makes them attributable. LCP failures usually come from a slow hero image or a render-blocking font. CLS failures are almost always missing dimensions on media.\n\nThat's the work. There's nothing magic about CWV; it's a measurable hygiene loop you can instrument and close.\n\n## Sources\n\n- [Understanding Core Web Vitals and Google Search results](https://developers.google.com/search/docs/appearance/core-web-vitals) (Google Search Central)\n- [Understanding page experience in Google Search results](https://developers.google.com/search/docs/appearance/page-experience) (Google Search Central)\n- [Avoid intrusive interstitials and dialogs](https://developers.google.com/search/docs/appearance/avoid-intrusive-interstitials) (Google Search Central)\n- [Deciphering INP and Core Web Vitals (SOTR 071)](https://search-off-the-record.libsyn.com/transcript-for-deciphering-inp-and-core-web-vitals) (Search Off the Record podcast, March 2024)",image_url:null,author:"Mahmoud Halat",category:"AEO & AI Search",tags:$R[53]=["Core Web Vitals","page experience","LCP","INP","CLS"],published_date:"2026-05-26",date_modified:"2026-05-26",featured:!1,read_time:7,lens:"development",parent_slug:"the-2026-google-seo-reality",series_order:5,series_section:"google-2026-bible",schema_type:"TechArticle",keywords:$R[54]=["Core Web Vitals 2026","INP threshold","LCP threshold","CLS threshold","field data vs lab data","75th percentile CWV","does Core Web Vitals affect ranking"],tldr:"Core Web Vitals are LCP under 2.5 seconds, INP under 200 milliseconds, CLS under 0.1, measured at the 75th percentile of CrUX field data. INP replaced FID in March 2024. CWV is one ranking signal among many. A fast empty page won't rank.",faqs:$R[55]=[$R[56]={question:"What are the Core Web Vitals thresholds in 2026?",answer:"Largest Contentful Paint under 2.5 seconds is good. Interaction to Next Paint under 200 milliseconds is good. Cumulative Layout Shift under 0.1 is good. The thresholds are measured at the 75th percentile of real-user field data from the Chrome User Experience Report, meaning three quarters of real user experiences must hit the threshold for a page to be considered good."},$R[57]={question:"When did INP replace FID?",answer:"Interaction to Next Paint replaced First Input Delay as a Core Web Vital in March 2024. FID measured only the first interaction on a page and roughly 99.9% of desktop sites passed it. INP measures all interactions and surfaces the slowest, exposing responsiveness issues FID was hiding."},$R[58]={question:"Does Core Web Vitals affect Google ranking?",answer:"Yes, but as one signal among many. Google's John Mueller confirmed on Search Off the Record episode 71 that CWV is used in ranking systems but declines to disclose thresholds or weights. A page with poor CWV but excellent content can still outrank a page with perfect CWV and weak content because content quality dominates."},$R[59]={question:"What is the difference between field data and lab data for Core Web Vitals?",answer:"Lab data comes from synthetic tools like Lighthouse running a throttled emulated mobile device, useful for debugging and reproducible tests. Field data comes from the Chrome User Experience Report (CrUX), which aggregates real-user metrics from opted-in Chrome users. Only field data at the 75th percentile is what Google uses to assess ranking impact."},$R[60]={question:"Why is my Lighthouse score red but my Core Web Vitals pass?",answer:"Lighthouse uses a worst-case throttled emulator and runs in a lab. CrUX uses real-user field data. They measure different things and frequently disagree. PageSpeed Insights shows both: the field data at the top, the Lighthouse score at the bottom. Field data at the 75th percentile is what determines whether your page passes Core Web Vitals."}],external_links:$R[61]=[$R[62]={title:"Understanding Core Web Vitals and Google Search results",url:"https://developers.google.com/search/docs/appearance/core-web-vitals",source:"Google Search Central"},$R[63]={title:"Understanding page experience in Google Search results",url:"https://developers.google.com/search/docs/appearance/page-experience",source:"Google Search Central"},$R[64]={title:"Avoid intrusive interstitials and dialogs",url:"https://developers.google.com/search/docs/appearance/avoid-intrusive-interstitials",source:"Google Search Central"},$R[65]={title:"Deciphering INP and Core Web Vitals (SOTR 071)",url:"https://search-off-the-record.libsyn.com/transcript-for-deciphering-inp-and-core-web-vitals",source:"Google Search Off the Record"},$R[66]={title:"web-vitals.js",url:"https://github.com/GoogleChrome/web-vitals",source:"GitHub / Chrome"}],related_slugs:$R[67]=["googlebot-2mb-byte-limit","javascript-seo-what-googlebot-renders","structured-data-types-google-2026"],further_reading_slugs:$R[68]=["wordpress-bloat-vs-ai-native-architecture","why-custom-web-design-beats-templates-ai-era"]},$R[69]={id:"c92a87f6-8e51-4d6a-b7c4-0f29e8a3c5d1",title:"Canonicalization: stop fighting Google's canonical picker",slug:"canonicalization-stop-fighting-google",short_description:"rel=canonical is a hint, not a directive. How Google actually picks the canonical URL, and the three signals that reliably move the decision",content:"Your `rel=\"canonical\"` tag is a hint. Google picks the canonical, and when you fight that you tend to lose for reasons that have nothing to do with the canonical tag itself.\n\nThis is the single most-misunderstood piece of technical SEO we encounter on client audits. A site has 12 versions of the same product page, each with a `rel=\"canonical\"` pointing to the \"real\" URL, and somehow Google keeps indexing the wrong one. The team blames the algorithm. The actual cause is almost always conflicting signals the team did not realize they were sending.\n\nHere is what the Google docs actually say, and how to use it.\n\n## The canonical tag is a hint\n\n[Per Google's canonicalization documentation](https://developers.google.com/search/docs/crawling-indexing/canonicalization): \"indicating a canonical preference is a hint, not a rule.\" Google selects the canonical based on a basket of signals it collects during indexing. `rel=\"canonical\"` is one input, weighted heavily but never as a hard rule.\n\n> Google's canonical selector chooses the URL it considers most representative of a piece of content from a set of duplicates. Your canonical tag is one of several signals; Google can and does override it when other signals point elsewhere.\n\nThe signals Google uses, [per the consolidate-duplicate-urls doc](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls), in rough order of weight:\n\n1. **Redirects**: a 301 from URL A to URL B is the strongest possible canonical signal\n2. **`rel=\"canonical\"` link annotations**: strong, but not binding\n3. **Sitemap inclusion**: a weak signal\n4. **HTTPS over HTTP**: a default preference, almost always picked\n5. **`hreflang` cluster membership**: URLs in a reciprocal hreflang cluster are preferred over URLs outside it\n6. **Internal linking consistency**: which URL you link to from your own pages\n7. **URL simplicity**: given two otherwise equal URLs, the shorter/cleaner one wins\n8. **Popularity**: links and search signals from elsewhere\n\nWhen Google picks a canonical different from yours, one of those signals is overriding your tag. Often it is something silly: you 301 the canonical to a different URL, then `rel=\"canonical\"` back to the original. Or your sitemap includes the version you said wasn't canonical. Or every internal link points to the duplicate.\n\n## The three signals that actually move the picker\n\nFor a small business site, you control three signals reliably. Get these three aligned and Google almost always picks the URL you want.\n\n**One. The canonical tag itself.** Use an absolute URL (`https://www.example.com/page`, not `/page`). Put it in the `\x3Chead>`, not the body. Only one canonical per page. Match the protocol, subdomain, and trailing-slash convention exactly to the URL you want indexed.\n\n**Two. 301 redirects from every duplicate to the canonical.** This is the strongest signal Google accepts. If you can redirect, redirect. Use it for HTTP-to-HTTPS, www-to-non-www, trailing-slash normalization, and old URL retirement.\n\n**Three. Internal linking.** Every link inside your site that points to the duplicate URL is a vote for the duplicate. Per Google's documentation: \"When linking within your site, link to the canonical URL rather than a duplicate URL.\" Run a crawl of your own site and check. We see this miss constantly: a site canonicals the clean URL but every menu link, footer link, and breadcrumb points to a tracking-parameter version.\n\n## When Google picks something different\n\nEven with all three signals lined up, Google sometimes picks a different canonical. Per [the troubleshooting doc](https://developers.google.com/search/docs/crawling-indexing/canonicalization-troubleshooting), use Search Console's URL Inspection tool to see which URL Google considers canonical for a given page. Before you fight it: think about whether Google's choice is actually fine.\n\nIf Google canonicaled to the HTTP version of a page where you wanted HTTPS, the cause is almost always one of these:\n\n- The HTTPS page has an invalid or mismatched SSL certificate\n- The HTTPS page loads insecure dependencies (HTTP scripts, HTTP iframes)\n- The HTTPS page redirects through HTTP somewhere in the chain\n- The HTTPS page has a canonical pointing to the HTTP page (this happens; it is usually a CMS bug)\n\nIf Google canonicaled to a different language variant: your `hreflang` cluster is incomplete. The de-DE page reciprocally points to de-CH; both prefer themselves over de-AT, which sits outside the cluster.\n\nIf Google canonicaled across domains to a copycat: someone is scraping you and Google's freshness signals favored the scraper, so file a DMCA. The [troubleshooting doc covers this case](https://developers.google.com/search/docs/crawling-indexing/canonicalization-troubleshooting#a-copycat-website) directly.\n\nIf Google canonicaled to a hacked URL: you have been hacked. A common attack injects a cross-domain `rel=\"canonical\"` into the head, redirecting your authority elsewhere. Check your server-rendered HTML for unexpected canonical tags and audit recent file changes.\n\n## Canonical vs noindex vs robots.txt\n\nThese are not interchangeable. We see them confused on almost every audit.\n\n- **`rel=\"canonical\"`**: \"this is a duplicate; consolidate signals to the canonical.\" Google still crawls; the duplicate stays out of the index but does not block other Google systems.\n- **`noindex`**: \"do not show this in Search at all.\" Google still crawls (to see the noindex), then drops the page from the index entirely. Use this for pages users need but Search does not (thank-you pages, internal search results).\n- **robots.txt disallow**: \"do not crawl this.\" Google cannot read the page or follow links from it. The URL may still appear in Search results (without a snippet) if other sites link to it.\n\nThe decision tree we walk clients through:\n\n- Page is a near-duplicate of another canonical URL → `rel=\"canonical\"`\n- Page has unique content but should not appear in Search → `noindex`\n- Page should not be crawled at all (faceted nav with millions of combinations, admin pages, internal tools) → robots.txt disallow\n\nPer Google's docs, do not use `noindex` for canonicalization. It removes the page entirely; it does not consolidate signals to another URL.\n\nPer Google's docs, do not use robots.txt for canonicalization. Google may still index the blocked URL (without content) if other signals point to it.\n\n## The bugs that quietly destroy canonicals\n\nA short list from real audits:\n\n**The protocol mismatch.** Your canonical reads `http://www.example.com/page` but the page lives at `https://www.example.com/page`. Google now has to pick. Often picks the HTTP version, then your HTTPS page disappears from Search.\n\n**The paginated canonical.** Page 2 of a listing canonicals to page 1. Google indexes page 1, but cannot find the items that only appeared on page 2. Per Google guidance, each paginated chunk should self-canonical and link sequentially.\n\n**The CMS-injected duplicate canonical.** Two `rel=\"canonical\"` tags on the same page, pointing to different URLs. Common with WordPress + an SEO plugin that does not respect a theme's existing canonical. Google picks one, but which one depends on parse order and is not your problem to control.\n\n**The JavaScript-injected canonical that conflicts with the server HTML.** Per Martin Splitt on Search Off the Record episode 105: Google advises against this directly. Either render the canonical from the server, or render it only in JavaScript, never both.\n\n**The `noindex, follow` page with a canonical.** When Google sees noindex, it may stop rendering and stop processing meta tags. Your canonical may not be read at all. If you noindex a page, do not bother canonicalizing it.\n\n**The fragment canonical.** `https://example.com/page#section`. Google does not support URL fragments as canonicals, so the page will be canonicaled to `https://example.com/page` regardless of the fragment you specify.\n\n## When site moves and canonicals collide\n\nPer Lizzi Sassman, John Mueller, and Gary Illyes on Search Off the Record episode 56: most \"lost traffic after a site move\" cases trace back to redirect misconfigurations that conflict with canonical signals. The pattern Gary called out: redirects that fire for users but not for Googlebot. The user sees the new site; Googlebot sees the old one. Canonical signals on the new site point to URLs that, from Googlebot's perspective, do not redirect properly. The whole signal map breaks.\n\nIf you are moving a site: keep a URL map. Test redirects from a Googlebot user-agent (or use Search Console's URL Inspection on representative URLs). Wait roughly a year before concluding the move failed. Google's recovery timeline is real and you cannot rush it.\n\n## The short version\n\n`rel=\"canonical\"` is a hint. Three signals do most of the work: the canonical tag itself, 301 redirects, and consistent internal linking. Google will sometimes overrule you, and when it does, the cause is almost always findable in the troubleshooting guide. Pick one signal type per case, and don't mix canonicals with noindex, block with robots.txt and canonicalize, or inject conflicting tags from JavaScript.\n\nOnce every signal you control points at the same URL, Google usually follows.\n\n## Sources\n\n- [What is URL Canonicalization](https://developers.google.com/search/docs/crawling-indexing/canonicalization) (Google Search Central, December 2025)\n- [How to specify a canonical URL](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls) (Google Search Central, March 2026)\n- [Fix canonicalization issues](https://developers.google.com/search/docs/crawling-indexing/canonicalization-troubleshooting) (Google Search Central, December 2025)\n- *Site moves: Are they getting any easier?*, Search Off the Record episode 56 (Gary Illyes, John Mueller, Lizzi Sassman)\n- *How Browsers Really Parse HTML*, Search Off the Record episode 105 (Gary Illyes and Martin Splitt)",image_url:null,author:"Mahmoud Halat",category:"AEO & AI Search",tags:$R[70]=["canonicalization","duplicate content","rel canonical"],published_date:"2026-05-26",date_modified:"2026-05-26",featured:!1,read_time:7,lens:"development",parent_slug:"the-2026-google-seo-reality",series_order:7,series_section:"google-2026-bible",schema_type:"TechArticle",keywords:$R[71]=["canonicalization","rel canonical","duplicate content google","how google picks canonical","google canonical not respected","canonical vs noindex","site move canonical"],tldr:"rel=canonical is a hint Google can override. Three signals reliably move the canonical decision: the tag itself, 301 redirects, and consistent internal linking. When Google picks a different canonical, the cause is usually a protocol mismatch, hreflang gap, or conflicting JavaScript-injected tag.",faqs:$R[72]=[$R[73]={question:"Why does Google ignore my canonical tag?",answer:"rel=canonical is a hint, not a directive. Google selects the canonical based on multiple signals: redirects, internal linking, sitemap inclusion, HTTPS preference, hreflang clusters, and the canonical tag itself. When Google picks a different URL, one of those other signals is contradicting your tag. The URL Inspection tool in Search Console shows which canonical Google chose and often hints at why."},$R[74]={question:"Should I use noindex or canonical for duplicate pages?",answer:"Use canonical for near-duplicate pages where you want signals consolidated to a preferred URL. Use noindex for pages that should not appear in Search at all (thank-you pages, internal search results, account pages). Do not use noindex for canonicalization, because it removes the page entirely instead of consolidating its authority to another URL."},$R[75]={question:"Can I canonicalize across domains?",answer:"Yes, Google supports cross-domain rel=canonical for syndicated content, but it is less reliable than same-domain canonicalization. For syndication partners, Google's documentation recommends asking partners to noindex the syndicated copy instead. Be cautious, because a cross-domain canonical from a hacked site is a known attack pattern that can redirect your authority elsewhere."},$R[76]={question:"Do I need a self-referencing canonical on every page?",answer:"Yes, ideally. A self-referencing canonical (where a page declares itself the canonical) protects against parameter-based duplicates and scraping. Google does not require it. If you do not specify a canonical, Google will pick one based on its other signals, but a self-canonical removes ambiguity at near-zero cost."},$R[77]={question:"How do I know which canonical Google picked?",answer:"Use the URL Inspection tool in Google Search Console. It shows both the user-declared canonical (your tag) and the Google-selected canonical (the URL Google actually indexes). When the two disagree, the tool typically indicates why, usually a stronger signal like a 301 redirect, an HTTPS preference, or an hreflang cluster."}],external_links:$R[78]=[$R[79]={title:"What is URL Canonicalization",url:"https://developers.google.com/search/docs/crawling-indexing/canonicalization",source:"Google Search Central"},$R[80]={title:"How to specify a canonical URL with rel=canonical and other methods",url:"https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls",source:"Google Search Central"},$R[81]={title:"Fix canonicalization issues",url:"https://developers.google.com/search/docs/crawling-indexing/canonicalization-troubleshooting",source:"Google Search Central"},$R[82]={title:"Site moves (SOTR 56)",url:"https://search-off-the-record.libsyn.com/transcript-for-site-moves-are-they-getting-any-easier",source:"Search Off the Record"},$R[83]={title:"How Browsers Really Parse HTML (SOTR 105)",url:"https://search-off-the-record.libsyn.com/how-browsers-really-parse-html-and-what-that-means-for-seo-transcript",source:"Search Off the Record"}],related_slugs:$R[84]=["robots-txt-in-2026-what-it-does","javascript-seo-what-googlebot-renders","crawl-budget-myth-small-sites"],further_reading_slugs:$R[85]=["structured-data-guide-ai-search-json-ld","what-is-an-ai-agent"]}]},ssr:!0}],lastMatchId:"�_layout�blog_�$slug�blog�javascript-seo-what-googlebot-renders"})($R["tsr"]);$_TSR.e();document.currentScript.remove()</script><script type="module" async="">import("/assets/index-ChUjxb3O.js")</script></body></html>