Guide
How we made 144 React pages readable to AI crawlers in one day, without leaving Vite or Lovable
We pre-rendered all 144 public routes of NearMe Kids PH at build time, so every venue, city, guide and blog page now sends AI crawlers its own content, title, canonical and structured data. It took one day, and the site still runs on React, Vite and Lovable. Blanc now scores the homepage 99/100, a city page 100/100 and a venue page 93/100. A second tool, Crawlly, audited all sitemap pages the same day and moved from 42.4 to 66.8, with open issues down from 20 to 10.
This is the follow-up to how we took the homepage from 57 to 99. That fix covered one page. This one covers the other 143.
What was wrong with every page except the homepage?
Every address on the site sent AI crawlers the same 18,550-byte homepage. NearMe Kids PH is a single-page React app built with Vite and hosted by Lovable on Cloudflare. Every URL returns one index.html, and React draws the real page in the browser, including the title, canonical and structured data.
We checked it live on November 6, 2026, requesting pages as GPTBot:
| URL tested | What came back |
|---|---|
/, a venue, a city, a guide, a blog post |
The same 18,550-byte page, the homepage title, no canonical |
/does-not-exist |
The same page with status 200 |
Google and Bing run JavaScript, so they saw real pages. GPTBot, ClaudeBot and PerplexityBot don't, and Vercel and MERJ's study of live crawler traffic found none of the major AI crawlers render JavaScript. Facebook, Messenger and Viber link previews don't either, so a shared venue link showed the homepage.
It showed up in answers too. Asked about entrance fees and age limits at indoor playgrounds, AI assistants answered from Google Maps, even though our venue pages had exactly that data. The full background, from what crawlers can and can't read, is in what blocks AI crawlers.
What is build-time pre-rendering?
It means writing a real HTML file for each page when the site is built, so a crawler gets that page's own content without running any JavaScript. Our script runs right after vite build and writes 144 files: 108 venues, 10 cities, 3 guides, 13 blog posts and 10 main pages.
Each file is a copy of the built index.html with four things added:
- The page's own title and meta description.
- A self-referencing canonical and Open Graph tags, so shared links show the right page.
- JSON-LD for that page: the venue, the city list or the article.
- Readable content: an H1, an answer-first paragraph, key facts, questions and answers, and links to related pages.
The content sits inside the hidden fallback block we added for the homepage fix. Browsers that run JavaScript hide it before the first paint, and React replaces it when the app loads, so visitors see the same site. Crawlers that don't run JavaScript read it.
We rejected full server rendering with hydration. It would have touched every component of a desktop design that was already signed off, for little gain over static files.
What did we change, and why?
Ten changes, shipped in this order. Each one was built, checked locally in a server that mimics the host, and verified live with curl after publishing.
| # | Change | Why it matters |
|---|---|---|
| 1 | Pre-render 144 routes after vite build |
Crawlers and link previews see each page's own facts |
| 2 | Mark generated head tags data-rh="true" |
The app's head manager replaces them after load, so there's never a second canonical or description |
| 3 | Keep crawler content in the existing hidden fallback block | No flash and no visual change for visitors |
| 4 | Share code between the pages and the generator | Titles, schema, city lists and FAQs can't drift apart |
| 5 | Generate the sitemap from the database | The hand-kept one was missing 3 venues and 1 blog post, and had no dates for venues |
| 6 | Store a data fingerprint with every build | A weekly job republishes only when places, posts or FAQs changed |
| 7 | Connect entities by @id |
One WebSite and one Organization, linked from every page; a second, conflicting Organization was removed |
| 8 | Richer venue schema | Structured hours on 75 venues, phone on 69, correct ages on 108 |
| 9 | Answer-ready content | Venue FAQs on all 108 venues (they were on 2) and "Quick answers" on city pages |
| 10 | Data fixes and admin fields | Every fact the page shows can now be edited, including hours |
Only three changes were visible to visitors, all approved first: the FAQ section on venue pages, the Quick answers section on city pages, and punctuation in city descriptions.
Why mark the tags with data-rh?
Without it, every page would carry two canonicals and two descriptions after the app loads. react-helmet-async, which the app uses to set head tags, removes and replaces any tag that carries data-rh="true". Our generated tags carry it, so Helmet takes them over instead of adding copies. This also fixed a bug we didn't know about: live pages already had two description tags.
Why share code between the app and the generator?
So the HTML crawlers get and the page visitors see can't say different things. Showing crawlers content that visitors never see is cloaking, which search engines treat as spam. We moved title builders, schema builders, FAQ builders and the city list into plain modules with no React, browser or database code. The page and the generator import the same files, and Vite's SSR build bundles them for the script.
Where do the FAQ answers come from?
From the listing data only. A question with no data behind it is skipped, never filled in. If a venue has no hours on file, its page has no hours question. City pages went from 88 to 100 on this change alone.
Google stopped showing FAQ rich results in Search on May 7, 2026, so FAQPage schema won't add anything to Google's results. Its value is the question-and-answer format itself, which AI answers quote from, and the schema still has to match the visible questions.
What do the audits show now?
Every page type now scores 93 or higher on Blanc, and every page sends crawlers its own title and canonical.
| Page | Before | After | What moved it |
|---|---|---|---|
Homepage (/) |
57 | 99 | The fallback and Organization schema, done on November 4 |
City (/explore/quezon-city) |
88 | 100 | Visible Quick answers and FAQPage schema |
Venue (/place/fantasy-worlds-sm-city-fairview) |
82 | 93 | Venue FAQs, richer venue schema, an answer-first first paragraph |
The city and venue "before" scores are their first runs after pre-rendering. Before pre-rendering, those pages had no content of their own to measure: every crawler got the homepage.
The venue page's 7 missing points are content and data, not crawler access: payment methods and a phone number missing from its schema, and two answers that open without a number.
What did a second tool find across the whole site?
Crawlly, an AI search audit tool we don't own, audits every page in the sitemap. On November 6 its score went from 42.4 at 9:24 AM, before the fixes went live, to 66.8 that afternoon. Its dashboard rounds them to 42 and 67.
| Crawlly site audit | Before (9:24 AM) | After (afternoon) |
|---|---|---|
| Overall score | 42.4, "Needs work" | 66.8, "Moderate" |
| Open issues | 20, 5 critical | 10, 3 critical |
| Pages audited | 141 | 144 |
| Answer readiness | 0 | 64 |
| Schema and markup | 58 | 93 |
Some of Crawlly's remaining suggestions, such as ROI pages, pricing pages and comparison pages, fit a software company better than a directory of kids' places. We left those alone.
What went wrong along the way?
Most of the day went into problems we only saw when we tested live. These are the ones worth checking first on your own site.
| Pitfall | What happened | Fix |
|---|---|---|
| Unknown host behaviour | Lovable doesn't document how it serves per-route files | One throwaway probe publish answered it |
.html versus folders |
/x served x/index.html, but x.html fell back to the homepage |
Always write route/index.html |
| Duplicate head tags | Static tags stayed beside Helmet's | data-rh="true" on generated tags |
| Browser-only code at build time | The data hook imported a database client that needs localStorage |
Plain modules plus a small row mapper for the build |
| Stale HTML | Static files only change on publish | Weekly job that compares a data hash |
| Missing timestamps | Blog edits left no updated_at trace |
Hash the data itself, not timestamps |
| Wrong facts from an import | One venue showed "Open 24 hours" from a Google Places import, with no admin field to fix it | An admin field for hours; the sync keeps existing hours |
| A false "deploy done" signal | Our check matched text already on the old page | Poll the build timestamp instead |
One gap is still open on purpose. Addresses that don't exist still return the homepage with status 200. The usual fix, a 404.html file, is undocumented on Lovable hosting, and if it worked it would also return 404 for routes we don't pre-render, such as survey and campaign links. That would cost more than the soft 404 does, so we left it.
How do I do this on my own React or Lovable site?
Confirm the gap, probe your host, then generate one HTML file per route from your app's own data. These steps fit any client-rendered app whose data can be read at build time, including sites built with Lovable, Bolt or Cursor.
- Confirm the gap. Request three different page types with
curl -A GPTBot. The same title or byte size on all three means crawlers see one page. - Probe your host. Publish two throwaway files,
probe/index.htmlandprobe2.html, one of them written by a post-build script. You'll learn whether post-build steps run, which file style is served at a clean URL, and whether it adds trailing-slash redirects. - Move page logic into shared modules with no React, browser or database code.
- Write the generator. After
vite build, bundle the shared modules, fetch the data with the public key, and writedist/<route>/index.htmlfor each route with its own head tags and readable content. - Fail loudly. Stop the build if the data comes back empty. A failed publish keeps the old site; an empty page set doesn't.
- Keep the root
index.htmlfree of a canonical. It's still the fallback for routes you didn't pre-render. - Generate the sitemap from the same data, with real
lastmoddates. - Plan the refresh, such as a data hash and a scheduled republish.
- Verify twice: one canonical and one description after the app loads, then
curlone live URL of each type. - Then improve the content: facts in the first paragraph, Q&A from real data, entities linked by
@id.
Run a free Blanc audit on a few page types afterwards. It shows each page's raw-HTML word count and flags subpages that still send the homepage's content.
When will we know if it worked?
Not yet. A higher score means crawlers can read each page; it doesn't prove AI assistants will cite it. We'll compare the same reports on December 4, 2026 and February 2, 2027, 30 and 90 days after the first fix, and publish the numbers either way.
The starting points, all from the three months to early November 2026:
- Google AI Overviews and AI Mode: about 20,700 impressions across 133 pages (the full report).
- Microsoft Copilot: 652 citations, led by the homepage (189).
- AI referral visits in Google Analytics: 170, of which ChatGPT sent 160.
What would count as success: more of those impressions, citations and visits landing on venue and city pages, not only the homepage and blog. Regular Google clicks shouldn't drop.
Crawlly is a trademark of its owner. We are not affiliated with it.
Questions
Do I have to move my React app to Next.js to be readable by AI crawlers?
No. We kept React, Vite and Lovable hosting. A script that runs after vite build writes one real HTML file per route from the same data and code the app uses.
Will pre-rendered pages go out of date?
Yes, unless you republish. Static HTML only changes on publish, so we store a hash of the data with every build and a weekly job republishes only when the live data no longer matches it.
Does a higher audit score mean AI assistants will cite the site?
No. It means crawlers can now read each page's own facts. We'll compare AI impressions, citations and referrals on December 4, 2026 and February 2, 2027, and publish the numbers either way.
Sources
- Vite: Server-side rendering and SSR builds
- react-helmet-async on GitHub
- Google Search Central: FAQ structured data (FAQ rich results no longer shown from May 7, 2026)
- The rise of the AI crawler, Vercel and MERJ, December 2024
Checked November 6, 2026. Tell us at hello@heyblanc.com if something has changed.