Implementing Static Site Generation (SSG) with Vite from Scratch
2025-04-14 Hanlun Wang
1. Introduction
Static Site Generation (SSG) has become increasingly popular for building high-performance websites with excellent SEO capabilities. As a developer working extensively with Vite and React, I recently implemented a complete SSG solution for a React application. In this article, I'll share the technical details of transforming a standard client-side rendered (CSR) Vite React application into a fully static generated site.
Unlike frameworks like Next.js or Nuxt that offer SSG out of the box, implementing SSG in a vanilla Vite project requires understanding the underlying mechanisms and making strategic modifications to the build process. This approach offers greater flexibility and control while achieving similar performance benefits.
2. Understanding SSG Principles
At its core, SSG pre-renders pages at build time rather than at request time (SSR) or in the client's browser (CSR). The primary benefit is combining SEO advantages of server rendering with the simplicity of static hosting.
The fundamental concept involves:
- Creating a dual-entry architecture that separates client and server rendering
- Using server-side React rendering to generate HTML strings
- Injecting these pre-rendered strings into HTML templates
- Implementing client-side hydration to make the static content interactive
3. Implementation Steps Overview
The SSG implementation can be broken down into five key phases:
- Dual Entry Architecture: Creating separate entry points for client and server rendering
- Routes Definition: Defining all routes to be pre-rendered
- Build Script Configuration: Setting up distinct build processes for client and server
- Static Generation Logic: Developing the script to convert React components to static HTML
- Core Configuration Adjustments: Modifying Vite configuration to support the SSG flow
Let's explore each component in detail.
4. Core Implementation Components
4.1. Client Entry File (entry-client.tsx)
The client entry file is responsible for "hydrating" the static HTML with interactive React components:
The critical difference from standard React initialization is using hydrateRoot
instead of createRoot
. This tells React to connect to the existing DOM nodes rather than rebuilding them, preserving the pre-rendered HTML while making it interactive.
4.2. Server Entry File (entry-server.tsx)
This file contains the logic for rendering components to HTML strings during the build process:
Key aspects of this implementation:
- Using
StaticRouter
instead ofBrowserRouter
since we don't have browser history during the build - Patching
useLayoutEffect
to prevent server rendering warnings - Maintaining the same provider structure as the client for rendering consistency
- Exporting a
render
function that accepts a URL path and returns the corresponding HTML
4.3. Routes Definition (routes.js)
This simple file defines all routes that should be pre-rendered:
The special '*'
route is used to generate a 404 page. This centralized approach makes it easy to maintain and extend the list of routes to be pre-rendered.
4.4. Static Generation Script (generate-static.js)
This is the heart of the SSG process:
This script performs several crucial operations:
- Imports the routes list and server render function
- Reads the HTML template from the client build
- For each route:
- Determines the output file path (creating the correct directory structure)
- Calls the server render function to generate HTML
- Injects the HTML into the template at the designated insertion point
- Writes the complete HTML file to the appropriate location
- Special handling for the 404 page
The script uses a primary insertion strategy (looking for <!--ssr-outlet-->
) with a fallback mechanism (inserting between root div tags) to ensure compatibility across different build configurations.
4.5. Package.json Script Configuration
The build process is split into distinct phases:
This configuration enables:
- A single
build
command that orchestrates the entire process - Separate client and server builds with appropriate options
- Development-specific build commands for easier debugging
- The final static generation step that produces the pre-rendered site
4.6. Vite Configuration (vite.config.ts)
The modified Vite configuration supports the SSG process:
Key adjustments include:
- A custom plugin that logs when static generation begins
- Controlled minification that preserves HTML comments in development mode
- Explicit specification of the HTML entry point
- TypeScript type enhancements for better developer experience
4.7. HTML Template Modifications (index.html)
The HTML template is simplified to include a placeholder for server-rendered content:
The <!--ssr-outlet-->
comment serves as the injection point for pre-rendered HTML. This placeholder will be replaced with the actual component HTML during the static generation process.
5. Workflow Analysis
5.1. Development Phase
During development, the application runs as a standard Vite application:
- Running
npm run dev
starts the Vite development server - The application functions in CSR mode with hot module replacement
- Developers can work with the familiar React development experience
5.2. Build Phase
When building for production, the npm run build
command triggers a three-step process:
-
Client Build (
build:client
)- Standard Vite build process
- Generates optimized client JavaScript, CSS, and HTML template
- Outputs to the
dist
directory
-
Server Build (
build:server
)- Uses Vite's
--ssr
flag for server-optimized build - Builds
entry-server.tsx
and its dependencies - Outputs to the
dist/server
directory - Creates a Node.js executable module
- Uses Vite's
-
Static Generation (
generate
)- Runs the
generate-static.js
script - Imports the routes list and server render function
- Renders each route and generates corresponding HTML files
- Creates the final static site structure
- Runs the
5.3. Runtime Flow
When a user visits the site:
-
Initial Load:
- The server returns the pre-rendered static HTML
- Users immediately see the complete page content (beneficial for SEO and initial load performance)
- No waiting for JavaScript to load and execute
-
Hydration Phase:
- Once client JavaScript loads
- React connects to the existing DOM through
hydrateRoot
- The page becomes interactive without re-rendering
-
Subsequent Navigation:
- Client-side routing handles page transitions
- No additional server requests for full HTML pages
- Provides a smooth SPA-like experience
6. Technical Benefits
6.1. SEO Enhancement
Static pre-rendering offers significant SEO advantages:
- Search engines can directly crawl complete HTML content
- Content remains visible even if JavaScript is disabled
- Improved indexing quality and completeness
6.2. Performance Improvements
The SSG approach yields measurable performance gains:
- Faster First Contentful Paint (FCP) and Largest Contentful Paint (LCP)
- Reduced Cumulative Layout Shift (CLS)
- Better Core Web Vitals scores overall
6.3. Developer Experience
Despite the more complex build process, developer experience remains strong:
- Vite's rapid development server and HMR features are preserved
- Separation of concerns: client hydration vs. server rendering
- Clear build process, easier to debug and extend
6.4. Deployment Flexibility
The static output provides deployment advantages:
- Pure static HTML files can be hosted on any static hosting service
- No need for a Node.js server in production
- Reduced server load and operating costs
7. Implementation Challenges
While implementing SSG with Vite, I encountered several technical challenges:
-
Libraries Using Browser APIs: Many React libraries assume browser APIs are available, requiring careful handling of server-side rendering contexts.
-
Hydration Mismatch: Ensuring that server-rendered HTML exactly matches what React expects during hydration was critical. Even small differences can cause hydration errors.
-
useLayoutEffect Warnings: React's useLayoutEffect hook isn't meant for server environments, requiring patching for libraries that use it internally.
-
Path Resolution: Ensuring correct file paths between development and production builds, particularly for dynamic imports.
-
CSS Handling: Managing CSS extraction and application during the SSR process required special attention to prevent styling inconsistencies.
8. Advanced Techniques
8.1. Dynamic Routes Support
For applications with dynamic routes (like blog posts), the routes.js file can be extended:
8.2. Route-Specific Data Prefetching
For pages requiring specific data:
8.3. Incremental Static Regeneration
Similar to Next.js ISG, a rebuild endpoint can be implemented:
9. Conclusion
Implementing SSG with Vite from scratch provides a powerful balance between performance, SEO, and developer experience. While frameworks like Next.js offer these capabilities out of the box, building a custom SSG solution offers greater flexibility and understanding of the underlying processes.
The approach described in this article follows a "best of both worlds" strategy: using pre-rendered static HTML for initial loading to optimize SEO and performance, while leveraging React's client-side hydration for smooth subsequent interactions.
By understanding the core principles and implementation flow of SSG, development teams can adapt and extend this solution according to their specific project requirements. The end result is a high-performance, SEO-friendly site that combines the speed of static sites with the interactivity of modern SPAs.