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.
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.
- 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 --><title>indexedindexed<h1>indexedindexed<main>indexedindexed<a href>indexedindexedrel=canonicalindexedindexedld+jsonindexedindexed<img src>indexedindexedno SSR fallbacknever seennever seenSSGThe 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.
| Page element | CSR | SSR | SSG | ISR |
|---|---|---|---|---|
| Title and meta description | first-wave | first-wave | first-wave | first-wave |
| Main heading | second-wave | first-wave | first-wave | first-wave |
| Main content body | second-wave | first-wave | first-wave | first-wave |
| Primary navigation links | second-wave | first-wave | first-wave | first-wave |
| Canonical URL | second-wave | first-wave | first-wave | first-wave |
| Structured data | second-wave | first-wave | first-wave | first-wave |
| Hero image source | second-wave | first-wave | first-wave | first-wave |
| Content behind a JS-only tab | never-seen | never-seen | never-seen | never-seen |
| Index-eligible in wave one | 1 / 8 | 7 / 8 | 7 / 8 | 7 / 8 |
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.
<!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.
<!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.
| App archetype | Recommended strategy | First-wave indexability | Crawl-budget cost | Caveat |
|---|---|---|---|---|
| Marketing site | SSG | Full HTML in wave one | Lowest | Rebuild to publish changes |
| SPA dashboard (behind login) | CSR | Not indexed, and that is fine | None (not crawled) | Never public, so SEO does not apply |
| E-commerce PLP | SSR or ISR | Full HTML in wave one | Low | Watch facet URLs and canonicals |
| Docs | SSG | Full HTML in wave one | Lowest | Large sets still need good internal links |
| Blog | SSG or ISR | Full HTML in wave one | Low | ISR 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.
1,400
CSR shell, no structured data
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.
| Option | clicks per month, one product page, at a fixed ranking position |
|---|---|
| CSR shell, no structured data | 1,400 |
| Static render + Product JSON-LD | 1,750 |
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.
<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.
<!-- 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.
<!-- 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.
<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> <script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Blog",
"item": "https://acme.example/blog" },
{ "@type": "ListItem", "position": 2, "name": "Trail runner" }
]
}
</script> <script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [{
"@type": "Question",
"name": "Does client-side rendering hurt SEO?",
"acceptedAnswer": { "@type": "Answer",
"text": "It defers indexing to wave two." }
}]
}
</script> <script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Acme trail runner",
"offers": { "@type": "Offer", "price": "129.00",
"priceCurrency": "USD", "availability": "InStock" }
}
</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.
- h1 and headings: present in view-source? Aim for first-wave.
- Main content body: readable in the raw HTML, not injected later.
- Primary nav links: real
<a href>in wave one, so crawl paths exist without render. - Canonical URL: in the first-wave head, self-referential on the primary.
- Structured data: valid JSON-LD in the response, tested in Rich Results.
- Images: a real
srcand descriptivealt, not a lazy placeholder with no source.
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.