Guide
How we took a React directory from 57 to 99 on AI readability without leaving Vite
We added a static fallback to index.html: a factual summary, three answered questions, crawlable links and Organization schema that crawlers can read before React loads. NearMe Kids PH, our directory of kid-friendly places in Metro Manila, went from 57/100 to 99/100 on the Blanc audit without moving off React and Vite. Our first version of the fix also had two side effects, and this article covers those too.
What was wrong with NearMe Kids PH?
The HTML our server sent to crawlers held 9 words. Everything a parent needs, from venue categories to prices and cities, appeared only after JavaScript ran.
NearMe Kids PH is a parent-run directory of more than 100 kid-friendly places across 10 Metro Manila cities. It's a client-side React app built with Vite, the kind of setup Lovable, Bolt and Cursor produce by default. It had real search demand and a Smart Parenting PH feature, and it still scored 57/100 on its first Blanc audit, at 1:31 PM on November 4, 2026.
The breakdown showed where the points went:
- AI crawler access: 25/25. Robots.txt allowed every AI crawler in.
- Schema and entities: 23/30. The richer Organization schema existed, but only after JavaScript ran.
- Answer-ready content: 4/30. There was almost nothing in the raw HTML to quote.
- Technical readability: 5/15. No links and no canonical in the HTML crawlers received.
Crawlers were allowed in. There was just nothing for them to read.
Why couldn't AI crawlers read it?
Most AI crawlers don't run JavaScript, so they only see the HTML a server sends before any script loads.
In a React + Vite app, that HTML is mostly an empty <div id="root"> and a script tag. A browser runs the script and builds the page. Vercel and MERJ found in their study of live crawler traffic that none of the major AI crawlers render JavaScript, including OpenAI's GPTBot and OAI-SearchBot, Anthropic's ClaudeBot and PerplexityBot. Google's crawler is the exception.
So when a parent asked an AI assistant about indoor playgrounds in Quezon City, our site had almost nothing those crawlers could use.
What did we change?
We kept the React app exactly as it was and put plain HTML inside <div id="root"> in index.html. React replaces it as soon as the app loads, so visitors see the same site. Crawlers that don't run JavaScript now get real content.
The fallback has four parts:
- A header with the same H1 the homepage already used.
- A 134-word factual summary: who runs the site, more than 100 places across 10 cities, 11 categories, our price brackets in pesos, and the ₱200 to ₱800 range for indoor playgrounds.
- Three questions with one-sentence answers: what places cost, which cities we cover, and who runs the site.
- 29 plain links to the main pages, guides, city pages and individual places.
We also moved our full Organization schema into the <head>. It already existed, but only appeared after JavaScript ran. Now it includes the description, founders, founding date, social profiles and contact email in the raw HTML.
A simplified version of the structure:
<div id="root">
<header>
<h1>Kid-friendly places in Metro Manila: know the cost, ages,
and what to expect before you go</h1>
</header>
<main>
<p>NearMe Kids PH is a parent-run directory of more than 100 ...</p>
<section id="faq">
<h2>Common questions</h2>
<h3>How much do kid-friendly places in Metro Manila cost?</h3>
<p>...</p>
</section>
</main>
<nav>
<a href="/guides/...">...</a>
</nav>
</div>
<script type="module" src="/src/main.tsx"></script>
We didn't change the script tag that loads the app. The fallback was committed at 1:34 PM, three minutes after the audit. Fixing the two side effects described below took the rest of the afternoon, between other work: the flash fix went in at 2:37 PM and the canonical removal at 4:37 PM.
What changed in the audit?
The score went from 57/100 to 99/100, and the raw HTML went from 9 words to 339.
| Before | After | |
|---|---|---|
| AI readability score | 57/100 | 99/100 |
| Words in the HTML sent to crawlers | 9 | 339 |
| Crawlable links in the HTML | 0 | 29 |
| AI crawler access | 25/25 | 25/25 |
| Schema and entities | 23/30 | 30/30 |
| Answer-ready content | 4/30 | 29/30 |
| Technical readability | 5/15 | 15/15 |
| Issues flagged | 8, including 1 critical | 0 |
We re-ran the audit the same afternoon. The missing point comes from one of the three FAQ answers. "Which cities does NearMe Kids PH cover?" is answered with a list of city names, and Blanc only counts an opening sentence as factual when it includes a number, price, time or a clear yes or no. The other two answers open with a price range and a founding year.
A higher score means AI crawlers can now read the page. It doesn't prove AI assistants will cite it. The next section has our starting numbers, and we'll update it at 30 and 90 days.
How much traffic did AI tools send before the fix?
In the 90 days before the fix, AI tools sent NearMe Kids PH 170 visits, about 0.95% of all traffic, and ChatGPT sent 160 of them.
These numbers come from Google Analytics 4, for August 6 to November 3, 2026:
| Source | Visits | Engagement rate | Avg. engagement time |
|---|---|---|---|
| ChatGPT | 160 | 89% | 35 s |
| Gemini | 7 | 100% | 57 s |
| Copilot | 2 | 100% | 54 s |
| Perplexity | 1 | 100% | 1 min 25 s |
| Claude | 0 | ||
| Total | 170 | 90% |
Two things stand out. First, ChatGPT already sent visitors even though its crawlers don't run JavaScript. ChatGPT's search draws partly on Bing, which does render JavaScript, so our pages could reach ChatGPT users by that route. Second, AI visitors engaged more than average: 90% engaged, compared with 80% across the whole site, and ChatGPT visitors stayed more than twice as long as the site average of 15 seconds.
The real number is probably higher. Some AI visits arrive without a referrer and show up as direct traffic. GA4's built-in "AI Assistant" channel also missed 4 of these 170 visits.
How often did Microsoft Copilot cite us?
Bing Webmaster Tools reported 652 citations of NearMe Kids PH across Microsoft Copilot and its partners in the three months before the fix.
Citations grew from a handful per day in August to peaks of about 40 a day in October. The most-cited pages:
| Page | Citations |
|---|---|
| Homepage | 189 |
| Free kid-friendly places in Metro Manila (blog) | 148 |
| Toddler-friendly places in Metro Manila (blog) | 55 |
| What to do in BGC with kids (blog) | 34 |
| Parks and playgrounds guide | 29 |
Copilot also cited individual venue pages, such as Filinvest City Central Park and Fantasy World Makati. The top queries it answered with our pages included "free kids activities this weekend" and "child friendly city."
This fits the rest of the picture. Bing renders JavaScript, so Copilot could already read our pages, including venue pages, before the fix. The fallback matters most for the crawlers that don't render JavaScript, such as GPTBot, ClaudeBot and PerplexityBot.
We'll compare the same GA4 and Bing reports at 30 and 90 days after the fix. A rise wouldn't prove the fix caused it, but it's the closest measure we have, and we'll publish the numbers either way.
What went wrong with the first fix?
Two things. Visitors briefly saw the plain-text fallback before the app loaded, and every page would have inherited the homepage canonical tag. We caught both after applying the fix, before either caused damage.
Visitors saw a flash of plain text
The fallback inside <div id="root"> is plain HTML, so the browser draws it first and React replaces it a moment later. Parents saw unstyled text flash on screen before the real page appeared.
We fixed it with a small script and style block at the top of <head>, then wrapped the fallback in <div class="nm-static">:
<script>document.documentElement.classList.add("js");</script>
<style>
/* Browsers that run JavaScript hide the fallback before the first paint */
.js .nm-static { display: none; }
/* A loading skeleton fades in only if the app takes over 350 ms */
.nm-boot { display: none; }
.js .nm-boot { display: block; opacity: 0;
animation: nm-boot-in 220ms ease-out 350ms forwards; }
@keyframes nm-boot-in { to { opacity: 1; } }
</style>
- Visitors: the browser adds the
jsclass before drawing anything, so the fallback is hidden from the first frame. - Crawlers that don't run JavaScript: they never add the class, so the fallback stays visible to them.
- When React loads:
createRoot()replaces everything inside the root div, fallback and skeleton included.
One rule matters here. The hidden fallback must say the same things the app shows. Showing crawlers content that visitors never see is cloaking, which search engines treat as spam. Our fallback only restates facts that are on the live site, such as prices, cities and categories.
Every page inherited the homepage canonical
Blanc's first prompt also added a homepage canonical tag to index.html. A canonical tag tells search engines which URL is the main version of a page. In a React SPA, every route, from /place/... to /guide/..., is served from the same index.html. So a static canonical pointing to https://nearmekidsph.com/ told crawlers that every venue and guide page was a copy of the homepage, which can lead them to skip those pages.
Our app already set the correct canonical for each page with react-helmet-async, but only after JavaScript ran. Google would have seen the right one. GPTBot, ClaudeBot and PerplexityBot would have seen the homepage canonical on every page.
We removed the static canonical and confirmed with curl that venue pages no longer send it. With none in the raw HTML, crawlers treat each page as its own page, and the app still sets the right canonical for crawlers that run JavaScript.
What we changed in Blanc
- On React apps with more than one page, Blanc no longer recommends a static canonical, and a missing one costs no points.
- Blanc flags it as critical when a subpage's canonical points to the homepage.
Does this fix every page?
No. The fallback fixes the homepage, but every other page on a React SPA still sends crawlers the same homepage content.
When GPTBot requests a venue page, it receives index.html with the homepage summary, FAQ and links. It doesn't receive that venue's hours, entrance fees or age rules. The 29 links help crawlers find our pages, but each page's own facts are still behind JavaScript.
The lasting fix is pre-rendering: generating a real HTML file for each page at build time, so every venue page sends its own content. Tools like vite-ssg or a pre-render plugin do this without moving off Vite. That's our next step for NearMe Kids PH, and we'll publish the results here.
How do I apply this to my own React + Vite site?
Check your raw HTML, add a factual fallback inside the root div, move your schema into the head, and skip the static canonical if your site has more than one page.
- Check what crawlers get. Run
curl -s https://yoursite.com | wc -w. If the number is tiny, crawlers that don't run JavaScript see an almost empty page. - Add a fallback inside
<div id="root">. Include an H1, a short summary with real facts, two or three questions with one-sentence answers, and links to your main pages. Keep it consistent with what the app shows, and use only facts that are true. - Move your JSON-LD into the
<head>ofindex.html, so it's there before any script runs. - Don't add a static canonical if your site has more than one page. Set canonicals per page in the app, or pre-render.
- Pre-render your important pages when you're ready, so each one sends its own content.
- Re-run the audit to confirm the content now shows up in the raw HTML.
You can run a free audit at heyblanc.com. It shows your raw-HTML word count and gives you a fix prompt for Lovable, Cursor or Bolt.
Sources
- The rise of the AI crawler, Vercel and MERJ, December 2024