项目共有 6 个 Node 脚本(scripts/*.js),其中 3 个作为 Vite 插件在构建期自动执行,1 个在 astro build 之后执行,2 个是手动命令。
1. 脚本总览
| 脚本 | 触发方式 | 触发时机 | 是否改写源文件 | 产物 |
|---|---|---|---|---|
ensure-post-slugs.js | Vite 插件(buildStart) | 构建 / dev 启动 | ✅ 补写 slug | .generated/post-index.json |
generate-lqips.js | Vite 插件(buildStart) | 构建 / dev 启动 | ❌ | .generated/lqips.json |
update-post-updated.js | Vite 插件(buildStart,受配置开关控制) | 构建 / dev 启动 | ✅ 写 updated | .generated/post-updated.json |
postbuild.js | pnpm build 的第二段命令 | astro build 之后 | 改写 dist/**/*.html | 内联脚本压缩 + modulepreload + pagefind 索引 |
new-post.js | pnpm new-post [slug] | 手动 | ✅ 新建文件 | src/content/posts/{slug}.md |
format-post-meta.js | pnpm format-post-meta | 手动 | ✅ 重排 frontmatter | — |
三个脚本会改写你的 markdown 源文件
ensure-post-slugs、update-post-updated、format-post-meta都会写文件。建议:
- 先构建再提交(
pnpm build && git commit),让自动改动一次进版本库;- 若 CI 环境是只读文件系统,请关闭
siteConfig.autoUpdatePostUpdated(默认已关闭),并确保所有文章已有slug。
2. scripts/ensure-post-slugs.js
2.1 职责
- 扫描
src/content/posts/**/*.md; - 对没有
slug的文章,用title生成拼音 slug 并写回文件; - 生成
.generated/post-index.json供rehypeRelativeLinks使用。
2.2 参数
| 常量 | 值 | 说明 |
|---|---|---|
CONTENT_ROOT | src/content | 内容根目录 |
POSTS_DIR | src/content/posts | 扫描起点(递归) |
INDEX_FILE | .generated/post-index.json | 输出(目录不存在会自动创建) |
SLUG_RE | /^[a-z0-9]+(?:-[a-z0-9]+)*$/ | slug 合法性校验,与 content.config.ts 的正则一致 |
2.3 titleToSlug(title) 的 pinyin 参数
pinyin(String(title), {
toneType: 'none', // 不带声调:zhōng → zhong
type: 'array', // 返回数组,便于逐段处理
nonZh: 'consecutive', // 非中文连续保留成一段(英文单词不会被拆开)
v: true, // ü 转成 v(如 lü → lv)
})
随后:join(' ') → toLowerCase() → 非 [a-z0-9\s-] 变空格 → 空白与连续 - 折叠 → 去首尾 - → 空则 'post'。
2.4 查重与写入
| 情况 | 处理 |
|---|---|
| slug 已被其他文章占用 | 依次尝试 {base}-1、{base}-2… |
| 仍冲突 | 追加 Date.now().toString(36).slice(-4) |
| slug 不合法(正则不过) | 直接置为 'post' |
文件原本有 slug: 行(空值) | 原地替换该行 |
文件没有 slug: 行 | 插到 title: 行的下一行 |
文件没有 title: 行 | 插到开头 --- 之后 |
| 换行符 | 检测到 \r\n 则沿用 CRLF |
控制台输出:[post-slug] slug filled: 文件名 -> slug
2.5 post-index.json 结构
[
{ "file": "posts/Markdown语法样例.md", "slug": "markdown-yu-fa-yang-li", "draft": false }
]
| 字段 | 说明 |
|---|---|
file | 相对 src/content 的路径(正斜杠,跨平台一致) |
slug | 文章 slug |
draft | 是否草稿(供相对链接在生成环境给出警告) |
排序:按 file 字典序。内容未变化时不重写文件(对比后写入)。
手动运行:该文件没有直接运行入口(不像
generate-lqips.js有process.argv[1]判断)。直接node scripts/ensure-post-slugs.js只会 import 模块——它仍然会执行模块顶层语句(mkdirSync创建.generated/目录),但不会扫描或改写任何文章。要单跑请调用工厂:js12import { ensurePostSlugs } from './scripts/ensure-post-slugs.js' ensurePostSlugs().buildStart()
3. scripts/generate-lqips.js
3.1 职责
扫描文章里的图片引用,采样 3 个主色,写入 .generated/lqips.json,供 rehypeImages 生成首屏渐变占位。
3.2 参数与环境变量
| 名称 | 值 | 说明 |
|---|---|---|
LQIP_FORCE | 环境变量,'1' 时强制全量 | 默认增量(跳过已存在的 key) |
IMG_SRC_RE | /!\[[^\]]*\]\(\s*([^)\s]+)(?:\s+"[^"]*")?\s*\)/g | 匹配  |
| 远程图片超时 | 8000ms(fetchBuffer 的 AbortController) | 超时即跳过该图 |
OUT_FILE | .generated/lqips.json | 输出 |
| 支持扩展名 | .webp .png .jpg .jpeg .gif .svg .avif .ico .bmp | 其他类型不处理 |
3.3 处理流程
1. 遍历 src/content/posts 下所有 .md
2. stripFencedCode() 去掉 ``` 围栏代码块(避免把示例里的图片语法当真)
3. stripInlineCode() 去掉 `行内代码`
4. 正则提取所有图片 src:
- data: 开头 → 跳过
- http(s):// 开头 → 作为远程 URL 目标(kind: 'url')
- / 或 # 或 scheme → 跳过(public 资源与锚点不采样)
- 其他(相对路径) → 解析成本地绝对路径,算 contentImageKey
5. 跳过 existing 里已有的 key(增量)
6. 逐个处理:
- 本地文件 → sharp 直接读
- 远程 URL → fetch(8s 超时)→ Buffer
7. sampleColors(): resize(2,2,{fit:'fill'}).removeAlpha().raw()
→ 取字节 0..3、3..6、9..12 三个像素的 RGB 十六进制拼接(共 18 位)
8. 过滤掉已不在 targets 里的旧 key,按 key 排序后写文件
3.4 输出格式
值是18 位无分隔符的小写十六进制字符串(3 个像素的 RGB 依次直接拼接,没有空格):
{
"/content-images/883d9fc6f4cc516e.webp": "632d1b6a301b582816"
}
拆解:632d1b(第 1 像素)+ 6a301b(第 2 像素)+ 582816(第 4 像素)。
rehypeImages.ts 消费时按 18 位长度校验,切成 3 个颜色生成 linear-gradient(135deg, c1 0%, c2 50%, c3 100%)。
3.5 控制台输出与失败处理
| 输出 | 含义 |
|---|---|
[lqip] processed N (ok M), total K entries | 处理了 N 张,成功 M 张,累计 K 条(N=本次待处理数,含失败) |
[lqip] incremental hit, total K entries, nothing new | 无新增 |
[lqip] sampling failed, skipped (runtime falls back to gray gradient): … | 单张失败,运行时降级灰渐变 |
[lqip] generation failed, building without LQIP fallback skeleton: … | 整体异常,构建继续(不阻断) |
3.6 常见操作
| 需求 | 命令 |
|---|---|
| 新增图片后重算 | pnpm build(自动增量) |
| 替换图片内容但路径不变 | $env:LQIP_FORCE=1; pnpm build(PowerShell)/ LQIP_FORCE=1 pnpm build(bash) |
| 单跑脚本 | node scripts/generate-lqips.js(有直接运行入口) |
| 完全关闭 LQIP | 在 astro.config.mjs 的 vite.plugins 中移除 generateLqips()(图片退化为灰白 shimmer) |
4. scripts/update-post-updated.js
4.1 职责
按文章文件的 mtime 同步 frontmatter 的 updated 字段。仅在 buildStart 执行(构建、dev 启动各一次),dev 运行中保存文章不会触发,避免保存过程中的中间态污染。
4.2 开关
astro.config.mjs:
updatePostUpdated({ enabled: siteConfig.autoUpdatePostUpdated })
enabled 为 false 时函数返回 null,Vite 忽略该插件(当前默认关闭)。
4.3 参数
| 常量 | 值 |
|---|---|
POSTS_DIR | src/content/posts |
STATE_FILE | .generated/post-updated.json |
| 时间戳格式 | YYYY-MM-DD HH:mm:ss(UTC 时间) |
4.4 状态文件与幂等机制
{
"MarkDown元数据说明.md": { "mtime": 1788447321747, "stamp": "2026-09-03 22:55:21" }
}
mtime是毫秒时间戳,stamp是同一时刻的 UTC 时间字符串(用getUTC*格式化,不依赖构建机时区;示例取自仓库真实的.generated/post-updated.json)。
| 判断 | 行为 |
|---|---|
状态里的 mtime 与当前文件 mtime 相同 | 说明同步后文件没被再保存 → 跳过 |
mtime 不同 | 读文件 → 生成新 stamp → 比较现有 updated → 不同则写入 |
现有 updated 已等于新 stamp | 只更新状态,不写文件 |
| 文件无 frontmatter | 记录状态并跳过 |
| 状态里有但文件已删除的条目 | buildStart 时清理 |
为什么用「mtime 快照」而不是把 mtime 改回去?
源码注释解释得很清楚:写入
updated本身会刷新文件mtime,所以需要记录「写入后的 mtime」来判断是否被再次保存。不能写完再把
mtime改回原值——Git for Windows 的索引按秒比对 mtime,恢复后字节数相同的时间戳变更会被 git 误判为「未修改」,导致改动提交不上去。
4.5 写入位置
| 情况 | 处理 |
|---|---|
已有 updated: 行 | 原地替换 |
无 updated: 行,有 draft: 行 | 插到 draft: 之前(与 format-post-meta 的规范顺序一致) |
无 updated: 也无 draft: | 追加到 frontmatter 末尾 |
换行符沿用原文件。
控制台输出:[post-updated] 文件名 -> 2026-09-15 20:49:56(每篇一行)、[post-updated] synced N posts(汇总)。
4.6 启用建议
| 谁 | 建议 |
|---|---|
| 本地写作,用 git 管理 | 可以开启,方便自动维护更新时间 |
| CI / 容器化构建 | 保持关闭(构建产物不应产生源文件改动,且 CI 上的文件系统可能只读) |
手工维护 updated | 保持关闭 |
5. scripts/postbuild.js
5.1 职责(四件事,并行 + 串行结合)
main()
├─ 并行:spawn('pagefind', ['--site', 'dist']) ← 生成站内搜索索引
└─ 串行:遍历 dist/**/*.html
├─ minifyHtml() 压缩内联脚本(esbuild)
└─ injectPreloads() 注入 modulepreload
└─ 串行:trimKatexFonts() ← 删除 dist/_astro 里 KaTeX 的 ttf/woff,只留 woff2
5.2 参数
| 常量 / 选项 | 值 | 说明 |
|---|---|---|
DIST_DIR | path.resolve(process.cwd(), 'dist') | 不存在时打印 [postbuild] dist/ not found, run astro build first 并 exitCode = 1 |
SCRIPT_RE | /<script([^>]*)>([\s\S]*?)<\/script>/gi | 匹配所有 script 块 |
JS_TYPES | '', text/javascript, application/javascript, module | 只有这些 type 会被压缩 |
shouldMinify | 含 src= → 跳过;type 不在 JS_TYPES → 跳过 | 外链脚本与 JSON-LD(application/ld+json)不压缩 |
esbuild target | 'es2018' | 压缩目标语法版本 |
esbuild minifyWhitespace | true | — |
esbuild minifySyntax | true | — |
esbuild minifyIdentifiers | false | 不改标识符名(避免破坏内联脚本里的具名引用) |
| 安全回退 | 压缩结果含 </script → 放弃压缩,保留原文 | 防止提前闭合标签 |
DEPS_RE | /__vite__mapDeps=\(i,m=__vite__mapDeps,d=\(m\.f||\(m\.f=\[([^\]]*)\]\)/ | 解析 Vite 生成的依赖映射 |
| pagefind 命令 | pagefind --site dist,Windows 下 shell: true | 读取项目根 pagefind.yml |
KATEX_LEGACY_FONT_RE | /^KaTeX_.*\.(ttf|woff)$/ | 字体裁剪只匹配 ttf / woff,.woff2 保留(现代浏览器只取 woff2) |
5.3 输出统计
[postbuild] minified N inline scripts, saved X KiB; injected M preload links (T/N HTML files touched)
[postbuild] trimmed K KaTeX font files (ttf/woff, woff2 kept), saved Y KiB
[postbuild] dist total A MiB (F files), _astro B KiB (G files)
[postbuild] top 5: <相对路径> <体积> | …
| 字段 | 含义 |
|---|---|
N | 被压缩的内联脚本数量 |
X KiB | 累计节省字节 |
M | 注入的 modulepreload 链接数 |
T/N | 被改写 / 总 HTML 文件数 |
K / Y KiB | 被删除的 KaTeX ttf/woff 数量与节省字节;已裁剪过时打印 no ttf/woff to trim (already trimmed)(幂等) |
A / F | dist/ 全部产物的总字节与文件数(含 Pagefind 索引、图片、public/music/ 音频等正式资源) |
B / G | 其中 dist/_astro/ 哈希资源的字节与文件数 |
top 5 | 全量产物里最大的 5 个文件(相对 dist/ 的路径 + 体积) |
5.4 体积预算
| 项 | 值 |
|---|---|
| 阈值 | BUDGET_BYTES = 60 * 1024 * 1024(60 MiB) |
| 统计范围 | dist/ 全量产物,包含音频与图片(不做优化或排除) |
| 超阈值行为 | console.warn 打印 budget exceeded: <总量> > 60.0 MiB (audio included) 并逐条列出 top 5;仅告警,不阻断构建(exit code 仍为 0) |
| 观察方式 | 每次 pnpm build 都会打印总量与 top 5,用于观察趋势、及时发现异常膨胀 |
预算实测与验证
V1.0.9 基线本地实测
dist total 6.3 MiB (122 files)、_astro 499.1 KiB (50 files)。触发告警的验证方式是临时把一个 58 MiB 文件放进public/后构建:总量 64.3 MiB → 输出budget exceeded与 top 5 明细(首条即该文件),删除后重建恢复正常。计划书原写「6 MB 文件」是按更大的历史产物量估算,当前基线需约 54 MiB 以上才会越过 60 MiB 门槛。
5.5 失败处理
字体裁剪与 KaTeX 按需加载配套
文章页只在正文真的渲染出
.katex时才动态import('katex/dist/katex.min.css')(见 05-内容与Markdown.md)。官方 CSS 的@font-face按 woff2 → woff → ttf 排列,后两者只是老浏览器兜底,因此构建后直接删除,dist/_astro里的 KaTeX 字体约 250 KiB。
5.4 失败处理
| 失败点 | 行为 |
|---|---|
| 单个脚本压缩失败 | console.warn 并保留原脚本,继续 |
pagefind 启动失败 | 打印 [postbuild] pagefind failed to start: 并 exitCode = 1 |
pagefind 非 0 退出 | 打印 [postbuild] pagefind exited with code N 并 exitCode = 1 |
dist 不存在 | 提示先跑 astro build,exitCode = 1 |
为什么需要 modulepreload 注入?
Vite 默认只对入口 chunk 输出 <link rel="modulepreload">,动态 import 的 chunk 依赖需要浏览器在运行时发现。injectPreloads 读取每个 module script 引用的 chunk,从里面的 __vite__mapDeps 数组解析出依赖列表,提前写进 <head>,从而减少瀑布式请求。
修改
pagefind.yml后必须重新构建
pagefind 只在 postbuild.js 里被调用。改 pagefind.yml 后需要 pnpm build 才能反映到索引。
6. scripts/new-post.js
| 项 | 说明 |
|---|---|
| 命令 | pnpm new-post [slug] |
| 参数 | process.argv[2];缺省为 post-{YYYY-MM-DD} |
| 输出路径 | src/content/posts/{slug}.md |
| 已存在 | 打印 already exists: <绝对路径>,process.exit(1) |
| 目录 | mkdirSync(dir, { recursive: true }) |
模板:
---
title: "{slug}"
slug:
index: 0
description: "一句话摘要"
category: "未分类"
tags: []
# 连载(同一主题 3 篇以上、需按顺序阅读)时才填写,零散文章留空:
# series: "系列名"
# series_order: 1
published: {当前 UTC 时间,YYYY-MM-DD HH:mm:ss}
---
从这里开始写作。
成功后打印 created: <绝对路径>。
7. scripts/format-post-meta.js
| 项 | 说明 |
|---|---|
| 命令 | pnpm format-post-meta |
| 作用 | 统一重排所有文章 frontmatter 的字段顺序 |
| 幂等 | 顺序已正确时不写文件 |
| 输出 | formatted: <相对路径>(每篇)+ formatted N posts 或 all post meta already in canonical order |
字段顺序规则
const ORDER = ['title', 'slug', 'index', 'description', 'category', 'tags', 'published']
const LAST = ['updated', 'draft']
最终顺序 = ORDER 中存在的 → 其他未列出的自定义字段(保持原相对顺序)→ LAST 中存在的。
| 实现细节 | 说明 |
|---|---|
| 字段识别 | ^([A-Za-z_][\w-]*):,非字段行归入上一个字段(支持多行数组等) |
| 重复 key | 合并到首次出现的位置 |
| 换行符 | 沿用原文件 |
| 无 frontmatter | 跳过 |
8. 构建产物结构
dist/
├── index.html # 首页(第 1 页)
├── 2/index.html 3/index.html … # 首页分页(第 1 页除外)
├── 404.html
├── about/index.html
├── archive/index.html
├── categories/index.html
├── categories/{分类名}/index.html
├── tags/index.html
├── tags/{标签名}/index.html
├── friends/index.html
├── posts/{slug}/index.html
├── content-images/{sha1前16位}.{ext} # 文章图片
├── pagefind/ # 搜索索引(postbuild 生成,以下为实测产物)
│ ├── pagefind.js # 检索入口(SearchModal 动态 import 的就是它)
│ ├── pagefind-ui.js pagefind-ui.css pagefind-highlight.js
│ ├── pagefind-modular-ui.js pagefind-modular-ui.css
│ ├── pagefind-component-ui.js pagefind-component-ui.css
│ ├── pagefind-entry.json pagefind-worker.js wasm.unknown.pagefind
│ ├── pagefind.{lang}_{hash}.pf_meta
│ ├── filter/ {lang}_{hash}.pf_filter
│ ├── fragment/ {lang}_{hash}.pf_fragment # 注意:在子目录里
│ └── index/ {lang}_{hash}.pf_index # 注意:在子目录里
├── rss.xml
├── robots.txt
├── sitemap-index.xml
├── sitemap-0.xml
├── _astro/ # 带内容哈希的 JS/CSS
├── avatar.webp logo.webp favicon.ico
├── friends/logo.webp
└── music/ # public/ 原样拷贝
8.1 缓存策略
本项目是纯静态产物,缓存由托管平台决定。全站只有 /_astro/ 与 /content-images/ 带内容哈希,其余资源路径固定——因此不能对所有资源用长缓存。
| 路径 | 建议缓存头 | 理由 |
|---|---|---|
/_astro/* | public, max-age=2592000, immutable(30 天) | 文件名含内容哈希,升级后 URL 变化,可长期缓存 |
/content-images/* | 路由已自带 public, max-age=31536000, immutable(一年) | 见下方「缓存陷阱」 |
/pagefind/* | public, max-age=604800(7 天)或随构建失效 | 文件名含内容哈希,但索引整体随内容变化 |
| 图片 / 字体 / CSS / JS / XML / TXT | public, max-age=604800(7 天) | 无哈希,需短缓存 |
*.html | no-cache 或很短(如 max-age=0, must-revalidate) | 内容随每次发布变化,不能长缓存 |
上表是「按路径分层」的原则,具体在哪个平台怎么填,由该平台的缓存配置入口决定。
content-images的缓存陷阱
/content-images/{hash}中 hash 只基于相对路径,不基于内容。且路由响应的Cache-Control是max-age=31536000, immutable(一年)。后果:替换
src/content/images/logo.webp的内容但路径不变时,URL 也不变,已访问过的浏览器会长期使用旧图。规避方式:改文件名(如
logo-v2.webp),或清理部署平台缓存。
8.2 HTML 后处理的具体变化
postbuild.js 会改写每个 HTML 文件:
| 变化 | 示例 |
|---|---|
| 内联脚本压缩 | <script>!function(){...}</script> 变成单行压缩版 |
| 注入 preload | <head> 末尾追加 <link rel="modulepreload" href="/_astro/xxx.js"> |
| 不改 | 外链脚本、application/ld+json、HTML/CSS 结构 |
9. 部署
9.1 环境要求
| 项 | 要求 |
|---|---|
| Node.js | >= 22.14.0(package.json 的 engines) |
| 包管理器 | pnpm(preinstall 脚本用 npx only-allow pnpm 强制),packageManager: pnpm@10.30.0 |
| 构建命令 | pnpm build |
| 输出目录 | dist |
9.2 各平台
| 平台 | 配置 |
|---|---|
| Cloudflare Pages | Build command: pnpm build;Output directory: dist;Node 版本设 22 |
| Vercel / Netlify | 同上,注意安装 pnpm(corepack enable) |
| GitHub Pages | 需要设置 siteConfig.url 为 Pages 域名(含子路径时需 Astro 的 base 配置,项目未内置) |
| 自托管(对象存储 / CDN / 自己的服务器) | 需要自行准备托管与缓存配置:把 dist/ 作为站点根目录,并配置「未知路径回落到 /404.html」 |
9.3 托管平台缓存与尾斜杠
更新说明.md 提到过一个真实问题:
在进行页面切换时发现了多次重复请求预取的问题,导致页面流量异常,经排查为预取请求与 Canonical URL 不一致造成的。将前端路由的缓存策略与托管平台(Cloudflare Pages)的 URL 规范严格对齐,现在已修复。
结论:全站 URL 统一使用尾斜杠形式(/posts/xxx/、/categories/xxx/)。localePath() 会自动补尾斜杠,但对含 . 的最后一段(如 rss.xml)不补(源码里 isFile 判断)。
若你换了托管平台且该平台默认去掉尾斜杠(或反之),需要保证 301 规则一致,否则 Swup 预取会产生大量重复请求。
10. 常用命令速查
| 命令 | 实际执行 | 副作用 |
|---|---|---|
pnpm dev / pnpm start | astro dev | 触发已注册的 buildStart 插件:ensure-post-slugs(会补写 slug 到 md)、generate-lqips;update-post-updated 在默认 false 下未注册 |
pnpm build | astro build && node scripts/postbuild.js | 写 md 源文件 + 生成 .generated/ + pagefind 索引 |
pnpm preview | astro preview | — |
pnpm check | astro check | Astro 组件与类型检查 |
pnpm type-check | tsc --noEmit | 覆盖 src 与 scripts |
pnpm new-post [slug] | node scripts/new-post.js | 新建 md |
pnpm format-post-meta | node scripts/format-post-meta.js | 重排全部文章 frontmatter |
pnpm format | biome format --write ./src | 格式化 src |
pnpm lint | biome check --write ./src | 检查并修复 src |
pnpm format:all | biome check --write ./src ./scripts ./astro.config.mjs ./biome.json ./package.json ./tsconfig.json | 全项目检查修复 |
preinstall | npx only-allow pnpm | 自动执行,非 pnpm 安装会失败 |
推荐的本地工作流
pnpm new-post my-post # 1. 新建
# 2. 写作
pnpm format-post-meta # 3. 元数据排序
pnpm dev # 4. 预览
pnpm check && pnpm type-check # 5. 检查
pnpm build # 6. 构建(补 slug / LQIP / 搜索索引)
pnpm preview # 7. 验证 dist
git add -A && git commit # 8. 提交(先 build 再 commit)
11. 构建问题排查表
| 现象 | 原因 | 解决 |
|---|---|---|
Invalid slug / Zod 校验失败 | slug 含大写、下划线或中文 | 改成小写 + 连字符,或直接留空让脚本生成 |
index 校验失败 | index 为负数或小数 | 改成非负整数 |
published 校验失败 | 日期格式不可解析 | 改成 YYYY-MM-DD HH:mm:ss |
目录里突然多了 slug: 行 | ensure-post-slugs 正常工作 | 预期行为 |
updated 被自动改写 | autoUpdatePostUpdated: true | 设为 false,或接受该行为 |
[rel-link] … kept as-is 警告 | 相对链接目标不存在 | 检查 .md 路径与目标文章的 slug |
[lqip] sampling failed | 远程图片不可达 / 本地文件缺失 | 检查图片路径;不影响构建 |
[postbuild] pagefind failed to start | 未安装依赖或 PATH 问题 | pnpm install,确认 pagefind 可执行 |
| 搜索结果为空 | 未执行 pnpm build(dev 下无索引) | 先构建;且文章需有 data-pagefind-body 容器 |
| 图片不显示 | 图片不在 src/content/ 下,或用了绝对路径 | 用相对路径(见 05 文档#7) |
preinstall 报错 | 用了 npm/yarn | npm i -g pnpm 后改用 pnpm install |
构建后 git status 有大量改动 | 三个脚本改写了 md | 预期行为,先 build 再 commit |