← Back to blog

Why this site moved from client-only rendering to static prerendering

A practical look at the architectural change behind this portfolio, why client-only rendering was limiting and how a Vite prerender step gives each important route real HTML.

The problem with a client-only portfolio

This site started as a Vite-powered React single page app. That made iteration quick: components were easy to build, routing stayed simple and the deployment target could serve one static shell for every URL.

The trade off was that every meaningful route depended on JavaScript before the page actually became useful. The server could return the same app shell for the homepage, portfolio, résumé and blog posts but the actual page content only appeared after React downloaded, parsed and rendered.

That is workable to start with, but it is not ideal for a content heavy portfolio.

What changed

The architecture now keeps Vite and React, but adds a build time prerender step.

The build still creates the JavaScript and CSS bundles. After that, a small Node script loads the app through Vite's server side module pipeline, renders known routes to HTML and the writes those files into the final dist folder.

That gives us static HTML for:

  • the homepage
  • the portfolio page
  • the blog index
  • the résumé page
  • every blog post

The client app still hydrates afterwards, so interactive features remain available.

Why this was the right first move

A full framework migration would have been possible but it would have changed more than necessary and taken more time. The goal was to improve the delivery model without losing the existing component structure.

Prerendering gives a useful middle ground:

NeedClient-only SPAStatic prerendering
Fast local developmentStrongStrong
Static hostingStrongStrong
Real HTML per routeWeakStrong
Route-specific metadata before JavaScriptWeakStrong
Interactivity after loadStrongStrong
Migration effortLowModerate

How the route list is generated

The static routes are explicit, while blog routes are generated from the content collection. That means publishing a new post automatically makes it eligible for prerendering and sitemap inclusion.

This is important because the content model becomes the source of truth. I do not want to remember to update three different files every time a blog post is published because of one change.

Hydration keeps the app behaviour intact

The static HTML is not a replacement for React. It is the first response.

Once the JavaScript bundle loads, React hydrates the markup and takes over client side navigation, form behaviour, animation and presentation controls. That gives users and crawlers a complete document up front while preserving the interactive experiences.

What this improves architecturally

The main benefits are:

  • Better resilience: useful HTML exists even before JavaScript completes.
  • More predictable deployment output: each public route has a generated file.
  • Cleaner content discovery: the sitemap is generated from the same route list.
  • Less framework churn: the site gets SSG style benefits without a full migration.
  • A safer migration path: if the site later moves to Astro or another SSG, the concepts are already in place.

What this does not solve by itself

Prerendering does not automatically make every interactive state indexable. A filtered portfolio view still needs a stable URL if we want it treated as a landing page. It also does not replace good content structure, internal linking or sensible metadata.

It does, however, establish the foundation those improvements need: real pages, generated at build time, with meaningful HTML and metadata in the initial response.