How to Generate Static Pages with React Router and Shared Layouts
In a community I participate in, a developer recently asked: “How do I use React Router to generate pages that share a layout at build time? I want the SEO benefits of static files but the DX of React Router’s nested routes.”
This is a common pain point. By default, React Router is a Client-Side Routing (CSR) library. To generate pages at build time (Static Site Generation or SSG), you need to bridge the gap between your route definitions and a build script that outputs HTML files.
The Immediate Fix: The “Framework” Approach
Section titled “The Immediate Fix: The “Framework” Approach”The most robust way to achieve build-time generation with React Router today is to use React Router v7 (the successor to v6 and Remix). It allows you to define a layout once and “prerender” specific paths into static HTML files during the build process.
// React Router v7 / Remix - illustrative exampleimport { type RouteConfig, route, layout } from "@react-router/dev/routes";
export default [ // Shared Layout wrapper layout("layouts/MainLayout.tsx", [ route("about", "routes/About.tsx"), route("contact", "routes/Contact.tsx"), ]),] satisfies RouteConfig;
// react-router.config.tsexport default { // This triggers build-time generation (SSG) async prerender() { return ["/about", "/contact"]; },};Detailed Explanation
Section titled “Detailed Explanation”To generate pages at build time with shared layouts, you must solve two problems: Nested Routing (the layout) and Static Generation (the build step).
Solution 1: React Router v7 / Remix (Recommended)
Section titled “Solution 1: React Router v7 / Remix (Recommended)”React Router v7 merged with the Remix framework. It uses the Outlet component to handle shared layouts and a prerender configuration to identify which routes should be turned into HTML at build time.
Version: React Router v7 / Node 20+
- Define the Layout: Create a file (e.g.,
layouts/MainLayout.tsx) that uses the<Outlet />component. This component acts as a placeholder for child routes. - Configure Routes: Use the
layout()helper in your route configuration to wrap specific routes. - Enable Prerendering: In your
react-router.config.ts, export aprerenderfunction. Duringnpm run build, the library will spin up a server, crawl these paths, and save the resulting HTML to yourdistfolder.
Why this works: It keeps your code DRY (Don’t Repeat Yourself). The MainLayout is rendered into every static HTML file generated, ensuring that users see the header/footer immediately upon page load without waiting for JavaScript.
Solution 2: Vite + Vite-Plugin-Ssg (For Standard React Router v6)
Section titled “Solution 2: Vite + Vite-Plugin-Ssg (For Standard React Router v6)”If you are using a standard Vite + React Router v6 setup and cannot migrate to the full framework model, you can use vite-plugin-ssg or vite-prerender.
Version: React 18, React Router v6.x, Vite 5
import { createBrowserRouter, RouterProvider } from "react-router-dom";import Layout from "./Layout";import Home from "./Home";
export const routes = [ { path: "/", element: <Layout />, // Your shared layout children: [ { path: "home", element: <Home /> }, { path: "about", element: <About /> }, ], },];
// For SSG, you often export the router or routes for a build scriptexport const router = createBrowserRouter(routes);Implementation Steps:
- Shared Layout: Use the
<Outlet />inside yourLayoutcomponent. - Build Script: Use a plugin like
vite-plugin-ssg. It hooks into the Vite build process, renders your React tree to a string usingreact-dom/server, and writes physical.htmlfiles for every route you provide.
Why this works: It intercepts the build process. Instead of delivering a single index.html with a blank <div>, it generates about.html and home.html containing the full markup of your layout and the specific page content.
Edge Cases and Troubleshooting
Section titled “Edge Cases and Troubleshooting”- Hydration Mismatches: If your layout uses logic that depends on the browser (like
window.innerWidth), the static HTML generated at build time won’t match the first render on the client. This causes a “Hydration Error.” Always wrap browser-only logic inuseEffect. - Dynamic Routes: Generating a page at build time for a route like
/blog/:idrequires you to provide a list of all possible IDs to the build script. If you have 10,000 blog posts, your build time will increase significantly. - Context Providers: Ensure your
UserProviderorThemeProviderwraps theOutletor the entireRouterProviderso that the static state is captured correctly during the build-time render.
Follow-up Concerns
Section titled “Follow-up Concerns”1. Is React Router v7 different from Remix? As of late 2024, they are the same thing. The Remix team merged the projects to simplify the ecosystem. If you want “build-time” pages, using the React Router v7 framework features is the official path forward.
2. Does this eliminate the need for a server?
Yes. Once you generate these pages at build time, you can host the dist or build folder on any static host like Netlify, Vercel, or S3/CloudFront. You only need a server if you decide to use Server-Side Rendering (SSR) instead of SSG.
3. Will my shared layout re-render on every page change?
When the user navigates between static pages after the initial load, React Router takes over as a Single Page Application (SPA). Because you are using a shared layout with <Outlet />, React is smart enough to persist the layout component and only re-render the changing child content, preserving state like scroll position or video playback in the sidebar.