SveltePress 可以让最新版文档继续使用普通 URL,同时把不可变的历史快照发布到 /v/8.1/ 之类的路径。该能力默认关闭;没有 sveltepress.versions.json 的站点行为不变。

演练场中的 文档版本管理 条目会按作者提交的内容启动带 versions 清单、一个冻结快照、自动「新增内容」目录、自动侧边栏和 New 徽章的默认主题 Starter,并打开 sveltepress.versions.json

安装与初始化

pnpm add -D @sveltepress/cli
pnpm exec sveltepress versions init --current 8.1 --label "8.1"
sh

命令会创建 sveltepress.versions.json。Vite 插件会自动发现它;可以用 sveltepress({ versions: false }) 关闭,或用 sveltepress({ versions: { manifest: 'path/to/versions.json' } }) 指定其他清单。

启用增量产物

已有站点只需迁移一次已提交的完整快照;新站点可在 versions init 后直接执行同一命令:

pnpm exec sveltepress versions migrate --site-id docs-example
sh

迁移会用已提交的 version-deltas/{id} 源码增量替代完整的 src/routes/v/{id} 副本,并在 .sveltepress/version-artifacts 初始化内容寻址页面存储。该存储属于构建缓存;应提交源码增量,并在 CI 中恢复或缓存产物存储。

生产构建脚本应改为:

{
  "scripts": {
    "build": "sveltepress versions build"
  }
}
json

versions plan 会在不构建的情况下报告需要编译、复用、删除和重新组合的路由。versions build 会从已提交增量恢复缺失的历史产物,只编译当前版本中真正变化的页面,以稳定 SveltePress 壳层组合全部路由,然后执行正常的 Vite 生产构建。壳层或索引变化可以重新组合路由而不重编译页面内容;页面编译器或产物 schema 变化则会有意使所有页面产物失效。

在 GitHub Actions 中,可在构建前恢复最近的兼容存储,并按当前提交保存更新后的存储:

- uses: actions/cache@v4
  with:
    path: .sveltepress/version-artifacts
    key: sveltepress-pages-${{ runner.os }}-${{ github.sha }}
    restore-keys: |
      sveltepress-pages-${{ runner.os }}-
- run: pnpm build
yaml

其他 CI 平台应使用对应的持久化缓存能力。不同 siteId 不要共用同一个存储。

页面生成模块(包括 Default Theme 的 LiveCode 组件)会写入所属页面产物。CI 只需缓存 .sveltepress/version-artifacts.sveltepress/live-code 是本地开发临时目录,恢复复用页面时不需要它。

LiveCode 产物自检

本页会实际使用上述能力。下面的交互组件由当前 Markdown 文件生成并写入页面产物;删除本地 .sveltepress/live-code 目录后,它仍会在复用产物的构建中完成服务端渲染。卡片能正常显示且按钮可以交互,就说明可复用产物和客户端水合链路都正常工作。

<script>
  let let interactions: numberinteractions = 
function $state<0>(initial: 0): 0 (+1 overload)
namespace $state

Declares reactive state.

Example:

let count = $state(0);
@seehttps://svelte.dev/docs/svelte/$state Documentation@paraminitial The initial value
$
function $state<0>(initial: 0): 0 (+1 overload)
namespace $state

Declares reactive state.

Example:

