view-source, through the crawler's eye

SEO is not a checklist you bolt on later. It is a property of the bytes your frontend returns. What ships solid gets indexed now. What waits for JavaScript waits for the render queue.

A raw HTTP response shown as view-source. The lines shipped in the first response, a title, a canonical link, JSON-LD, an h1, and the main product copy, are inked solid and labelled index-eligible in wave one. The lines that only appear after JavaScript runs, an empty root div and the script that injects the heading and body, are ghosted and labelled wave two. The thesis: what a crawler can index is a property of the response bytes.

SEO-friendly frontend architecture, from raw bytes to rankable pages

Reframe SEO as a software engineering property of your response bytes. Trace one causal thread from a rendering decision to what Googlebot receives in wave one, what it must render to see in wave two, what becomes index-eligible, and what can rank.

How Googlebot actually fetches, renders, and indexes#

Most SEO advice is a checklist. This guide is a causal chain instead. To build an SEO-friendly frontend architecture, you first need the model of how a crawler turns your response into an index entry. Google is explicit that this happens in two waves. The diagram below is the whole pipeline. Read it once, because every later section is a zoom into one node of it.

The two-wave crawl, render, and index pipelineA URL enters the crawl queue and the raw HTTP response is fetched. In wave one, the first-wave HTML is parsed and indexed, so whatever ships in those bytes is index-eligible now. If the response needs JavaScript to show its content, the page waits in the render queue (seconds, sometimes days) before headless Chromium renders it and wave two indexes the DOM. Index-eligible content from either wave can then compete to rank.

Wave one: the raw HTTP response#

Wave one is the fast path. Googlebot fetches your URL and gets back a stream of bytes. Then it parses that HTML immediately and indexes what it finds. So the question that decides your fate is simple. Is the content in those bytes, or not? Open any page and view source, because that literal text is wave one. Whatever a crawler can index without running your JavaScript lives right there.

Wave two: the render queue and the rendered DOM#

Wave two is the slow path, and its cost is time. When your content only appears after JavaScript runs, Google cannot see it in wave one. Instead the page joins a render queue. Google states plainly that a page queues for rendering and "may stay on this queue for a few seconds, but it can take longer," gated on "once Google's resources allow." Because of that delay, JavaScript-dependent content is indexed later than static content, and new or changed pages surface more slowly.

The one question that decides everything#

Here is the lens the rest of the guide uses. For every element on a page, ask one thing. Is it first-wave, second-wave, or never-seen? First-wave content ships in the response and is index-eligible now. Second-wave content appears only after render, so it waits. Never-seen content sits behind a click, a tab, or a login, and a crawler never triggers it. Consequently this single question turns fuzzy SEO folklore into a property you can inspect in view-source.

The crawler's-eye ledger: toggle a mode, watch index-eligibility change#

Reading about two waves is one thing. Watching them is another. The explorer below makes the lens physical. Flip the rendering mode across CSR, SSR, SSG, and ISR. Then watch two panels update together: the raw HTTP response a crawler receives in wave one, and the rendered DOM it sees only in wave two. Meanwhile the ledger tags every element first-wave, second-wave, or never-seen, and the index-eligibility score recomputes live.

Visibility ledger explorer
7 / 8elements index-eligible in wave one
render-queue wait
none
content freshness
at build time
crawl-budget cost
lowest

Wave 1raw HTTP response (view-source)

<!doctype html>
<title>Trail runner, size 10 | Acme</title>
<link rel="canonical" href="/p/acme-10">
<script type="application/ld+json">Product</script>
<h1>Acme trail runner</h1>
<main> ...full product copy... </main>

Wave 2rendered DOM (after JS runs)

<h1>Acme trail runner</h1>
<main> ...full product copy... </main>
<nav> ...primary links... </nav>
<!-- the JS-only tab panel is still
     empty: nothing clicked it -->
