Client routing / crawlability

33o1.com is a single-page app: one document answers every path, and a router in the browser decides what to draw. Most of its URLs have no file behind them.

  • Shell: one 14,339-byte document answers any unresolved path
  • Prerendered: five static documents, 18,214–22,621 bytes on the wire, one per advertised route
  • Guard: a source-only test asserting strings are absent from what is served

Every byte count below was taken from the live site with the commands printed alongside it, so you can re-take them rather than trust them: this section on 2026-08-18, the cloaking section on 2026-08-19.

Why a route can have no file behind it

A client router reads the path and swaps the matching view module into a container. The server is never asked about /About — the browser was already running when the URL changed. That survives a reload or a pasted link only if unrecognised paths return the shell, not a 404.

The cost is undersold: your 404 is gone. The server returns 200 and HTML for everything, so a mistyped asset path looks like a successful load, and “not a route” becomes the app’s own decision.

What a crawler receives instead

The shell has no content in it; the words arrive when JavaScript runs. Rather than bet on the crawler executing it, each of a small allowlist of named routes gets a prerendered static document holding the text the browser would have built. A crawler asking for such a path receives that file; a browser receives the shell and routes normally.

The size gap is the story, and it is the one claim on this page you should not take on faith. Two commands return it. The first asks for a path that is not a route, so what comes back is the fallback shell:

$ curl -s -o /dev/null -w '%{size_download}\n' https://33o1.com/NoSuchPathXyz
14339

The second asks for each advertised route while identifying as Googlebot, which is what decides whether you are handed a prerendered document or the shell:

$ for u in / /Map /About /Changelog /Privacy; do
    curl -s -A 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' \
         -o /dev/null -w "$u %{size_download}\n" "https://33o1.com$u"
  done
/ 18231
/Map 19546
/About 18214
/Changelog 22621
/Privacy 20368

So the gap is real but uneven: the smallest prerendered document, /About, is 3,875 bytes larger than the 14,339-byte shell — 27% more — and the largest, /Changelog, is 8,282 bytes larger, because a changelog accumulates entries and the other four routes do not. Those extra bytes are the rendered view. Diff the two documents and the difference is a <div id="app"> that is empty in the shell and full in the prerendered copy; both ship the same eight <script> tags, so nothing is being added or withheld except the view itself.

The prerendered documents are regenerated on a schedule outside the repository, so these exact byte counts drift — which is why this section prints the commands instead of asking you to trust a number. How often, I have not measured and will not guess: the copy checked into the repo carries a prerender-time of 2026-08-05T02:30:07Z and the copy the live server handed me carries 2026-08-17T02:30:03Z. Two stamps twelve days apart, both within seconds of 02:30Z, tell you the regeneration is scheduled and roughly when in the day it runs. They do not tell you the interval, and reporting one from two samples would be inventing it.

The failure that makes it worthless

A sitemap entry promises a distinct document exists. On 2026-08-05 two routes were listed with no prerendered file behind them; the fallback did its job and handed the crawler the shell, so both URLs returned the homepage body byte for byte — duplicate content produced by the safety net. A guard now pins the other half: every sitemap location must name a real prerendered file whose canonical agrees with it. It reports “5 sitemap <loc>s, each mirrored by a prerender file with a matching canonical”. Those entries were deleted, not prerendered.

Why this is not cloaking, and when it would be

Name the mechanism first, because the rest of this section is worth nothing if I dodge it: the split is a branch on the User-Agent request header. Same URL, same second, same server — change that one header and a different document comes back.

$ curl -s -A 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' \
       -o /dev/null -w '%{size_download}\n' https://33o1.com/About
18214
$ curl -s -o /dev/null -w '%{size_download}\n' https://33o1.com/About
14339

Two commands, ten seconds, 3,875 bytes apart. Sending a Firefox 120 string returns 14339, and sending no user-agent at all (curl -H 'User-Agent:') returns 14339, so nothing else about the request is doing the deciding.

It is also not a Googlebot special case. A bingbot string returns 18214, and so do DuckDuckBot, Applebot, YandexBot, Baiduspider, Yahoo! Slurp, AhrefsBot, SemrushBot, Twitterbot and facebookexternalhit. The match is case-insensitive and anywhere in the string — GOOGLEBOT, and a Chrome string with googlebot glued on the end, both return 18214 — but it is a list of names, not a “contains the word bot” heuristic: SomeRandomBot/1.0 and a bare bot both get the 14,339-byte shell. So do GPTBot, ClaudeBot and Bytespider, which is a decision about AI training crawlers rather than anything to do with search.

So: yes, this branches on user agent. That is not what makes something cloaking. Cloaking is showing a crawler content the visitor does not get, in order to win a ranking. The header is only the switch; what sits on either side of it is what decides whether the switch is honest, and that is the two tests this build has to pass.

Direction: where the responses differ, the crawler gets less. Correspondence: each prerendered file is a snapshot of what the browser renders at that URL, so nothing exists for the crawler that a visitor cannot reach.

Correspondence survives naming the mechanism, and it survives partly because the branch is trivial to flip. The curl above claimed to be Googlebot from an address that is plainly not Google’s and the server took its word for it — there is no reverse-DNS check. That reads like a hole and is closer to the opposite: cloaking only pays when the visitor cannot see the crawler’s copy, and here anyone can see it with one flag in ten seconds. There is no second, hidden document to be caught with.

Direction is where re-measuring cost me a claim. This section used to say that a crawler asking for an unadvertised path gets the same shell a browser does. On 2026-08-19 that is false:

$ curl -s -A 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' \
       -o /dev/null -w '%{size_download}\n' https://33o1.com/NoSuchPathXyz
18231

18,231 bytes and HTTP 200, and cmp reports it byte-for-byte identical to the prerendered homepage — while a browser at that same path gets the 14,339-byte shell shown earlier. On paths that are not routes the crawler gets more, not less: the duplicate-homepage failure described two sections up, arriving through the crawler branch this time instead of the sitemap. What limits the damage is inside the document — its rel="canonical" reads https://33o1.com/, so the copy disclaims the URL that was asked for. That is a mitigation, not a pass, and the honest summary is that this build passes Correspondence and currently fails Direction on every path that is not a route. Whether the browser’s router also lands on the homepage there, I have not measured, so I will not claim the two correspond.

The test is not technical: would a reader who saw only the crawler’s copy feel sold something the page does not contain? If yes it is cloaking, however implemented. Do not branch on user agent to improve rankings — that is the harmful version of this shape, and not what the split is for here.

A test whose whole job is to prove nothing is there

This site withholds answers, so one guard exists purely to assert an absence. It derives the forbidden set from the project at runtime rather than hard-coding it, then reads every other served file and asserts none contains one. What matters is the two assertions before the scan: the forbidden set must be non-empty, and so must the file list. Absence tests fail by passing, so this one prints both counts on success (6 checks passed (… vs 15 files)).

What it caught: presence in the deployed directory is publication. Build scripts left inside the tree that gets mirrored to the server were served as plain source to anyone guessing a filename. Directory listing was off and the robots file did not mention them; neither is access control. Moving them out fixed it — ignoring them in version control would not, because the deploy mirrors and deletes.

When not to build it this way

If you are choosing now, render on the server or generate pages statically instead. Prerendering behind a fallback is a retrofit for an app that already exists, and it buys crawlability at the price of a second copy of every page — one that drifts unless something checks.

JavaScriptSEOSingle-Page AppPrerenderingTestingMeasurement

← Back to Projects