Case study: a single page application that Google received empty
By Carlos Alberto Cardona B. · published · updated
This is the case that taught me not to trust a dashboard. The site looked perfect in the browser and, to Google, it was a blank page repeated 24 times. In job-ad language: a single page application SEO problem, JavaScript rendering, soft errors and redirect mapping.
What follows is the measured problem, what I did to fix it, and how it looks in Search Console two months later. At the end is what I can't claim, which counts too.
What needed solving
regresoempresarial.com is the business insolvency consultancy site of Carlos Alberto Cardona O., my father. It's built on Base44, a platform that generates React applications: the content is drawn with JavaScript after the page loads.
In the browser everything looked fine. Requested as Googlebot, the home page returned the title and an empty container. The search engine had to execute JavaScript to see anything, and when it did, it received the same body at each of the 24 addresses.
<title>Inicio</title><div id="root"></div>→ 0 words of content for the crawler
<title>Reorganización empresarial en Bogotá | …</title><h1>…</h1> + 1,674 visible words + JSON-LD→ Google receives the full page
Routes that didn't exist answered 200 with the home page: to Google they were duplicate pages, not errors.
Redirects were temporary (302) instead of permanent (301), so the search engine consolidated nothing.
Titles and descriptions were defined in three different places and didn't match each other.
What was done
I didn't hand-code anything. I specified each piece, generated it with Claude Code, tested it with two inputs that had to produce different outputs, and verified it in production with the Googlebot user agent. When a test returned the same thing for two inputs, the piece started over.
Content served from the first byte: at build time an HTML file is generated per route with its text, its metadata and its structured data (service page, in Spanish). Google no longer needs to execute JavaScript to read the page.
A Cloudflare Worker in front of the site: real 301 redirects, a real 404 for what doesn't exist, and a sitemap that lists only what's indexable.
Metadata with a single source: an automated check warns if any copy of the titles, descriptions or routes drifts from the others.
Internal linking by design, measured on Sep 14, 2026 over the 24 published routes: 25 in-text links to the pillar page and 69 links to official sources such as the Superintendencia de Sociedades, the Senate and the DIAN (the tax authority).
19 automated checks before every publish: visible questions match the declared ones, no code leaks into the HTML, internal links point to real routes, metadata matches across all its sources. The build fails if one doesn't pass.
$ curl -sI https://regresoempresarial.com/sitemap.xml | grep x-edge-routerx-edge-router: authoritative-sitemap-xml$ curl -s -o /dev/null -w "%{http_code}" https://regresoempresarial.com/no-existe-123404
How it turned out later
With the owner's authorization I publish the Search Console figures. They belong to a new site in a small niche: what they show is not a business, it's that Google now receives the content and shows it.
Weekly impressions on Google, July to September 2026
Each point is a full week, Monday to Sunday. The vertical line marks the multi-route prerender commit (Aug 18, 2026); the other changes landed in the following weeks.
- Impressions
- Clicks
Source: Google Search Console (API), propiedad de dominio · read on 2026-09-19 · window 2026-07-11 to 2026-09-17
Show the chart values
| Week of | Impressions | Clicks |
|---|---|---|
| 2026-07-13 | 65 | 1 |
| 2026-07-20 | 83 | 0 |
| 2026-07-27 | 74 | 0 |
| 2026-08-03 | 53 | 1 |
| 2026-08-10 | 83 | 2 |
| 2026-08-17 | 147 | 0 |
| 2026-08-24 | 244 | 4 |
| 2026-08-31 | 284 | 2 |
| 2026-09-07 | 319 | 4 |
10.8 → 42.9
impressions per day, July versus September
GSC · 21 days of July · 17 of September
1,504
impressions in 120 days, with 17 clicks
GSC · May 22 to Sep 17, 2026
28.3
average position in the window; in July it hovered around 50
GSC · average weighted by impressions
Search Console · grouped by month · read on Sep 19, 2026
| Month | Days with data | Impressions | Impressions per day | Clicks |
|---|---|---|---|---|
| July | 21 | 227 | 10.8 | 3 |
| August | 31 | 547 | 17.6 | 7 |
| September (to the 17th) | 17 | 730 | 42.9 | 7 |
The Worker leaves a trail too. In the last 30 days it served 63,095 requests at the edge, and the status code distribution says whether the real 404 works with real traffic, not just in my terminal.
Cloudflare GraphQL Analytics, zona regresoempresarial.com · 30 days to 2026-09-19 · 63,095 requests
| Code | Requests | Share | What it means |
|---|---|---|---|
| 200 | 46,151 | 73.2% | Page or asset served |
| 301 | 5,785 | 9.2% | Permanent redirect (www, trailing slashes) |
| 204 | 4,358 | 6.9% | Correct empty response (analytics) |
| 404 | 3,446 | 5.5% | Nonexistent page: a real 404, not the home page in disguise |
| 403 | 2,314 | 3.7% | Blocked by Cloudflare (bots) |
53,680
Worker invocations in 30 days
Cloudflare GraphQL Analytics, Worker regreso-edge-router
10
errors in those 30 days, all on Sep 17 (0.02%)
I declare them because a round zero would be a lie
There's one report the API doesn't expose and the owner exported by hand from the interface: impressions in generative AI features (AI Overviews and AI Mode). I publish it with its source because it's the only thing I can say about AI visibility without making it up.
79
impressions in AI Overviews and AI Mode, Aug 22 to Sep 17, 2026
Google Search Console, generative AI features report (interface, Excel export)
16
distinct pages shown in those features
from zero in the first days of the window
71 of 79
impressions from Colombia; the rest from the United States, Chile and Panama
the report carries no clicks: impressions only
The five pages with the most impressions in AI features · Search Console interface · exported on Sep 19, 2026
| Page | Impressions in AI features |
|---|---|
| /blog/embargo-cuenta-empresarial-dian | 29 |
| /ley-1116-historial-crediticio | 8 |
| /blog/impagos-empleados | 5 |
| /blog/incapacidad-pago-inminente | 5 |
| /reorganizacion-vs-liquidacion | 5 |
What I learned, mistakes included
There was a moment when the Worker replaced itself after every change. I blamed the wrong cause twice, and both times the data fit.
The real cause was another integration listening to the same event. Since then, before attributing a problem, I list everything that reacts to that event.
And I learned not to trust a dashboard: the platform's said everything was fine while the served HTML said the opposite. What a dashboard says is not evidence of what gets served.
If you want to check it yourself
You don't have to take my word for it. With curl installed, three commands show what Google receives today:
curl -s -A "Googlebot" https://regresoempresarial.com/ley-1116 | grep -c "<p"→ dozens of paragraphs in the served HTML, without executing JavaScriptcurl -s -o /dev/null -w "%{http_code}" https://regresoempresarial.com/lo-que-sea→ 404curl -sI https://regresoempresarial.com/sitemap.xml | grep x-edge-router→ authoritative-sitemap-xml
The full technical method, applied to this very site, is in the case about putting cardonaseo.com into production.
The tool I use to calculate difficulty and audit structured data is Cayu.
If your site is on React, Base44 or Lovable, the free diagnosis starts with exactly this check, and the single page application SEO guide explains the five symptoms.
What I can't claim
I don't publish positions per keyword: Search Console's query view anonymizes rare searches and its rows don't add up to the total. There's no Chrome real-user data either: the site doesn't have enough traffic yet for Google to report it. And the impressions in AI features come from an interface report the API doesn't expose: they get re-read by hand on every measurement and can't be automated.
And I don't attribute the curve to a single change. Between August and September there were several (prerender, Worker, linking, new content), and separating each one's effect would require an experiment I didn't run.
Next measurement
The dates are written before the data exists. If I haven't updated this page by then, call me on it.
| Date | What gets measured |
|---|---|
| 30-day impressions and clicks in Search Console; Worker HTTP codes and errors | |
| Quarterly comparison and real-user data (CrUX), if Google reports it by then |