Page elementWave 1Wave 2
Title and meta description<title>indexedindexed
Main heading<h1>indexedindexed
Main content body<main>indexedindexed
Primary navigation links<a href>indexedindexed
Canonical URLrel=canonicalindexedindexed
Structured datald+jsonindexedindexed
Hero image source<img src>indexedindexed
Content behind a JS-only tabno SSR fallbacknever seennever seen

SSGThe HTML is in the first response, so 7 of 8 elements are index-eligible in wave one, with no render wait. Freshness is at build time. Only the JS-only tab stays unseen, because a crawler does not click to reveal it.

First-wave visibility by rendering mode (illustrative). "first-wave" is index-eligible now; "second-wave" waits for the render queue; "never-seen" is not indexed at all.
Page elementCSRSSRSSGISR
Title and meta descriptionfirst-wavefirst-wavefirst-wavefirst-wave
Main headingsecond-wavefirst-wavefirst-wavefirst-wave
Main content bodysecond-wavefirst-wavefirst-wavefirst-wave
Primary navigation linkssecond-wavefirst-wavefirst-wavefirst-wave
Canonical URLsecond-wavefirst-wavefirst-wavefirst-wave
Structured datasecond-wavefirst-wavefirst-wavefirst-wave
Hero image sourcesecond-wavefirst-wavefirst-wavefirst-wave
Content behind a JS-only tabnever-seennever-seennever-seennever-seen
Index-eligible in wave one1 / 87 / 87 / 87 / 8
Toggle the rendering mode with the segmented control. Wave 1 is view-source (the raw response). Wave 2 is the rendered DOM. Each element is tagged first-wave, second-wave, or never-seen, and the score counts what is index-eligible in wave one. Every specimen is illustrative teaching markup, not a capture of a real page.

Notice the pattern as you toggle. In CSR the shell ships almost nothing, so the score collapses. In SSR, SSG, and ISR the HTML is in the first response, so the score jumps to near-full. Meanwhile the JS-only tab stays never-seen in every mode. That row is the reminder that some content is invisible to search no matter how you render.

Rendering strategy is an indexability decision, not a performance one#

Teams usually pick a rendering mode for speed. For an SEO-friendly frontend architecture, judge it first by what a crawler can see. Performance still matters, and we will get to crawl budget. First, though, separate the two questions cleanly. One question is what Google can index. The other is how fast the page loads for a human. They are related, yet they are not the same axis.

CSR: the near-empty shell#

Client-side rendering ships a shell and builds the page in the browser. So view-source shows almost nothing but an empty container and a script tag. The primary content does not exist until JavaScript runs, which means it is second-wave by construction.

view-source: a CSR SPA · html
<!doctype html>
<html lang="en">
  <head>
    <title>Acme</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="/assets/app.js"></script>
  </body>
</html>

For a public marketing page or a product listing, that is a real risk. The heading, the copy, and the structured data all wait for the render queue. Although Google can render them eventually, "eventually" is exactly the tax you are choosing to pay.

SSR and streaming SSR: first-wave HTML in 2026#

Server-side rendering sends full HTML in the first response, then hydrates in the browser. So your content is first-wave, and hydration is a progressive enhancement on top. In 2026 the picture is richer than the old SSR-versus-CSR split. Streaming SSR flushes HTML as it is ready. React Server Components render on the server and send no component JavaScript at all. Islands and partial hydration ship interactivity only where a page needs it. The common thread is that primary content lands in wave one. For the Cloudflare-specific tradeoffs, our guide on choosing SSR over CSR on Cloudflare walks the same modes at the edge.

SSG: full HTML in wave one#

Static site generation renders the HTML at build time and serves the same file to everyone. Therefore the response is complete on the first byte, with no per-request work and no render wait. This is the baseline an SEO-friendly frontend architecture should default to for content that does not change per request.

view-source: an Astro SSG page · html
<!doctype html>
<html lang="en">
  <head>
    <title>Trail runner, size 10 | Acme</title>
    <link rel="canonical" href="https://acme.example/p/acme-10">
    <script type="application/ld+json">{ "@type": "Product" }</script>
  </head>
  <body>
    <h1>Acme trail runner</h1>
    <main>...the full product copy ships right here...</main>
  </body>
