> For AI agents: the complete documentation index is available at https://rspress.rs/llms.txt, the full documentation bundle is available at https://rspress.rs/llms-full.txt.

# Static site generation (SSG)

## What is SSG

SSG (Static Site Generation) refers to pre-rendering pages into HTML files during the **build phase**, rather than rendering them when users visit.

**Advantages of SSG:**

- **Faster First Contentful Paint**: Users don't need to wait for JavaScript to load and execute; they see complete content as soon as the browser loads the HTML
- **SEO Friendly**: Search engine crawlers can directly access complete HTML content
- **Easy to Deploy**: Output consists of static files and can be hosted on any static hosting service without a server

Rspress enables SSG by default. When you run `rspress build`, each page is pre-rendered into an HTML file with complete content. The following sections explain how SSG works.

## Differences between dev and build

Rspress employs different rendering strategies depending on the mode: it uses Client-Side Rendering (CSR) during development for a better experience, while defaulting to SSG during production builds for optimal performance.

| Aspect         | Dev Mode (Development)           | Build Mode (Production)                |
| -------------- | -------------------------------- | -------------------------------------- |
| Command        | `rspress dev`                    | `rspress build`                        |
| Rendering      | Pure CSR (Client-Side Rendering) | SSG (default) or CSR                   |
| Pre-rendering  | None                             | Pre-renders all pages when SSG enabled |
| Focus          | Debugging, HMR                   | Performance, SEO                       |
| Preview Method | Access dev server directly       | `rspress preview`                      |

### Dev mode

```bash
rspress dev
```

Dev mode uses **pure Client-Side Rendering (CSR)** without pre-rendering. This prioritizes iteration speed and Hot Module Replacement (HMR) capabilities.

