Static-site generation for React on Vite.
npm install vite-react-ssgStatic-site generation for React on Vite.
See demo(also document): docs
> [!IMPORTANT]
> React Router v7 Notice
>
> React Router v7 now has built-in SSG support. If you are using React Router v7, we recommend using its official SSG capabilities for better official support and integration.
>
> vite-react-ssg will continue to maintain SSG functionality for React Router v6 users.
**🎈 Support for @tanstack/router
and wouter is in progress!**
Support for the @tanstack/router router is still experimental, and pathname.lazy.tsx routes are not yet supported.
For usage examples, see: main/examples/tanstack/src/main.tsx


- Usage
- Use CSR during development
- Extra route options
- entry
- getStaticPaths
- Data fetch
- lazy
-
- Document head
- Reactive head
- Redirect
- Public Base Path
- Future config
- CSS in JS
- Critical CSS
- Configuration
- Custom Routes to Render
- Roadmap
- Credits
npm i -D vite-react-ssg react-router-dom
``diff
// package.json
{
"scripts": {
- "build": "vite build"
+ "build": "vite-react-ssg build"
// If you need ssr when dev
- "dev": "vite",
+ "dev": "vite-react-ssg dev",
// OR if you want to use another vite config file
+ "build": "vite-react-ssg build -c another-vite.config.ts"
}
}
`
`ts
// src/main.ts
import { ViteReactSSG } from 'vite-react-ssg'
import routes from './App.tsx'
export const createRoot = ViteReactSSG(
// react-router-dom data routes
{ routes },
// function to have custom setups
({ router, routes, isClient, initialState }) => {
// do something.
},
)
`
`tsx
// src/App.tsx
import type { RouteRecord } from 'vite-react-ssg'
import React from 'react'
import Layout from './Layout'
import './App.css'
export const routes: RouteRecord[] = [
{
path: '/',
element:
entry: 'src/Layout.tsx',
children: [
{
path: 'a',
lazy: () => import('./pages/a'),
},
{
index: true,
Component: React.lazy(() => import('./pages/index')),
},
{
path: 'nest/:b',
lazy: () => {
const Component = await import('./pages/nest/[b]')
return { Component }
},
// To determine which paths will be pre-rendered
getStaticPaths: () => ['nest/b1', 'nest/b2'],
},
],
},
]
`
Vite React SSG provide SSR (Server-Side Rendering) during development to ensure consistency
between development and production as much as possible.
But if you want to use CSR during development, just:
`diff`
// package.json
{
"scripts": {
- "dev": "vite-react-ssg dev",
+ "dev": "vite",
"build": "vite-react-ssg build"
}
}
For SSG of an index page only (i.e. without react-router-dom);vite-react-ssg/single-page
import instead.
`tsx
// src/main.tsx
import { ViteReactSSG } from 'vite-react-ssg/single-page'
import App from './App.tsx'
export const createRoot = ViteReactSSG(
`
The RouteObject of vite-react-ssg is based on react-router, and vite-react-ssg receives some additional properties.
#### getStaticPaths
The getStaticPaths() function should return an array of path
to determine which paths will be pre-rendered by vite-react-ssg.
This function is only valid for dynamic route.
`tsx`
const route = {
path: 'nest/:b',
lazy: () => import('./pages/nest/[b]'),
entry: 'src/pages/nest/[b].tsx',
// To determine which paths will be pre-rendered
getStaticPaths: () => ['nest/b1', 'nest/b2'],
}
#### entry
You are not required to use this field. It is only necessary when "prehydration style loss" occurs.
It should be the path from root to the target file.
eg: src/pages/page1.tsx
These options work well with the lazy field.
`tsx
// src/pages/[page].tsx
export function Component() {
return (
{/ your component /}
)
}
export function getStaticPaths() {
return ['page1', 'page2']
}
`
`ts`
// src/routes.ts
const routes = [
{
path: '/:page',
lazy: () => import('./pages/[page]'),
}
]
Note that during the build process, vite-react-ssg will automatically detect the files directly dynamically imported in the function you pass to the lazy field. This helps vite-react-ssg to get the route's style files or other static resources during the build, preventing flash of unstyled content.
If you still encounter FOUC (flash of unstyled content), please open an issue.
If your component isn't loading, make sure you have wrapped it or its parent in Suspense tags as described in the React documentation.
See example.
You can use react-router-dom's loader to fetch data at build time and use useLoaderData to get the data in the component.
In production, the loader will only be executed at build time, and the data will be fetched by the manifest generated at build time during the browser navigations .
In the development environment, the loader also runs only on the server.It provides data to the HTML during initial server rendering, and during browser route navigations , it makes calls to the server by initiating a fetch on the service.
`tsx
import { useLoaderData } from 'react-router-dom'
export default function Docs() {
const data = useLoaderData() as Awaited
return (
<>
export const Component = Docs
export const entry = 'src/pages/json.tsx'
export async function loader() {
// This code will avoid shiki and node:fs being mark as 'modulepreload' and sent to the client.
if (!import.meta.ssr) {
return null
}
// The code here will not be executed on the client side, and the modules imported will not be sent to the client.
const fs = (await import('node:fs'))
const cwd = process.cwd()
const json = (await import('../docs/test.json')).default
const packageJson = await fs.promises.readFile(${cwd}/package.json, 'utf-8')
const { codeToHtml } = await import('shiki')
const packageJsonHtml = await codeToHtml(packageJson, { lang: 'json', theme: 'vitesse-light' })
return {
...json,
packageCodeHtml: packageJsonHtml,
}
}
`
If you need to render some component in browser only, you can wrap your component with .
`tsx
import { ClientOnly } from 'vite-react-ssg'
function MyComponent() {
return (
{() => {
return {window.location.href}
}}
)
}
`
> It's important that the children of is not a JSX element, but a function that returns an element.
> Because React will try to render children, and may use the client's API on the server.
You can use
to manage all of your changes to the document head. It takes plain HTML tags and outputs plain HTML tags. It is a wrapper around React Helmet.`tsx
import { Head } from 'vite-react-ssg'function MyHead() {
return (
My Title
)
}
`Nested or latter components will override duplicate usages:
`tsx
import { Head } from 'vite-react-ssg'function MyHead() {
return (
My Title
Nested Title
)
}
`Outputs:
`html
Nested Title
`$3
`tsx
import { useState } from 'react'
import { Head } from 'vite-react-ssg'export default function MyHead() {
const [state, setState] = useState(false)
return (
head test {state ? 'A' : 'B'}
{/ You can also set the 'body' attributes here /}
body-class-in-head-${state ? 'a' : 'b'}} />
)
}
`Redirect
You should not use redirect in the loader.
In vite-react-ssg, the loader only executes during the build process for data fetching.
If you need to perform a redirect in certain situations, you can use the following method to redirect on the client side:
`tsx
export const routes: RouteRecord[] = [
{
path: '/:lng',
Component: Layout,
getStaticPaths: () => Object.keys(resources),
children: [
// ... some routes
],
},
{
path: '/',
Component: () => {
const navigate = useNavigate()
useEffect(() => {
navigate('/en', { replace: true })
}, [navigate]) return null
},
},
]
`Public Base Path
Just set
base in vite.config.ts like:`ts
import react from '@vitejs/plugin-react-swc'
// vite.config.ts
import { defineConfig } from 'vite'// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
base: '/base-path',
})
``ts
// main.ts
import { ViteReactSSG } from 'vite-react-ssg'
import { routes } from './App'
import './index.css'export const createRoot = ViteReactSSG(
{
routes,
// pass your BASE_URL
basename: import.meta.env.BASE_URL,
},
)
`Vite React SSG will give it to the react-router's
basename.See: react-router's create-browser-router
Future config
`tsx
export const createRoot = ViteReactSSG(
{
routes,
basename: import.meta.env.BASE_URL,
future: {
v7_normalizeFormMethod: true,
v7_startTransition: true,
v7_fetcherPersist: true,
v7_relativeSplatPath: true,
v7_skipActionErrorRevalidation: true,
v7_partialHydration: true,
},
},
)
`See: react-router's optsfuture
CSS in JS
Use the
getStyleCollector option to specify an SSR/SSG style collector. Currently only supports styled-components.`tsx
import { ViteReactSSG } from 'vite-react-ssg'
import getStyledComponentsCollector from 'vite-react-ssg/style-collectors/styled-components'
import { routes } from './App.js'
import './index.css'export const createRoot = ViteReactSSG(
{ routes },
() => { },
{ getStyleCollector: getStyledComponentsCollector }
)
`You can provide your own by looking at the implementation of any of the existing collectors.
Critical CSS
Vite React SSG has built-in support for generating Critical CSS inlined in the HTML via the
beasties package.
Install it with:`bash
npm i -D beasties
`Critical CSS generation will automatically be enabled for you.
To configure
beasties, pass its options
into ssgOptions.beastiesOptions in vite.config.ts:`ts
// vite.config.ts
export default defineConfig({
ssgOptions: {
beastiesOptions: {
// E.g., change the preload strategy
preload: 'media',
// Other options: https://github.com/danielroe/beasties#usage
},
},
})
`Configuration
You can pass options to Vite SSG in the
ssgOptions field of your vite.config.js`js
// vite.config.jsexport default {
plugins: [],
ssgOptions: {
script: 'async',
},
}
``ts
interface ViteReactSSGOptions {
/**
* Set the scripts' loading mode. Only works for type="module".
*
* @default 'sync'
*/
script?: 'sync' | 'async' | 'defer' | 'async defer'
/**
* Build format.
*
* @default 'esm'
*/
format?: 'esm' | 'cjs'
/**
* The path of the main entry file (relative to the project root).
*
* @default 'src/main.ts'
*/
entry?: string
/**
* The path of the index.html file (relative to the project root).
* @default 'index.html'
*/
htmlEntry?: string
/**
* Mock browser global variables (window, document, etc...) from SSG.
*
* @default false
*/
mock?: boolean
/**
* Apply formatter to the generated index file.
*
* It will cause Hydration Failed.
*
* @default 'none'
*/
formatting?: 'prettify' | 'none'
/**
* Vite environment mode.
*/
mode?: string
/**
* Directory style of the output directory.
*
* flat: /foo -> /foo.html
* nested: /foo -> /foo/index.html
*
* @default 'flat'
*/
dirStyle?: 'flat' | 'nested'
/**
* Generate for all routes, including dynamic routes.
* If enabled, you will need to configure your serve
* manually to handle dynamic routes properly.
*
* @default false
*/
includeAllRoutes?: boolean
/**
* Options for the beasties packages.
*
* @see https://github.com/danielroe/beasties#usage
*/
beastiesOptions?: BeastiesOptions | false
/**
* Custom function to modify the routes to do the SSG.
*
* Works only when includeAllRoutes is set to false.
*
* Defaults to a handler that filters out all the dynamic routes.
* When passing your custom handler, you should also take care of the dynamic routes yourself.
*/
includedRoutes?: (paths: string[], routes: Readonly) => Promise | string[]
/**
* Callback to be called before every page render.
*
* It can be used to transform the project's index.html file before passing it to the renderer.
*
* To do so, you can change the 'index.html' file contents (passed in through the indexHTML parameter), and return it.
* The returned value will then be passed to renderer.
*/
onBeforePageRender?: (route: string, indexHTML: string, appCtx: ViteReactSSGContext) => Promise | string | null | undefined
/**
* Callback to be called on every rendered page.
*
* It can be used to transform the current route's rendered HTML.
*
* To do so, you can transform the route's rendered HTML (passed in through the renderedHTML parameter), and return it.
* The returned value will be used as the HTML of the route.
*/
onPageRendered?: (route: string, renderedHTML: string, appCtx: ViteReactSSGContext) => Promise | string | null | undefined /**
* A function that is run after generation is complete.
* It receives the build output directory as a string.
*
* You can use this to add, edit, or delete files in the output
directory that you don't want to manage in React.
*/
onFinished?: (dir: string) => Promise | void
/**
* The application's root container
id.
*
* @default root
*/
rootContainerId?: string
/**
* The size of the SSG processing queue.
*
* @default 20
*/
concurrency?: number
}
`See src/types.ts. for more options available.
$3
You can use the
includedRoutes hook to include or exclude route paths to render, or even provide some completely custom ones.`js
// vite.config.jsexport default {
plugins: [],
ssgOptions: {
includedRoutes(paths, routes) {
// exclude all the route paths that contains 'foo'
return paths.filter(i => !i.includes('foo'))
},
},
}
``js
// vite.config.jsexport default {
plugins: [],
ssgOptions: {
includedRoutes(paths, routes) {
// use original route records
return routes.flatMap(route => {
return route.name === 'Blog'
? myBlogSlugs.map(slug =>
/blog/${slug})
: route.path
})
},
},
}
``ts
export default defineConfig({
server: {
https: true,
},
})
`$3
- for react18, with flag
useLegacyRender: true, it will use the legacy render and hydrate methods.
- for react17, on top of above, you will need minor update to react and react-dom example to polyfill the mjs import and the react-dom/client.Roadmap
- [x] Support
react19`
- [ ] no index.html modeCredits
This project inspired by vite-ssg, thanks to @antfu for his awesome work.
License