Home / Guides

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:

  1. The page's own title and meta description.
  2. A self-referencing canonical and Open Graph tags, so shared links show the right page.
  3. JSON-LD for that page: the venue, the city list or the article.
  4. 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.

Raw HTML of the FantasyWorld SM City Fairview venue page, excerpt with real line numbers: its own title on line 44, a description starting Entry from 800 pesos on line 131, a canonical pointing to the venue's own URL on line 132, and the pre-rendered fallback with the H1 and an answer-first paragraph on lines 222 to 230

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.

Blanc audit of nearmekidsph.com/explore/quezon-city: 100 out of 100, AI crawler access 25 of 25, schema and entities 30 of 30, answer-ready content 30 of 30, technical readability 15 of 15 Blanc audit of the FantasyWorld SM City Fairview venue page: 93 out of 100, AI crawler access 25 of 25, schema and entities 27 of 30, answer-ready content 26 of 30, technical readability 15 of 15, with 4 small fixes left

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
Crawlly site audit of nearmekidsph.com before the fixes: score 42 out of 100, Needs work, 5 critical issues open, 141 pages audited, answer readiness 0, schema and markup 58 Crawlly site audit of nearmekidsph.com after the fixes: score 67 out of 100, Moderate, 3 critical issues open, 144 pages audited, answer readiness 64, schema and markup 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.

  1. 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.
  2. Probe your host. Publish two throwaway files, probe/index.html and probe2.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.
  3. Move page logic into shared modules with no React, browser or database code.
  4. Write the generator. After vite build, bundle the shared modules, fetch the data with the public key, and write dist/<route>/index.html for each route with its own head tags and readable content.
  5. Fail loudly. Stop the build if the data comes back empty. A failed publish keeps the old site; an empty page set doesn't.
  6. Keep the root index.html free of a canonical. It's still the fallback for routes you didn't pre-render.
  7. Generate the sitemap from the same data, with real lastmod dates.
  8. Plan the refresh, such as a data hash and a scheduled republish.
  9. Verify twice: one canonical and one description after the app loads, then curl one live URL of each type.
  10. 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.

Related guides

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

Checked November 6, 2026. Tell us at hello@heyblanc.com if something has changed.