</html>

The heading, the copy, the canonical, and the JSON-LD are all right there in the first-wave HTML. So wave one indexes the whole page, and there is nothing left for wave two to catch up on.

ISR: cached HTML with revalidation#

Incremental static regeneration serves cached HTML and refreshes it on a schedule or on demand. So a crawler still gets first-wave HTML, exactly like SSG. The one caveat is freshness. A page can be slightly stale until it revalidates, which is a fair trade for content that changes hourly rather than per request.

Pick by archetype: the decision table#

There is no single winner. The right mode depends on what the page is. Match your app archetype to a strategy, then read across to its first-wave indexability and its crawl-budget cost.

Match an app archetype to a rendering strategy, its first-wave indexability, and its crawl-budget cost
App archetypeRecommended strategyFirst-wave indexabilityCrawl-budget costCaveat
Marketing siteSSGFull HTML in wave oneLowestRebuild to publish changes
SPA dashboard (behind login)CSRNot indexed, and that is fineNone (not crawled)Never public, so SEO does not apply
E-commerce PLPSSR or ISRFull HTML in wave oneLowWatch facet URLs and canonicals
DocsSSGFull HTML in wave oneLowestLarge sets still need good internal links
BlogSSG or ISRFull HTML in wave oneLowISR if you publish very often

Crawl budget: when wave-two latency becomes a ranking tax#

On a small site, none of this bites. On a large one, wave-two latency turns into a real cost. Crawl budget is the number of pages Google will fetch and process in a given window. So when every page needs a render pass, you spend that budget slower, and new pages wait longer to appear.

The 10,000-page threshold#

Crawl budget is not a concern for most sites. Google says so directly. It matters mainly for large or frequently changing sites, and Google draws the line concretely.

Worked example: CSR versus static rendering on a 12,000-page catalog#

Consider an illustrative team shipping a 12,000-page catalog, just above Google's medium-site threshold. Two choices sit in front of them. First, a CSR single-page app, where the initial response is a near-empty shell and the content only exists after JavaScript runs. Second, an Astro SSG build, where full HTML ships in the first response and is indexed in wave one with no render wait. The CSR path spends crawl budget on rendering every page, so indexing of new and changed pages lags. The static path does not.

Now layer structured data on top, using Google's own published case figure. Take one product page ranking with 40,000 monthly impressions at a 3.5% click-through rate, which is 1,400 clicks per month. Adding valid JSON-LD with no change in ranking position, and applying Rotten Tomatoes' first-party measured +25% click-through rate from structured data, lifts that to 4.375%, or 1,750 clicks per month.

Static render plus JSON-LD versus a CSR shell, on one product page+25% CTR from markup alone, at the same ranking

1,400

CSR shell, no structured data

+350 / mo

1,750

Static render + Product JSON-LD

Static HTML gets the page indexed in wave one; the JSON-LD then adds +350 clicks a month per page from richer SERP presentation, with no ranking change. Results vary by infrastructure and configuration; the numbers are illustrative first-party figures, not a guarantee.

Static render plus JSON-LD versus a CSR shell, on one product page (clicks per month, one product page, at a fixed ranking position)
Optionclicks per month, one product page, at a fixed ranking position
CSR shell, no structured data1,400
Static render + Product JSON-LD1,750

Source: Google structured-data case study (Rotten Tomatoes)

Across 12,000 pages, even fractional adoption compounds. Meanwhile the human-facing budget still applies. Largest Contentful Paint must stay at or under 2.5 seconds at the 75th percentile to pass Core Web Vitals. For the full speed side of that story, see the full speed and Core Web Vitals playbook.

Semantic HTML is a ranking input, not an accessibility nicety#

Semantic HTML gets filed under accessibility, and it does help there. It also feeds ranking, through a chain few articles draw. Here is the chain. A clear heading and landmark structure helps Google extract the main content, which helps it index passages and segments of the page accurately. So a flat wall of divs makes that extraction harder, while a real outline makes it easy.

