Crawlers receive an empty shell from a Vue SPA. Instead of adopting SSR, serve bots a fully rendered page from a headless-Chromium sidecar with SSRF and loop guards and a never-5xx fallback.
If the real problem is that search and preview bots see a blank page, the proportionate fix is to render the real page for them specifically — not to rebuild your app around server-side rendering.
What a crawler actually receives
Load your Vue single-page app, view source, and look at what the server sent before any JavaScript ran. It is a near-empty shell: a <div id="app">, a script tag, almost no content. Your browser fills that shell in by executing the bundle. A search crawler or a social-media link-preview bot may or may not do the same — many do not, or do it inconsistently. For a private dashboard that does not matter. For your public marketing pages, blog, or product catalogue, it means the bot indexes an empty page: no title worth ranking, no description, no rich preview. That is the SPA SEO problem, and it never fully went away.
The reflexive fix is server-side rendering, which renders your component tree to HTML on the server for every request. It works, but it changes your whole application model — universal code that runs in both Node and the browser, a hydration step, an SSR server to operate — a permanent architectural commitment to solve what is, for most sites, a bot-only problem. VBWD takes a smaller path.
Dynamic rendering: render for bots, not for everyone
Dynamic rendering asks a different question. Instead of rendering on the server for all visitors, detect the class of client and serve accordingly. Humans get the normal SPA — fast, interactive, edge-cacheable. Bots get a fully rendered HTML snapshot produced on demand by a headless browser. Your application does not change; there is no universal-code rewrite and no hydration model to debug. You place a component in front of the app that knows how to produce a rendered snapshot when a bot asks for one.
VBWD implements this as a standalone sidecar service. When a request arrives from a bot, the edge routes it to the sidecar, which loads the page in headless Chromium, waits for the app to finish rendering, and returns the resulting HTML — the same content a human would see after the bundle executed, but delivered as static markup the crawler can read immediately. Human traffic never passes through Chromium.
The “never an empty shell” guarantee
The core promise is that a public page is never served as a blank shell to something that needs content. The renderer waits for the application to reach a rendered state before it captures the HTML, so the snapshot carries the real title, meta tags, body copy, and structured data — not the pre-hydration placeholder. That is what moves the needle for both ranking and link previews: the bot gets a complete, meaningful document on the first byte it can parse.
This pairs naturally with per-entity SEO. A plugin can attach SEO metadata to an individual entity — a dataset, a product, a catalogue item — so that entity’s page carries its own title, description, and social image, and the renderer turns that into a crawlable page. You get a catalogue where every item can rank on its own URL, with no SSR framework underneath.
The guards that keep it from hurting you
Putting a headless browser in your request path is as dangerous as it sounds if you do it naively. Two failure modes matter, and the sidecar is designed to contain both.
Server-side request forgery. A renderer that will load any URL you hand it is an open proxy into your internal network. The sidecar only renders your own public pages, not arbitrary attacker-supplied URLs. Without that guard, a headless renderer becomes a tool for probing internal services; with it, the render surface is limited to the site it exists to serve.
Render loops. If the renderer fetches a page that is itself served by the renderer, you can create a loop where Chromium renders a page that triggers another render. The sidecar includes a loop guard so a bot request cannot recursively re-enter the renderer — the kind of bug that is invisible in testing and catastrophic under a live crawl, so it is designed out rather than patched later.
Failing without taking the site down
A rendering sidecar is an extra moving part, and extra parts fail. The important decision is what happens when it does. The sidecar is built so a rendering failure never becomes a 5xx for a visitor: if the renderer errors, the request falls back rather than returning a server error to the crawler, and a human was never routed through the renderer in the first place. The renderer is an enhancement layer, not something the whole site’s availability rests on. You can deploy, restart, or scale it independently, and if it is briefly unavailable, your public pages still respond — the opposite of SSR, where the render server is the request path and its downtime is your downtime.
SSR versus the sidecar: choose against your constraint
Neither approach is universally right.
Choose SSR when your pages must be fast and fully rendered for every human on first paint — content-heavy sites where first-contentful-paint on real devices is a ranking and conversion factor, or where you want streaming HTML. SSR renders for humans too, so it improves perceived performance, at the cost of the permanent architectural weight.
Choose the sidecar when your problem is specifically that bots do not see your content and you do not want to rebuild your app around universal rendering. You keep a plain client-rendered SPA — simpler to write and reason about — and bolt on crawlability as an isolated service. The honest limitation is that dynamic rendering does nothing for human first-paint performance; it is a crawlability solution, not a speed one. If your Lighthouse scores for real users are the problem, this is not the fix. If your problem is “Google and social previews see an empty page,” it is exactly the fix, at a fraction of SSR’s complexity.
Running it well
A few operational notes. Headless Chromium is memory-hungry — give the sidecar its own resource envelope so a burst of crawler traffic cannot starve your API. Cache rendered snapshots where you can, since crawlers re-fetch and re-rendering identical pages wastes CPU. Make sure the asset references in the rendered HTML point at the correct hashed bundles, or you can ship prerendered pages that reference stale assets. And keep the bot-detection rule honest: over-matching sends humans through Chromium and slows them; under-matching leaves bots on the empty shell.
The takeaway
SPA SEO does not require adopting server-side rendering and carrying its complexity for the life of the project. If the real problem is that crawlers and preview bots receive an empty shell, the proportionate fix is to render the real page for them specifically — with a headless-Chromium sidecar that guards against request forgery and render loops, falls back instead of returning a server error, and leaves human traffic on the fast client-rendered path. You keep a simple Vue app, and your public pages — down to individual catalogue entities — become fully crawlable. The full design lives in the VBWD docs.