Single-page applications have a search-engine problem that never fully went away. Your Vue app renders in the browser, but the first thing a crawler receives is a near-empty HTML shell with a <div id="app"> and a script tag. Some crawlers execute JavaScript; many bots, previews, and scrapers do not, or do it inconsistently. The usual fix is server-side rendering, which means restructuring your app around an SSR framework and carrying that complexity forever. VBWD takes a different route: a small headless-Chromium sidecar that renders the real page for bots while humans keep getting the fast SPA. This post explains the design, the guards that keep it safe, and the trade-offs against SSR.
A client-rendered Vue app ships an HTML document whose body is essentially empty until JavaScript runs. For a logged-in dashboard that is fine — nobody needs to index it. For public, SEO-critical pages — a marketing site, a blog, a product catalogue, per-entity landing pages — it is a liability. When a crawler or a social-media link-preview fetcher requests the page and does not execute your bundle, it sees no title, no content, no meta description worth indexing. You lose ranking and you lose rich link previews.
SSR solves this by rendering the component tree to HTML on the server for every request. It works, but it changes your whole application model: universal code that must run in both Node and the browser, careful handling of browser-only APIs, a hydration step, and an SSR server to operate. That is a large, permanent architectural commitment to solve what is, for most sites, a bot-only problem.
Dynamic rendering flips the question. Instead of rendering on the server for everyone, you detect the class of client and serve accordingly: humans get the normal SPA (fast, interactive, cached at the edge); bots get a fully rendered HTML snapshot produced on demand by a headless browser. The application itself does not change — there is no universal-code rewrite, no hydration model to debug. You add a component in front of it that knows how to produce a rendered snapshot when a bot asks.
VBWD implements this as a standalone service — an SEO renderer sidecar. When a request arrives from a bot, the edge routes it to the sidecar, which loads the corresponding 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. Humans are never sent through Chromium; their requests hit the SPA directly.
The core promise of this design 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 contains the real title, meta tags, body copy, and structured data — not the pre-hydration placeholder. That is what makes the difference for both ranking and link previews: the bot receives a complete, meaningful document on the first byte it can parse.
This pairs naturally with per-entity SEO. In VBWD, a plugin can attach SEO metadata to an individual entity — a dataset, a product, a catalogue item — so that entity's public page carries its own title, description, and social image. The renderer turns that per-entity metadata into a crawlable page. The result is a catalogue where every item can rank on its own URL, without an SSR framework underneath.
Putting a headless browser in your request path is exactly as dangerous as it sounds if you do it naively. Two failure modes matter, and the sidecar is built to contain both.
Server-side request forgery (SSRF). A renderer that will load any URL you hand it is an open proxy into your internal network. The sidecar guards against this: it 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 itself is served by the renderer, you can create a loop where Chromium renders a page that triggers another render, and so on. The sidecar includes a loop guard so a bot request cannot recursively re-enter the renderer. This is the kind of bug that is invisible in testing and catastrophic under a crawl, so it is designed out rather than patched later.
A rendering sidecar is an extra moving part, and extra moving parts fail. The important design decision is what happens when it does. The sidecar is built so that a rendering failure never turns into a 5xx for a visitor: if the renderer errors, the request falls back rather than returning a server error to the crawler. A bot that would otherwise have received a broken response gets a usable one, and a human was never routed through the renderer in the first place. The renderer is an enhancement layer, not a dependency the whole site's availability rests on.
This matters operationally. You can deploy, restart, or scale the renderer independently, and if it is briefly unavailable, your public pages still respond. Contrast that with SSR, where the render server is the request path — if it is down, the page is down.
Neither approach is universally correct. Choose against your actual constraints.
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 user devices is a ranking and conversion factor, or where you want streaming HTML. SSR renders for humans too, so it improves perceived performance, not just crawlability. The cost is the permanent architectural weight described above.
Choose the dynamic-rendering 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, simpler to reason about — and bolt on crawlability as an isolated service. You accept that humans still get client-side rendering (with its first-paint characteristics) and that you now operate one more container.
The honest limitation of dynamic rendering is that it does nothing for human first-paint performance — it is a crawlability solution, not a speed solution. 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 precisely the fix, at a fraction of SSR's complexity.
A few practical points if you run this. 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 — crawlers re-fetch, and re-rendering identical pages wastes CPU. Make sure your 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 at the edge honest: over-matching sends humans through Chromium and slows them down; under-matching leaves bots on the empty shell.
SPA SEO does not have to mean adopting server-side rendering and carrying its complexity for the life of the project. If the actual 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 SSRF and render loops, falls back instead of 5xx-ing, and leaves your 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. It is the smaller commitment that solves the smaller, real problem.
The renderer runs as an independent service alongside the frontend; enable it for your public routes and verify a rendered snapshot with a bot user-agent before you rely on it.
