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

# 静态站点生成（SSG）

## 什么是 SSG

SSG（Static Site Generation，静态站点生成）是指在**构建阶段**将页面预渲染为 HTML 等静态文件。

**SSG 的优势：**

- **首屏渲染更快**：用户无需等待 JavaScript 加载和执行，浏览器加载 HTML 后即可看到完整内容，而不是在用户访问时才进行渲染。
- **SEO 友好**：搜索引擎爬虫可以直接抓取到完整的 HTML 内容。
- **易于部署**：产物是纯静态文件，无需部署服务器 API，可直接上传到 CDN 或任何静态托管服务。

Rspress 默认启用 SSG，这意味着当你执行 `rspress build` 时，每个页面都会被预渲染为包含完整内容的 HTML 文件。以下是关于 SSG 实现的详细说明，旨在帮助你更深入地理解其工作原理。

## dev 和 build 下的区别

Rspress 在不同的运行模式下采用不同的渲染策略：在开发阶段使用客户端渲染以保证开发体验，而在生产构建阶段则默认开启 SSG 以优化性能。

| 对比项      | dev 模式 (开发)   | build 模式 (生产)     |
| -------- | ------------- | ----------------- |
| 命令       | `rspress dev` | `rspress build`   |
| 渲染方式     | 纯 CSR（客户端渲染）  | SSG（默认）或 CSR      |
| 预渲染 HTML | 无             | SSG 时预渲染所有页面      |
| 适用场景     | 开发调试、热更新      | 产物性能、SEO          |
| 预览方式     | 直接访问开发服务器     | `rspress preview` |

### dev 模式

```bash
rspress dev
```

dev 模式为了获得更快的开发体验和热更新能力，使用**纯客户端渲染（CSR）**，不进行 HTML 预渲染。

:::tip 提示
如果你的代码在 dev 模式下正常运行，但在 build 模式下报错，这通常是因为 SSG 在 Node.js 环境中渲染，无法访问浏览器 API（如 `window`、`document`）。请参考下文的[「常见问题与解决方案」](#常见问题与解决方案)。
:::

### build 模式

```bash
rspress build
```

build 模式默认启用 SSG，你可以通过 [`ssg` 配置项](#配置选项)来控制：

- `ssg: true` （默认）：启用 SSG，构建时，Rspress 会在 Node.js 环境中执行 React 组件的渲染，将每个页面转换为包含完整内容的 HTML 文件。
- `ssg: false`：禁用 SSG，使用纯 CSR，生成的 HTML 仅包含空容器，等待客户端渲染。

**构建完成后：**

- **本地预览**：使用 `rspress preview` 启动本地静态服务器预览产物

  ```bash
  rspress preview
  ```

- **服务器部署**：将 `doc_build` 目录部署到静态托管服务（GitHub Pages、Netlify、Vercel 等）

## SSG 与 CSR 产物的对比

### 产物目录结构

无论是 SSG 还是 CSR 模式，产物的目录结构是相同的：

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

其中 `404.html` 是 Rspress 自动生成的 404 页面，用于处理不存在的路由。该文件在 SPA 部署时有重要作用，详见[「刷新页面时出现 404」](#refresh-404)。

### HTML 内容差异

两种模式的核心区别在于 HTML 文件的内容：

**SSG 产物 HTML**（包含预渲染的完整内容）：

```html
<body>
  <div id="__rspress_root">
    <!-- 预渲染的完整页面内容 -->
    <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 产物的 HTML**（只有空容器，等待 JS 渲染）：

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

### 加载流程差异

**SSG 加载流程：**

1. 浏览器加载 HTML → 用户**立即看到完整内容**
2. JavaScript 加载完成 → React hydrate（水合），绑定事件交互
3. 后续页面导航 → SPA 模式，客户端渲染

**CSR 加载流程：**

1. 浏览器加载 HTML → 用户看到**空白页面**
2. JavaScript 加载完成 → React 渲染页面内容
3. 后续页面导航 → SPA 模式，客户端渲染

## 常见问题与解决方案

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

**原因**：SSG 在 Node.js 环境中渲染页面，而 `window`、`document` 等是浏览器特有的全局对象，在 Node.js 中不存在。

**解决方案**：

1. **使用 [`BrowserOnly`](https://rspress.rs/zh/ui/runtime-components/browser-only.md) 进行仅浏览器渲染**：把无法在 SSG 阶段运行的代码放在函数中。

   ```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>
     );
   }
   ```

   children 必须是函数，而不是 JSX。否则 `window.location.href` 这类表达式仍然会在 React 构建 SSG 渲染树时被求值。

2. **使用 `useEffect` 延迟执行**：将浏览器 API 的调用放在 `useEffect` 中，确保只在客户端执行

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

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

     useEffect(() => {
       // 只在客户端执行
       setWidth(window.innerWidth);
     }, []);

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

3. **条件检查**：在访问浏览器 API 前检查环境

   ```tsx
   if (typeof window !== 'undefined') {
     // 浏览器环境
     console.log(window.location.href);
   }
   ```

4. **在 `useEffect` 中动态导入**：对于依赖浏览器 API 的第三方库，在 `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

**原因**：服务器渲染的 HTML 内容与客户端首次渲染的内容不一致。React 在 hydration 时会检测两者是否匹配，不匹配会导致警告或错误。

**常见场景**：

- 使用了 `Date.now()` 或随机数
- 根据 `window` 对象的属性（如 `window.innerWidth`）渲染不同内容
- 使用了只在客户端存在的数据（如 localStorage）

**解决方案**：确保首次渲染的输出在服务器和客户端是一致的。对于需要在客户端动态变化的内容，使用 `useEffect` 在 hydration 完成后再更新。

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

function MyComponent() {
  // 首次渲染使用默认值，保证服务器和客户端一致
  const [theme, setTheme] = useState('light');

  useEffect(() => {
    // hydration 完成后再从 localStorage 读取
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
      setTheme(savedTheme);
    }
  }, []);

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

### 刷新页面时出现 404 \{#refresh-404}

**现象**：在站点内通过链接导航到其他页面正常，但刷新页面后出现 404 错误。

**原因**：当你刷新页面或直接访问某个 URL 时，请求会发送到服务器，但服务器上可能并没有对应路径的文件（尤其是在某些静态托管服务中）。

**解决方案**：大部分静态托管服务（如 GitHub Pages、Netlify、Vercel 等）默认会使用 `404.html` 来处理所有未匹配的路由，无需额外配置。如果你的服务器没有自动处理，需要手动配置将未匹配的请求重定向到 `404.html`。Rspress 生成的 `404.html` 包含完整的应用代码，能够在客户端正确处理路由并显示对应页面。

如果你的托管服务支持 `_redirects` 文件，可以使用以下配置示例：

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

## 配置选项

你可以通过 `ssg` 配置来控制是否启用 SSG：

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

export default defineConfig({
  ssg: true, // 默认值，启用 SSG
});
```

如果你的站点有特殊需求，可以禁用 SSG：

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

export default defineConfig({
  ssg: false, // 禁用 SSG，使用 CSR
});
```

:::warning 注意
请谨慎关闭 SSG，这会丧失首屏渲染速度和 SEO 优势。
:::

## 自定义 HTML 内容

如果你需要向 HTML 中注入自定义标签（如 meta 标签、统计代码、脚本或样式），请参考 [自定义 Head 标签](https://rspress.rs/zh/guide/advanced/custom-head.md)。