:::tip
If your code works in dev mode but throws errors in build mode, it is usually because SSG renders in a Node.js environment and cannot access browser APIs such as `window` or `document`. See ["Common Issues and Solutions"](#common-issues-and-solutions) below.
:::

### Build mode

```bash
rspress build
```

Build mode enables SSG by default. You can control this via the [`ssg` configuration](#configuration):

- `ssg: true` (Default): Enable SSG. During the build, Rspress executes React component rendering in a Node.js environment, converting each page into an HTML file with complete content.
- `ssg: false`: Disable SSG. Use pure CSR. The generated HTML contains only an empty container, waiting for client-side rendering.

**After building:**

- **Local Preview**: Use `rspress preview` to start a local static server for previewing the output

  ```bash
  rspress preview
  ```

- **Server Deployment**: Deploy the `doc_build` directory to static hosting services (GitHub Pages, Netlify, Vercel, etc.)

## SSG vs CSR output

### Output directory structure

Whether using SSG or CSR mode, the output directory structure is the same:

```tree
doc_build/
├── static/
│   ├── js/
│   │   ├── main.[hash].js
│   │   └── async/
│   └── css/
│       └── main.[hash].css
├── index.html
├── 404.html
├── guide/
│   └── getting-started.html
└── api/
    └── config.html
```

The `404.html` is automatically generated by Rspress to handle non-existent routes. This file plays an important role in SPA deployment, see ["Page shows 404 after refresh"](#refresh-404) for details.

### HTML content differences

The core difference between the two modes lies in the HTML file content:

**SSG Output HTML** (Pre-rendered complete content):

```html
<body>
  <div id="__rspress_root">
    <!-- Pre-rendered complete page content -->
    <nav>...</nav>
    <main>
      <article>
        <h1>Getting Started</h1>
        <p>Welcome to Rspress...</p>
      </article>
    </main>
  </div>
  <script src="/static/js/main.[hash].js"></script>
</body>
```

**CSR Output HTML** (only empty container, waiting for JS to render):

```html
<body>
  <div id="__rspress_root"></div>
  <script src="/static/js/main.[hash].js"></script>
</body>
```

### Loading flow differences

**SSG Loading Flow:**

1. Browser loads HTML → User **immediately sees complete content**
2. JavaScript finishes loading → React hydrates, binds event interactions
3. Subsequent navigation → SPA mode, client-side rendering

**CSR Loading Flow:**

1. Browser loads HTML → User sees **blank page**
2. JavaScript finishes loading → React renders page content
3. Subsequent navigation → SPA mode, client-side rendering

## Common issues and solutions

### `window is not defined` / `document is not defined`

**Cause**: SSG renders pages in a Node.js environment, where browser-specific global objects like `window` and `document` don't exist.

**Solutions**:

1. **Use [`BrowserOnly`](https://rspress.rs/ui/runtime-components/browser-only.md) for browser-only rendering**: Wrap code that cannot run during SSG in a function.

   ```tsx
   import { BrowserOnly } from '@rspress/core/runtime';

   function MyComponent(props) {
     return (
       <BrowserOnly fallback={<div>Loading...</div>}>
         {async () => {
           const { LibComponent } =
             await import('some-lib-that-accesses-window');
           return <LibComponent {...props} />;
         }}
       </BrowserOnly>
     );
   }
   ```

   The children must be a function, not JSX. Otherwise, expressions like `window.location.href` are still evaluated while React builds the SSG render tree.

2. **Use `useEffect` for delayed execution**: Place browser API calls inside `useEffect` so they only run on the client.

   ```tsx
   import { useEffect, useState } from 'react';

   function MyComponent() {
     const [width, setWidth] = useState(0);

     useEffect(() => {
       // Only runs on client
       setWidth(window.innerWidth);
     }, []);

     return <div>Window width: {width}</div>;
   }
   ```

3. **Conditional check**: Check the environment before accessing browser APIs

   ```tsx
   if (typeof window !== 'undefined') {
     // Browser environment
     console.log(window.location.href);
   }
   ```

4. **Dynamic import in `useEffect`**: For third-party libraries that depend on browser APIs, use dynamic imports in `useEffect`

   ```tsx
   import { useEffect, useState } from 'react';

   function MyComponent() {
     const [Editor, setEditor] = useState(null);

     useEffect(() => {
       import('some-browser-only-library').then(mod => {
         setEditor(() => mod.default);
       });
     }, []);

     if (!Editor) return <div>Loading...</div>;
     return <Editor />;
   }
   ```

### Hydration mismatch

**Cause**: The server-rendered HTML content doesn't match the client's first render. React checks for consistency during hydration, and mismatches will cause warnings or errors.

**Common scenarios**:

- Using `Date.now()` or random numbers
- Rendering different content based on `window` object properties (like `window.innerWidth`)
- Using data that only exists on the client (like localStorage)

**Solution**: Ensure the first render output is consistent between server and client. For content that needs to change dynamically on the client, use `useEffect` to update after hydration completes.

```tsx
import { useEffect, useState } from 'react';

function MyComponent() {
  // First render uses default value for server/client consistency
  const [theme, setTheme] = useState('light');

  useEffect(() => {
    // Read localStorage after hydration completes
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
      setTheme(savedTheme);
    }
  }, []);

  return <div className={theme}>...</div>;
}
```

### Page shows 404 after refresh \{#refresh-404}

**Symptom**: Navigating to other pages within the site works normally, but refreshing the page results in a 404 error.

**Cause**: When you refresh the page or directly visit a URL, the request is sent to the server, but the server may not have a corresponding file for that path (especially on some static hosting services).

**Solution**: Most static hosting services, such as GitHub Pages, Netlify, and Vercel, use `404.html` by default to handle unmatched routes and require no additional configuration. If your server does not do this automatically, configure it to redirect unmatched requests to `404.html`. The `404.html` generated by Rspress contains the complete application code, so it can handle routing on the client and display the correct page.

If your hosting service supports Netlify-style redirects, you can use a `_redirects` file like this:

```txt title="docs/public/_redirects"
/*    /404.html   200
```

## Configuration

Control whether SSG is enabled with the `ssg` configuration:

```ts title="rspress.config.ts"
import { defineConfig } from '@rspress/core';

export default defineConfig({
  ssg: true, // Default value, SSG enabled
});
```

If your site has special requirements, you can disable SSG:

```ts title="rspress.config.ts"
import { defineConfig } from '@rspress/core';

export default defineConfig({
  ssg: false, // Disable SSG, use CSR
});
```

:::warning
Disable SSG carefully, because doing so removes the faster First Contentful Paint and SEO benefits.
:::

## Custom HTML content

To inject custom HTML tags, such as meta tags, analytics code, scripts, or styles, see [Customizing Head Tags](https://rspress.rs/guide/advanced/custom-head.md).
