diff --git a/infra/deploy/build.sh b/infra/deploy/build.sh index ec364f8..33c07d3 100755 --- a/infra/deploy/build.sh +++ b/infra/deploy/build.sh @@ -28,6 +28,7 @@ pnpm astro build # 存在性守卫:新文件未推送到 main 前构建仓无此脚本,绝不让发布链因车道在途文件断掉 [ -f scripts/build-perf/subset-font-css.mjs ] && node scripts/build-perf/subset-font-css.mjs dist || echo "[build] subset 脚本未就绪,跳过" # 首页阻塞 CSS 内联(车道 D):守卫先行同上——脚本未到 main 前绝不炸链 +[ -f scripts/build-perf/gen-sw-precache.mjs ] && node scripts/build-perf/gen-sw-precache.mjs dist || echo "[build] sw-precache 生成器未就绪,跳过" [ -f scripts/build-perf/inline-home-css.mjs ] && node scripts/build-perf/inline-home-css.mjs || echo "[build] inline-home 脚本未就绪,跳过" echo "[build] rsync -> $STAGING" diff --git a/public/admin/config.yml b/public/admin/config.yml index b41d6e2..2ae6b37 100644 --- a/public/admin/config.yml +++ b/public/admin/config.yml @@ -16,6 +16,22 @@ output: omit_empty_optional_fields: true collections: + # 瞬间:随手发(text/image 至少其一——Sveltia 无法表达条件必填,两者都设可选, + # 保存前自查;pubDate 必填)。图片走图库媒体库(content/photos 同源可复用) + - name: moments + label: 瞬间 + folder: content/moments + create: true + slug: '{{year}}-{{month}}-{{day}}-{{slug}}' + summary: '{{fields.pubDate}} · {{fields.text}}' + fields: + - { label: 时间, name: pubDate, widget: datetime, date_format: 'YYYY-MM-DD', time_format: false, format: 'YYYY-MM-DD' } + - { label: 文字, name: text, widget: text, required: false, hint: '一句话(与图片至少填一项)' } + - { label: 图片, name: image, widget: image, required: false, hint: '与文字至少填一项;建议先压缩' } + - label: 标签 + name: tags + widget: list + required: false - name: photos label: 图库照片 folder: content/photos diff --git a/public/sw.js b/public/sw.js index acfa60f..de0c8f0 100644 --- a/public/sw.js +++ b/public/sw.js @@ -10,12 +10,15 @@ const VERSION = 'w4x-v2'; // install 预缓存核心路由(SSG 全量 ~30 页 HTML,<1MB,换来全站离线可读; // 列表由构建产物路由手工维护,新增顶层页时补一行) +// PRECACHE:AUTO-BEGIN 以下为兜底清单——构建期由 scripts/build-perf/gen-sw-precache.mjs +// 按产物自动重写(规则:顶层+en 轨路由,深度≤1 段;/admin 永不预缓存) const PRECACHE = [ '/', '/archives/', '/gallery/', '/moments/', '/books/', '/opensource/', '/about/', '/terminal/', '/en/', '/en/archives/', '/en/gallery/', '/en/moments/', '/en/books/', '/en/opensource/', '/en/about/', '/en/terminal/', ]; +// PRECACHE:AUTO-END const IMMUTABLE = [ /\/_astro\//, // 构建产物(文件名带哈希) /\/fonts?\//, // 字体切片(fontsource,内容不变) diff --git a/scripts/build-perf/gen-sw-precache.mjs b/scripts/build-perf/gen-sw-precache.mjs new file mode 100644 index 0000000..f1a0f19 --- /dev/null +++ b/scripts/build-perf/gen-sw-precache.mjs @@ -0,0 +1,40 @@ +/** + * SW PRECACHE 自动生成:post-build 按 dist 产物推导路由清单,重写 dist/sw.js + * 兜底段(AUTO-BEGIN/END 标记之间)。规则:顶层路由 + /en/ 轨(路径段 ≤1, + * 排除 /en 自身段计数),排除 /admin(CMS 非离线内容)。新增顶层页自动入列—— + * 根治手工清单漏页(Phase 5e 维护债)。 + */ +import { readdirSync, readFileSync, writeFileSync, statSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; + +const dist = process.argv[2] ?? 'dist'; +const swPath = join(dist, 'sw.js'); +const routes = []; + +// dist 下所有 index.html → 站点路由(目录名映射) +function walk(dir, prefix) { + for (const name of readdirSync(dir)) { + const full = join(dir, name); + if (!statSync(full).isDirectory()) continue; + if (name === 'admin') continue; // CMS 不进离线包 + walk(full, `${prefix}/${name}`); + } + if (prefix && existsSync(join(dir, 'index.html'))) { + const segs = prefix.replace(/^\/en(?=\/|$)/, '').split('/').filter(Boolean); + if (segs.length <= 1) routes.push(prefix.endsWith('/') ? prefix : `${prefix}/`); + } +} + +walk(dist, ''); + +routes.sort(); +const sw = readFileSync(swPath, 'utf-8'); +const re = /(\/\/ PRECACHE:AUTO-BEGIN[\s\S]*?const PRECACHE = \[)[\s\S]*?(\];\n\/\/ PRECACHE:AUTO-END)/; +if (!re.test(sw)) { + console.error('[gen-sw-precache] sw.js 缺少 AUTO 标记,跳过(保持兜底清单)'); + process.exit(0); +} +const list = routes.map((r) => ` '${r}',`).join('\n'); +const next = sw.replace(re, `$1\n${list}\n$2`); +writeFileSync(swPath, next); +console.log(`[gen-sw-precache] ${routes.length} 路由入预缓存: ${routes.slice(0, 4).join(' ')}${routes.length > 4 ? ' …' : ''}`);