Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to our course on migrating your React SPA to a modern SSR/SSG architecture.
In our last lesson, we developed a crucial set of strategies for assessing and isolating third-party libraries that depend on client-side APIs. This ability to control the execution environment is fundamental not only for Server-Side Rendering (SSR) but also for our topic today.
This lesson focuses on the learning outcome: Configure static site generation (SSG) for specific routes within the Tanstack Start application. We will explore SSG as a powerful rendering strategy that complements SSR and CSR, and learn how Tanstack Start's architecture is designed to support hybrid applications that mix these approaches.
1. Understanding Static Site Generation (SSG)
Before diving into configuration, let's clarify what SSG is and where it fits in our architectural toolkit.
- Client-Side Rendering (CSR): The browser receives a minimal HTML shell and a JavaScript bundle. React runs in the browser to fetch data and render the page. This is the traditional SPA model.
- Server-Side Rendering (SSR): When a user requests a page, the server fetches data, renders the React components to an HTML string, and sends the fully-formed page to the browser. This happens on-demand for every request.
- Static Site Generation (SSG): The server renders React components to full HTML files once, at build time. The resulting static files (
index.html,about.html, etc.) are then deployed to a server or CDN. When a user requests a page, they are served a pre-built static file.
Key Benefits of SSG:
- Maximum Performance: Serving a static file is incredibly fast, leading to an excellent Time to First Byte (TTFB). There's no per-request server computation.
- Robustness & Scalability: Static files can be served from a global CDN, making your site highly available and able to handle massive traffic spikes with ease.
- Enhanced SEO: Search engine crawlers receive complete HTML content without needing to execute JavaScript.
SSG is ideal for content that doesn't change frequently, such as marketing pages, blog posts, documentation, or an "About Us" page.
2. The Hybrid Challenge: Mixing SSG and Dynamic Routes
A purely static site is simple. However, most real-world applications are hybrids. You might want to statically generate your landing page (/) and blog posts (/blog/some-post), but your user dashboard (/dashboard) must be rendered dynamically on the client because its content is specific to the logged-in user.
This hybrid model introduces a critical architectural challenge:
- A user visits
/blog/some-post(an SSG page). The server sendsblog/some-post.html. - The user then clicks a link to
/dashboard(a CSR page). The client-side router takes over and renders the dashboard. This works perfectly.
But what happens if the user directly navigates to /dashboard? The server looks for a static dashboard.html file, which doesn't exist because it's a dynamic route. The standard fallback for SPAs is to serve index.html. If your index.html is a pre-rendered homepage, the user will briefly see the homepage before the client-side JavaScript loads and re-renders the correct /dashboard content. This is known as a "flash of incorrect content."
This is a core problem that modern frameworks like Tanstack Start are designed to solve. The following resource, a discussion from the Tanstack Router repository, provides deep insight into the problem and the solution.
[RFC] SPA Mode Enhancements #3394
This GitHub discussion, initiated by a core contributor, explores the architectural nuances of supporting SPA-like behavior (i.e., client-side routing for some pages) within a pre-rendered or statically generated application. It's a technical read that reveals the 'why' behind the framework's design.
Read the first section of the discussion, from the beginning down to the heading '"Shell" vs "Index"'. Focus on understanding the distinction between a non-prerendered and a prerendered index.html and why using a prerendered homepage as a fallback for dynamic routes leads to a poor user experience.
3. The Solution: The Application Shell (_shell.html)
As you read, the solution is to decouple the prerendered homepage from the client-side routing fallback. Instead of one index.html serving two purposes, we create two distinct files:
- Prerendered Pages (
*.html): These are the fully rendered static pages for your SSG routes (e.g.,index.html,about.html). They contain all the HTML and content for that specific page. - The Application Shell (
_shell.html): This is a minimal HTML file. It contains the root layout of your app (e.g.,<html>,<head>,<body>), links to your CSS and JS bundles, but no page-specific content. It's an empty frame waiting for the client-side router to fill it in.
With this model, your web server is configured like this:
- If a request matches a static file (e.g.,
/about->about.html), serve that file. - For any other request (e.g.,
/dashboard), serve the_shell.htmlfile.
The browser receives the empty shell, the JavaScript bundle loads, and Tanstack Router immediately renders the correct /dashboard route. No flash of incorrect content.
Let's explore how the framework architects are thinking about implementing this.
[RFC] SPA Mode Enhancements #3394
Continuing with the same GitHub discussion, let's look at the proposed implementation details for generating this shell and how it would be configured.
Read the sections '"Shell" vs "Index"' and 'Implementation'. Pay attention to the idea of using the __root.tsx file to generate the shell and the clever technique proposed to 'remove' the <Outlet /> during the shell-rendering process. Also, note the proposed configuration in app.config.tsx.
4. Configuring SSG in Tanstack Start
Now let's translate this theory into practice. While the exact APIs are still evolving (as you saw in the RFC), the principles are clear. Configuration happens at two levels: enabling the build process and marking routes for prerendering.
A. Enabling the Shell Fallback
First, you need to tell Tanstack Start to generate the _shell.html file during the build. Based on the discussion, this would likely be a simple flag in your application config.
app.config.ts (Conceptual)
import { defineConfig } from '@tanstack/start/config';
export default defineConfig({
// This flag instructs the build process to generate the special
// _shell.html file for client-side routing fallbacks.
shell: true,
});
When you run bun run build, Tanstack Start will produce the _shell.html in your output directory (dist/client/). You would then configure your hosting provider (like Vercel or Netlify) with a rewrite rule to serve this file for any path that doesn't match a static asset.
B. Marking Routes for Prerendering (SSG)
With the shell fallback in place, you can now decide which routes should be statically generated. This is typically done on a route-by-route basis.
While Tanstack Start doesn't have a finalized, documented API for this yet, modern frameworks commonly use a route-level export. Let's imagine what this might look like.
In a route file, you could export a configuration object that tells the build process to prerender it.
app/routes/about.tsx (Conceptual)
import { createRoute } from '@tanstack/react-router';
import { rootRoute } from './__root__';
// This is a conceptual API for enabling SSG on a route
export const route = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
component: AboutComponent,
// By adding this, you signal to the build process
// that this route should be prerendered to about.html
prerender: true,
});
function AboutComponent() {
return (
<div>
<h1>About Us</h1>
<p>This page was generated at build time!</p>
</div>
);
}
The Build Process:
When you run the build command:
- The build tool sees the
shell: trueconfig and generatesdist/client/_shell.html. - It then scans your routes.
- For the
/aboutroute, it seesprerender: true. It will:- Run the route's
loaderfunction on the server to fetch any data. - Render the
AboutComponentto HTML. - Save the result as
dist/client/about.html.
- Run the route's
- For a route like
/dashboardwithout theprerenderflag, it does nothing. These routes will be handled by the shell fallback and client-side rendering.
This hybrid approach gives you the best of both worlds: the raw speed of static files for your public-facing content and the flexibility of a client-rendered SPA for your dynamic, user-specific application areas.
Conclusion
Today we've added the final major rendering pattern, Static Site Generation, to your toolkit. You now understand the trade-offs between CSR, SSR, and SSG, and more importantly, how to combine them effectively within a single Tanstack Start application.
Key Takeaways:
- SSG renders pages to static HTML at build time, offering the best possible performance for static content.
- In a hybrid application, mixing SSG and dynamic (CSR) routes creates a challenge: how to handle direct navigation to dynamic URLs.
- The Application Shell (
_shell.html) pattern solves this by providing a content-free fallback that initializes the client-side router without a "flash of incorrect content." - Configuring this involves two main steps: enabling shell generation in your app config and marking specific routes for prerendering.
Next Up:
We have now covered the full spectrum of rendering techniques and strategies for handling dependencies. However, the interaction between server-rendering, static generation, and client-side hydration can introduce a class of subtle but important bugs. In the final lesson of this module, we will learn how to test the migrated application to validate and resolve hydration errors, ensuring your hybrid application is not just fast, but also correct and robust.