let count = $state(0);
@seehttps://svelte.dev/docs/svelte/$state Documentation@paraminitial The initial value
state
(0)
const const checks: string[]checks = [ '生成模块已嵌入', '服务端渲染完成', '客户端水合就绪', ] </script> <section class: stringclass="artifact-check" data-version-artifact-live-code> <div class: stringclass="artifact-check__status" aria-hidden="true"></div> <div class: stringclass="artifact-check__content"> <p class: stringclass="artifact-check__eyebrow">实时文档检查</p> <h3>产物自检通过</h3> <ul> {#each const checks: string[]checks as let check: stringcheck} <li><span aria-hidden="true"></span>{let check: stringcheck}</li> {/each} </ul> <button type: stringtype="button" onclick: () => numberonclick={() => let interactions: numberinteractions++}> 测试交互{let interactions: numberinteractions ? ` · ${let interactions: numberinteractions}` : ''} </button> </div> </section> <style> .artifact-check { display: grid; grid-template-columns: auto 1fr; gap: 1rem; overflow: hidden; padding: 1.25rem; border: 1px solid color-mix(in srgb, currentColor 18%, transparent); border-radius: 1rem; background: radial-gradient(circle at 100% 0%, rgb(255 94 122 / 18%), transparent 45%), color-mix(in srgb, currentColor 4%, transparent); } .artifact-check__status { display: grid; width: 2.75rem; height: 2.75rem; place-items: center; border-radius: 0.85rem; color: #14231a; font-size: 1.4rem; font-weight: 800; background: #70e19b; box-shadow: 0 0 0 0.35rem rgb(112 225 155 / 12%); } .artifact-check__content h3, .artifact-check__content p { margin: 0; } .artifact-check__eyebrow { color: #ff5e7a; font-size: 0.72rem; font-weight: 800; letter-spacing: 0.14em; } .artifact-check__content h3 { margin-top: 0.15rem; font-size: 1.2rem; } .artifact-check__content ul { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.85rem 0; padding: 0; list-style: none; } .artifact-check__content li { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.3rem 0.55rem; border-radius: 999px; font-size: 0.78rem; background: color-mix(in srgb, currentColor 8%, transparent); } .artifact-check__content li span { color: #45c97c; font-weight: 800; } .artifact-check__content button { padding: 0.55rem 0.8rem; border: 1px solid color-mix(in srgb, currentColor 20%, transparent); border-radius: 0.65rem; color: inherit; font: inherit; font-size: 0.85rem; font-weight: 700; background: transparent; cursor: pointer; } .artifact-check__content button:hover { border-color: #ff5e7a; } </style>
svelte
Expand code

创建发版快照

先推进版本,再编辑下一版文档

从干净、完整的 outgoing 文档开始。在编辑下一版页面或添加对应的 :::since 标记之前,先构建并创建下一版本。create 会冻结 outgoing current,并把传入的 ID 设为新的 current;只有此后,新文档才应使用这个 ID。

pnpm exec sveltepress versions build
pnpm exec sveltepress versions create 8.2 --label "8.2"

# 8.2 现在是 current:编辑文档并添加 version="8.2" 标记

pnpm exec sveltepress versions build
pnpm exec sveltepress versions validate
sh

不要在已经写入下一版内容的文档上直接运行 versions create,否则这些改动会被错误地冻结到 outgoing 版本中。如果已经提前开始编辑,应先恢复已知的干净 outgoing 状态,完成构建和版本推进,再把改动重新应用到新的 current。

CLI --locale 按语言选择清单

多语言站点为每种语言保留独立的版本清单(sveltepress.versions.jsonsveltepress.versions.zh.jsonsveltepress.versions.bn.json 等)。在 initcreatevalidate 等 versions 子命令上传入 --locale <id>,即可读写该语言的清单与增量(例如 version-deltas-zh/)。默认语言省略 --locale

不带 --localesveltepress versions build 会先为每种语言生成草稿,再把 /v//zh/v//bn/v/ 等一并写入同一份生产产物。只有需要单独处理某一种语言时才使用 --locale zh(可配合 --draft-only)。

sveltepress versions build --locale zh
sveltepress versions create 8.2 --label "8.2" --locale zh
sveltepress versions validate --locale zh
sh

语言与版本基路径如何组合(/zh/v/bn/v)见国际化

create 会发布当前草稿清单,只把变化页面和 tombstone 写入 version-deltas/8.1/,冻结路由、侧栏和变化元数据,把 8.1 移入历史版本,并将 8.2 设为当前版本。过期草稿、重复 ID、符号链接、脏 Git 工作区和冻结边界外的依赖都会被拒绝。只有当未提交内容就是本次发版来源时才使用 --allow-dirty

已发布版本还会获得自动生成的 sourceHash,每个 delta 还会用元数据哈希绑定冻结的路由、侧栏和变化目录。versions validate 会重建每个已提交 delta 并检查这两个哈希,因此即使产物缓存为空也能发现源码或元数据漂移。不要手工修改哈希或 delta 文件。

操作是原子的:预检失败不会留下半成品增量,也不会修改清单。versions list 列出版本顺序,versions publish 8.1 输出供 CI 发布使用的不可变清单哈希,versions gc --dry-run 可在清理前报告未引用的本地产物。

清单配置

{
  "$schema": "./node_modules/@sveltepress/cli/schema/versions.schema.json",
  "basePath": "/v",
  "current": { "id": "8.2", "label": "8.2" },
  "versions": [
    {
      "id": "8.1",
      "label": "8.1",
      "status": "deprecated",
      "message": "请升级到 8.2。",
      "sourceRef": "v8.1.0",
      "search": { "facetFilters": ["version:8.1"] }
    }
  ],
  "content": {
    "include": ["**"],
    "exclude": ["internal/**"],
    "shared": ["$lib/**", "static/**"]
  },
  "artifacts": {
    "mode": "incremental",
    "siteId": "docs-example",
    "store": ".sveltepress/version-artifacts",
    "sources": "version-deltas"
  }
}
json

版本 ID 必须是适合 URL 的小写标识,可包含点和连字符。includeexclude 决定冻结哪些路由文件;shared 明确声明不复制、继续引用当前文件的依赖。共享清单应尽量小,因为未来修改会影响所有历史版本。

status 设为 deprecatedeol 会在全站导航上方显示醒目的旧版横条,提示旧版站点的功能可能不可用,并链接到最新版的同一逻辑页面。sourceRef 让历史页面的编辑链接指向对应 Git ref,也可用 editLink: false 隐藏。EOL 版本默认输出 noindex,其他版本可用 noIndex: true 主动关闭索引。

导航、搜索与构建输出

默认主题会自动加入可键盘操作的版本选择器。历史版本中的内部链接和冻结侧栏会留在同一版本;切换时优先保留当前逻辑页面,目标版本不存在该页面时则进入其首页并显示说明。

版本构建还会输出页面 canonical、版本化 sitemap.xml/v/{id}/llms.txt,根目录 LLM 文件只包含当前文档。PWA 不预缓存历史 HTML,并对历史页面使用 network-first 策略。

自定义主题可导入 virtual:sveltepress/versions,使用其中的清单与路径解析函数。version-deltas 是不可变的发版源码,应审查并提交但不要手工修改。冷 CI 可以用它恢复产物,而持久化 CI 缓存可避免重新编译历史页面;修订当前路由后再创建下一版增量。

在浏览器代码中直接从包导入这些解析函数时,请使用 @sveltepress/vite/versioning/runtime;该入口不包含 Node 文件系统代码。构建和配置代码仍可使用 @sveltepress/vite/versioning

说明当前版本新增了什么

SveltePress 会按清单顺序比较当前路由与最近的历史版本。只存在于当前版本的路由会被列为新增页面。可通过 frontmatter 补充摘要或排除不应进入变化总览的页面:

---
title: 新增内容
versionChanges:
  exclude: true
  summary: 可选的页面摘要
---
yaml

已有 Markdown 页面中的重点新增段落使用显式标记;版本、标题和页面内唯一的稳定 ID 都是必填项:

### 热更新

已有的文档内容。

:::since[热更新配置]{version="8.2" id="hot-reload" summary="无需重启"}
这里是新增的文档内容。
:::
md

未知版本、重复 ID、未知字段或错误类型会直接终止开发服务器和生产构建。新增页面只进入“新增页面”,不会因为内部存在 since 标记而再次进入“更新页面”;第一个受管理版本没有比较基准,也不会把整站视为新增。

自动标记页面、段落与导航徽章

默认主题会在文档的多个位置自动标记新特性,并且这些徽章仅在浏览内容首次引入的版本时才会激活显示;切换到其他历史版本或后续版本时会自动隐藏或调整,确保文档呈现精准一致:

  1. 页面标题自动标记(页面级)

    • 当某个路由页面为当前版本相比上一版本新增的页面时(处于 newPages 列表中),默认主题会自动在页面顶部的一级标题(h1.page-title)右侧显示高亮版本徽章,例如 8.2 新增New in 8.2
    • 徽章文本模板默认为 "New in {version}",可在主题选项的 i18n.versionNewLabel 中自定义(模板中的 {version} 会被自动替换为对应的版本标签或版本 ID)。
    • 当用户浏览后续版本时,该页面不再属于新增页面,主标题旁的新增徽章会自动隐去。
  2. 新增段落/小节自动标记(段落级)

    • 在已有页面中,通过 :::since[标题]{version="8.2" id="unique-id" summary="..."} 指令包裹的内容会被渲染为一个带边框与背景的高亮小节,并在该段落的标题栏中自动插入版本徽章。
    • 该段落徽章仅在浏览 version="..." 所指定的对应引入版本时才会激活显示;在浏览其他版本时会自动隐藏。徽章文案同样遵循 i18n.versionNewLabel 模板。
  3. 页内导航自动标记(目录级)

    • 页面右侧的“当前页面”页内目录(Table of Contents)会自动根据段落标记解析标题关联:
      • 如果 :::since 指令内部包含子标题,这些标题将直接关联该变更 ID;
      • 如果 :::since 指令自身不含标题,默认主题会自动将其关联到同一 Markdown 容器中最近的前置标题;
      • 同一个标题可以关联多个不同版本的 :::since 标记。
    • 当用户浏览对应版本时,页内目录中所有关联了该版本变更的标题项右侧,都会自动显示紧凑的“新”徽章(VersionNavigationBadge)。
    • 该紧凑徽章文案默认为 "New",可通过主题选项的 i18n.versionNavigationNewLabel 自定义(例如配置为 "新")。
  4. 侧边栏导航自动标记(侧边栏级)

    • 侧边栏会自动为当前版本中所有新增页面(newPages)以及包含变更的更新页面(updatedPages)旁边显示紧凑的“新”徽章(受 i18n.versionNavigationNewLabel 控制)。

变化总览页面

除了在具体页面、段落与导航中自动标记徽章外,站点还可以按需添加独立的变化总览路由:

src/routes/whats-new/+page.svelte
<script>
  import 
type VersionChanges = {
    $on?(type: string, callback: (e: any) => void): () => void;
    $set?(props: Partial<Record<string, never>>): void;
}
const VersionChanges: Component<Record<string, never>, {}, "">
VersionChanges
from '@sveltepress/theme-default/VersionChanges.svelte'
</script> <const VersionChanges: Component<Record<string, never>, {}, "">VersionChanges />
svelte

可在本站的新增内容页面查看对应路由示例。演练场中的文档版本管理 Starter 同样带有 /whats-new/ 目录、/guide/ 下的自动侧边栏,以及当前 1.1 页面上的 New 徽章。

按路由隔离的新增内容

VersionChanges 默认采用当前页面 URL 解析出的文档版本;有效的 ?version={id} 可显式覆盖该上下文。每个版本的变化总览都以清单中紧邻的上一版本为比较基准,历史变化集冻结后不受后续当前文档影响。当前链接不加前缀,历史链接精确指向 /v/{id}/... 及段落锚点。

自定义主题可以读取相同的冻结数据:

import { const changeSets: Record<string, VersionChangeSet>changeSets, const resolveVersionChanges: (versionId?: string, pathname?: string) => VersionChangeSet | nullresolveVersionChanges } from 'virtual:sveltepress/versions'

const const currentChanges: VersionChangeSet | nullcurrentChanges = function resolveVersionChanges(versionId?: string, pathname?: string): VersionChangeSet | nullresolveVersionChanges()
const const historicalChanges: VersionChangeSet | nullhistoricalChanges = function resolveVersionChanges(versionId?: string, pathname?: string): VersionChangeSet | nullresolveVersionChanges('8.1')
ts

versions create 会把即将冻结的当前变化集写入不可变产物清单和源码增量。历史变化只从冻结元数据读取;versions validate 还会检查标记、版本引用、锚点唯一性、损坏产物和增量漂移。