Fit and sizing#

Materials#

" data-language="html" data-astro-cid-jgrc2lfe>
a real landmark and heading outline · html
<body>
  <header><nav aria-label="Primary"> ... </nav></header>
  <main>
    <article>
      <h1>Acme trail runner</h1>
      <p>One clear intro paragraph the crawler reads first.</p>
      <h2>Fit and sizing</h2>
      <h2>Materials</h2>
    </article>
  </main>
  <footer> ... </footer>
</body>

One h1 states the topic. The h2 elements break the body into segments Google can index on their own. Landmarks like main and nav tell the crawler which bytes are the article and which are chrome. Because this structure ships in the first-wave HTML, it is first-wave signal, not something deferred to render.

Directives: canonical, robots meta, and what the crawler obeys#

Directives tell Google how to treat a URL. The ledger lens applies to them too. A directive in the first-wave HTML is honored reliably. One injected by JavaScript is a wave-two gamble.

Canonical: consolidating duplicate URLs#

Use a canonical tag to name the preferred URL when several show the same content. For it to be honored reliably, it must sit in the first-wave HTML head. Google uses it to consolidate duplicate URLs and focus signals on one page.

canonical in the first-wave head · html
<!-- In the first-wave HTML head, one self-referential canonical -->
<link rel="canonical" href="https://acme.example/p/acme-10">

<!-- A duplicate URL points at the primary, not at itself -->
<!-- https://acme.example/p/acme-10?color=blue -->
<link rel="canonical" href="https://acme.example/p/acme-10">

Ship it server-side, so it is present the moment Google parses the response. A canonical added later by client JavaScript may be missed or applied inconsistently, which defeats the point.

robots meta and X-Robots-Tag: noindex is a wave-two trap on CSR#

The robots meta tag controls whether a page is indexed. On a client-rendered page, injecting it with JavaScript is a genuine trap. Google reads a JS-added robots tag in wave two, so the timing is unreliable, and a noindex can misfire in either direction.

robots directive in wave one · html
<!-- Ships in the raw response: honored reliably in wave one -->
<meta name="robots" content="index,follow">

<!-- Or as a response header, read before a single byte of body -->
X-Robots-Tag: index, follow

Structured data: same bytes, a richer SERP result#

Structured data does not change your ranking. It changes how your result looks once it ranks. JSON-LD is a script in your HTML that describes the page in schema.org vocabulary, and Google reads it to build rich results.

Copy-paste, Rich-Results-validated JSON-LD#

Here are four common blocks. Each one maps to the exact rich result it can unlock. Add them to the first-wave HTML, then validate every one in Google's Rich Results Test before you ship.

html
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "TechArticle",
  "headline": "SEO-friendly frontend architecture",
  "author": { "@type": "Person", "name": "Kalpesh Patel" },
  "datePublished": "2026-01-06"
}
</script>

Article markup can surface the headline and date. Breadcrumb markup replaces the raw URL with a readable trail. FAQ markup can expand questions under your listing. Product markup can show price and availability. Each is the same bytes doing more work in the results page.

Structured data changes CTR, not ranking#

Return to the worked example above. The +350 clicks per month came from markup alone, at a fixed ranking position. That is the honest framing. Structured data will not push you up the results. Instead it makes the position you already hold more clickable, which is why the bytes-not-ranking thesis holds here too.

When NOT to reach for this#

An SEO-friendly frontend architecture is a means, not a goal. Some pages should never chase first-wave indexing at all. Naming those cases keeps the effort honest.

The first-wave visibility ledger: your SEO-friendly frontend architecture checklist#

Close the loop by auditing your own page the way the explorer does. Open view-source, not DevTools, because view-source is wave one. Then tag each element first-wave, second-wave, or never-seen.

Anything you tag second-wave is a candidate to move server-side. Anything never-seen is content search will never rank, so decide on purpose whether that is fine. This is the whole method of an SEO-friendly frontend architecture: make what matters first-wave, and prove it in view-source.

Talk to us about a crawlable frontend

Keep reading