A single-page application built with React, Vue or Angular and served without server rendering sends the same document for every URL: a head, one empty div, and a script tag. Everything a person sees is assembled in the browser after that script runs. An agent that fetches the page with a plain HTTP request and stops there receives the shell, and the shell contains nothing. Check A7 measures exactly this, and on a client-rendered app it reads at or near zero, taking most of the rubric with it.
This post is for the team that owns such an app and cannot rewrite it this quarter: what the scanner sees, why the damage is not confined to one check, the three ways out in order of effort, and how to choose between them.
What the default gives you
As of this writing, the starter templates for all three frameworks produce a client-rendered application by default: a static index.html and a bundle. Each framework documents a server-rendering path, but none of them is the default output of the tool that scaffolds a new project. A team that started with the scaffold and shipped has a client-rendered site unless someone deliberately changed that.
The raw response for the homepage of such a site looks like this:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Acme</title>
<script type="module" src="/assets/index-3f2a1c.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
Open it in a browser and you see the whole site. Fetch it with curl and you see the above. The two fetches explains why most agents get the second view.
How A7 reads it
A7 is worth five points and is the only check in the discovery pillar rated High effort. The scanner fetches the homepage twice: once as a plain request, once in a headless browser that runs the JavaScript. It extracts the visible text from each, with script, style and SVG contents removed, and divides the raw length by the rendered length. A ratio at or above 0.8 scores five, between 0.5 and 0.8 scores three, below 0.5 scores zero. The evidence block on the report shows rawTextLength, renderedTextLength and the ratio.
On the shell above, the raw text is the title element and nothing else. A homepage that renders two thousand characters of visible copy scores a ratio of a few hundredths, and the recommendation the scanner writes for that case is blunt on purpose: an agent fetching your HTML sees almost nothing.
Google's documentation for JavaScript SEO basics is worth reading for one reason. It describes a crawler that fetches a page, queues it, and renders it later in a headless browser as a separate stage. That is documentation of how one crawler chose to handle client-rendered pages, not a description of what an agent does. An agent doing a plain fetch has no second stage, and the rubric scores the first stage because that is what most agents get.
Why one check becomes ten
A7 sits upstream of nearly every check that reads page content, because those checks parse the raw HTML too.
B1 looks for application/ld+json blocks in the raw document. If your JSON-LD is injected by a component after mount, it is not in the raw document, and the homepage scores zero of B1's five points even though a browser extension would show you perfect Organization markup. B2, which needs the types those blocks declare, follows it down.
C2 audits the link graph from the raw homepage. It wants at least five real anchor elements and no click-handled non-anchors before it awards the two points for real navigation. A shell has no anchors at all; your router may render impeccable a href elements after hydration, and the scanner never sees them. B3 finds no h1, no main, no nav. D2 finds no contact link and no Organization schema. The same absence is counted, check by check, across three pillars. The one thing the shell does deliver is the head, which is why B4 can pass on a site that fails everything else.
The fixes, in order of effort
The rubric rates each check Low, Medium or High. The order below follows that scale, and the first tier is worth doing whatever you decide about the rest, because its points are independent of rendering.
Low effort, nothing rendered differently
Everything that lives outside the application can be fixed today. An llms.txt and llms-full.txt at the root (A4), a robots.txt that names the six AI crawlers (A1, A2), a sitemap referenced from it (A3), and a security.txt (D6) are static files that do not care how the app renders.
The head of the shell is also static, and you control it. Put your Organization or WebSite JSON-LD directly into index.html rather than injecting it from a component. B1 awards three of its five points for a valid block on the homepage, and the block does not need to know anything the page renders. Give it a sameAs array and D2 recovers two points with it.
Medium effort, prerendering
Prerendering runs your application at build time, once per known route, and writes the resulting HTML to disk. The server then sends that HTML instead of the shell, and the bundle hydrates on top of it. For a marketing site with a fixed set of routes this is usually a build step and a list of paths, not a rewrite.
What it buys on the rubric: A7 goes to a ratio near 1 for everything that was known at build time, and B1, B3, C2 and D2 start seeing the real page. What it does not buy: any content fetched from an API after load. A product grid that calls your backend on mount is still empty in the raw HTML, and the ratio will land wherever the split between static and fetched text puts it. Measuring server-rendered parity has a script for finding that number per route before you commit to a build change.
The catch is routes you cannot enumerate. Thousands of product pages, or pages that depend on who is looking, do not prerender well.
High effort, server rendering
Server rendering produces the HTML per request, so every route, enumerable or not, arrives complete. For React that means moving to a framework such as Next.js or one of its peers; for Vue it means Nuxt; Angular ships its own server-rendering package. In every case it is an architecture change: components that touch window on first render break, data loading moves to the server, and the deployment target changes from a static bucket to something that runs code. This is what the A7 effort rating of High describes: the fix that addresses every route, and the one that costs weeks rather than days.
A decision guide for a team that cannot rewrite
The scanner measures A7 on the homepage, and an agent starts there too, so start with the routes an agent goes to rather than the application as a whole.
If your public site is a fixed set of pages and the application proper lives behind a login, split them. Move the public pages to a static generator or a prerendered build and leave the app as it is. Agents do not reach past your login, and the scanner does not crawl it; the app's rendering mode was never the problem.
If the public site and the app are one bundle with one router, prerender the public routes and accept a partial ratio on any route that fetches after load.
If your commercial pages are numerous and dynamic, prerendering will lift the homepage and leave the pages that sell. The choice is then between a server-rendering migration and living with the score, and the second is a legitimate answer as long as it is a decision rather than a default. The three tiers of fixes sets out what each tier tends to cost, and the implementation service exists for teams that want the migration done rather than described.
Check your own site
Run the free scan and read the A7 evidence block: the raw and rendered text lengths tell you at once whether you have a shell or a page. The thresholds and the effort ratings are published under discovery and access, and the checks that fall with it are under understanding and actionability.