SEO & GEO — What a Crawler Actually Sees

Search crawlers and most LLM crawlers/retrievers (GPTBot, ClaudeBot, PerplexityBot, CCBot) fetch raw HTML — they don't execute JavaScript. So the real test isn't "does it look right in a browser," it's "what does curl get back." Verified directly, not assumed.

StackPer-route <title>/description a crawler seesTrail content in raw HTML (Browse page)Why
vanilla-static✓ Yes — real✗ No (0 of 7 trails)Separate real files give free per-page titles; card content is still filled in by client JS
vanilla-spa✗ No — one generic title for every route✗ No (0 of 7 trails)Single index.html shell; JS updates the tab title after load, invisible to a crawler
site-server✓ Yes — real, from front matter✓ Yes (7 of 7 trails)Per-request server renders real title/meta + real trail card HTML before sending
react✗ No — one generic title for every route✗ No (0 of 7 trails)CSR-only Vite build; same document.title-after-load limitation
vue✗ No — one generic title for every route✗ No (0 of 7 trails)Same limitation, via router.afterEach instead of a hook
angular✗ No — one generic title for every route✗ No (0 of 7 trails)Same limitation, via Angular's own Title/Meta services
The trap we walked into on purpose, to show it: vanilla-spa, react, vue, and angular all got a real code change — a per-route document.title/meta-description update (Angular via its built-in Title/Meta services, Vue via router meta + afterEach, the others directly). It looks like an SEO fix, and it does correctly update the browser tab and any social-preview bot that happens to run JS. But a plain HTTP crawler — including most LLM crawlers — never executes that code, so it still receives the exact same generic title and empty content it did before the change. Only vanilla-static (separate real files) and site-server (a real per-request response) are actually improving what a crawler receives.
Why site-server can show real content and the others can't, cheaply: it already does per-request logic, so rendering real trail card HTML from trails.json before sending the response is a small addition. Doing the same for vanilla-static would mean adding a build step (a static-site generator) — which gives up its own no-build-step pitch. Doing it for vanilla-spa/react/vue/angular means true SSR — Next.js, Nuxt, or Angular Universal — not a small addition, a different architecture.