前言
这个博客自 2025-07-18 之后就没怎么动过了。时间一长,几个问题会一起冒出来:Dependabot 攒了 5 个依赖 PR 没人合、质量门禁从 2025-12 开始连续 10 次失败、内容集合还停留在旧式 API、以及很多别的 Fuwari 站点早就有的阅读与交互功能本站一直缺着。
所以这次不是「加个小功能」,而是一次从 CI 到内容层再到前端交互的整包升级。全程以 Node 24 本地开发、保持 engines.node >= 18 与 CI 的 [22, 23] 矩阵不变,最后同步到 GitHub 主仓库并由 Vercel 自动重部署。
NOTE本文属于「博客改造计划」系列,写法沿用之前的教程体:先讲清问题,再给出改动前后的代码差异,最后附上验证结果。所有改动都已上线,可以直接打开本站对应页面对照。
TIP本文开启了
comments: true,底部的 Waline 评论区正好可以用来验证第五部分修好的暗黑模式可读性——切到夜间模式看看「评论」计数和排序标题是不是变成浅色了。
本次改动总览
四个提交,按依赖顺序落地:
| 提交 | 日期 | 内容 | 规模 |
|---|---|---|---|
34bf148 | 2026-11-06 | 修复 CI 门禁、整合依赖、迁移 Content Layer、关闭 5 个 PR | 15 files, +1834 / -1276 |
b536f6f | 2026-11-06 | 参考 Besty0728/fuwari 的全站功能改造 | 20 files, +1096 / -28 |
00bebbb | 2026-11-06 | 悬浮面板与 /hot/ 排序标签的暗黑可读性修复 | 1 file, +16 / -2 |
77878ce | 2026-11-09 | Waline 评论组件的暗黑可读性修复 | 1 file, +10 |
一、修复 CI 质量门禁(commit 34bf148)
问题:Biome 版本漂移
先说最容易被忽略、但其实最该先修的一项。原来的质量门禁是这样写的:
name: Code quality
on: push: branches: [ main ] # Adjust branches as needed pull_request: branches: [ main ] # Adjust branches as needed
jobs: quality: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Biome uses: biomejs/setup-biome@f382a98e582959e6aaac8e5f8b17b31749018780 # v2.5.0 with: version: latest - name: Run Biome run: biome ci ./src --reporter=github关键在 version: latest。仓库里锁定的却是 @biomejs/biome: 2.1.1,而 CI 里那个 latest 实际跑到了 Biome 2.6.5。两个版本的规则不一致,于是每次 biome ci 都会吐出约 10 条 annotation,main 分支上这个检查自 2025-12 起连续 10 次 failure。
CAUTION这是个「既有问题」,不是那 5 个 dependabot PR 引入的。也就是说,光把 PR 合掉并不会让 CI 变绿——必须先把门禁修对,否则后面每次提交都还是红的。
修法:让 CI 用与仓库完全相同的版本
思路很简单:不要在 CI 里单独装一个 Biome,直接用 pnpm install --frozen-lockfile 装出来的那个,再用 pnpm exec 调用它。
name: Code quality
on: push: branches: [ main ] pull_request: branches: [ main ]
concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true
permissions: contents: read
jobs: quality: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup Biome uses: biomejs/setup-biome@f382a98e582959e6aaac8e5f8b17b31749018780 # v2.5.0 with: version: latest - name: Setup pnpm uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 with: run_install: false - name: Setup Node.js uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: 24 - name: Install dependencies run: pnpm install --frozen-lockfile - name: Run Biome run: biome ci ./src --reporter=github run: pnpm exec biome ci ./src --reporter=github顺手补了两点:
concurrency:同一分支上重复推送时取消旧的在跑任务,避免排队浪费;permissions: contents: read:把 token 权限收到最小。
修完之后本地 pnpm exec biome ci ./src 与 CI 跑的是同一个 2.1.1,结果一致、可复现。
NOTE这里还有一个很容易踩的坑:仓库的 blob 本身是纯 LF,
git config core.autocrlf true只是把 checkout 出来的工作区转成 CRLF,Biome 2.1.1 对行尾并不敏感,但如果你为了「统一行尾」去做整树字节级重写,会损坏 PNG、并让git status凭空冒出上百个M。正确做法是git config core.autocrlf false后git reset --hard HEAD,工作区就回到纯 LF 了。别用git show | Out-File去测 blob 行尾,PowerShell 管线会自己转成 CRLF,测出来的是假的。
二、一次性整合依赖,处理 5 个 dependabot PR
5 个 open PR 全是 dependabot,mergeable_state 都是 unstable,而且它们都从同一个基线 097787d 切出来。
| PR | 内容 | head sha |
|---|---|---|
| #73 | patch 组 22 项 | 522e8167 |
| #72 | overlayscrollbars 2.11.4 → 2.14.0 | c84c5812 |
| #71 | svelte 5.36.1 → 5.53.7 | f5bff87b |
| #70 | unist-util-visit 5.0.0 → 5.1.0 | 442237b7 |
| #62 | pagefind 1.3.0 → 1.4.0 | 4f050e9e |
实测顺序合并:#73、#72 能过,但 #71、#70、#62 都会在 package.json 和 pnpm-lock.yaml 上冲突——因为它们各自独立地改了同一份依赖清单。与其一个个 rebase,不如把这 5 组升级一次性整合进一次 pnpm install,让 pnpm 自己重算 lockfile,再把 5 个 PR 统一留言 Superseded by the maintenance commit 34bf148 后关闭。
主要版本变化:
| 依赖 | 旧 | 新 |
|---|---|---|
astro | 5.11.1 | 5.18.2 |
@astrojs/svelte | 7.1.0 | 7.2.5 |
@astrojs/check | ^0.9.4 | ^0.9.12 |
@astrojs/rss | ^4.0.12 | ^4.0.19 |
@astrojs/sitemap | ^3.4.1 | ^3.7.4 |
astro-expressive-code 四件套 | ^0.41.3 | ^0.44.3 |
pagefind | ^1.3.0 | ^1.4.0 |
svelte | ^5.36.1 | ^5.53.7 |
tailwindcss | ^3.4.17 | ^3.4.19 |
overlayscrollbars / katex / markdown-it / sharp / unist-util-visit 等 | — | 同步升到 patch 组目标版本 |
同时在 package.json 里补上:
{ "engines": { "node": ">=18" }, "packageManager": "pnpm@9.14.4"}NOTE
build.yml的matrix.node: [22, 23]这次没有动。质量门禁用 Node 24 是因为它只跑 Biome;构建/检查矩阵仍保留 22、23 两个版本做兼容性验证,而engines.node >= 18只是给使用者一个下限声明。三者互不冲突。
升级后顺手把 Biome 2.1.1 报出来的 lint 问题清干净:src/utils/content-utils.ts 三处 map → forEach、archive/tag/[tag].astro:24 的 forEach 表达式体改块体、setting-utils.ts / ArchivePanel.astro 四处 Number.parseInt 补 radix 10、plugins/expressive-code/language-badge.ts:9 直接删掉 // @ts-ignore(那里 baseStyles 本来就合法,换成 @ts-expect-error 反而会报 ts(2578): Unused '@ts-expect-error' directive)。
CAUTION改完依赖一定要重新
pnpm install,否则pnpm-lock.yaml与package.json不一致,CI 上的--frozen-lockfile会直接失败。另外调整.astro缩进别手动敲 tab,用pnpm exec biome format --write <单文件>让格式化器来改最省事。
三、迁移到 Astro Content Layer API(本次最核心的代码改动)
旧版 Fuwari 用的是 src/content/config.ts + defineCollection({ schema }) 的旧式集合,Astro 5 已经推荐改用 Content Layer。这一步改动最大,也最需要小心 URL 兼容性。
3.1 集合定义:从 schema 到 glob loader
文件从 src/content/config.ts 移到根目录的 src/content.config.ts:
import { defineCollection, z } from "astro:content";import { defineCollection } from "astro:content";import { glob } from "astro/loaders";import { z } from "astro/zod";
const postsCollection = defineCollection({ schema: z.object({const posts = defineCollection({ loader: glob({ base: "./src/content/posts", pattern: "**/*.{md,mdx}" }), schema: z.object({ title: z.string(), published: z.date(), updated: z.date().optional(), draft: z.boolean().optional().default(false), description: z.string().optional().default(""), image: z.string().optional().default(""), tags: z.array(z.string()).optional().default([]), category: z.string().optional().default(""), lang: z.string().optional().default(""), comments: z.boolean().optional().default(false), pinned: z.boolean().optional().default(false), series: z.string().optional(), /* For internal use */ prevTitle: z.string().default(""), prevSlug: z.string().default(""), nextTitle: z.string().default(""), nextSlug: z.string().default(""), }),});export const collections = { posts: postsCollection,};
const spec = defineCollection({ loader: glob({ base: "./src/content/spec", pattern: "**/*.{md,mdx}" }), schema: z.object({}),});
export const collections = { posts, spec };要点:
z从astro/zod引入,不再从astro:content引入;- 每个集合都要声明
loader,用glob({ base, pattern })指向实际文件目录; spec集合(about / friends 等独立页面内容)也一起迁过来,之前getEntry("spec", "about")这类调用不用改;- 旧的
src/content/config.ts删除,避免两套配置并存。
3.2 调用点改动
全站所有读取文章的地方同步改三处:
import { getCollection, type CollectionEntry } from "astro:content";import { getCollection, render, type CollectionEntry } from "astro:content";
const { Content } = await entry.render();const { Content } = await render(entry);
const url = getPostUrlBySlug(entry.slug);const url = getPostUrlBySlug(entry.id);
const posts: CollectionEntry<"posts">[] = ...const posts: CollectionEntry<"posts">[] = ... // 类型名不变,数据源换成 loader受影响的还有 PostCard.astro、PostPage.astro、widget/Series.astro、archive/tag/[tag].astro、rss.xml.ts、utils/content-utils.ts 等一共十来个文件。
3.3 一个容易踩的坑:子目录封面图
IMPORTANT
globloader 默认生成的entry.id恰好等于旧式的entry.slug(同样做小写化、去掉/index),所以 51 条既有文章 URL 完全不变——这是本次迁移能安全落地的前提,我用构建后的 URL 清单做了 51/51 逐条 diff,LOST_COUNT=0。
但 id 里不含目录。像 src/content/posts/docker/index.md 这种文章,旧式 slug 是 docker,如果封面图用相对路径写在 frontmatter 里,按 id 去拼目录就会 404。为此新增一个 getPostDir(entry),用 Content Layer 保留的 entry.filePath 还原真实目录:
/** * Resolve the directory that contains a post's source file, relative to `src/`. * Content Layer entries keep their real path in `filePath` (e.g. * `src/content/posts/docker/index.md`), which is needed to locate cover images * referenced with relative paths. The `id` is only a slug and has no directory. */export function getPostDir(entry: { id: string; filePath?: string }): string { const filePath = entry.filePath ?? `src/content/posts/${entry.id}`; const dirname = filePath.includes("/") ? filePath.slice(0, filePath.lastIndexOf("/")) : ""; return `${dirname.replace(/^src\//, "")}/`;}TIP迁移是否成功,不要只看
astro check过没过。最可靠的验收是:干净克隆 →pnpm install→pnpm build→ 把产出的路由清单和迁移前对比。本次还额外确认了草稿(如case/http1.1.md的草稿态)没有生成页面、子目录封面图正常加载、spec的弃用警告消失。
这次维护还用 OpenSpec 留了完整档案:change revive-fuwari-maintenance 归档为 2026-11-06-revive-fuwari-maintenance,包含 ci-quality-gate、dependency-maintenance、content-collection-model 三项能力与 36 个任务,openspec validate --all 3 passed。
四、参考 Besty0728/fuwari 的全站功能改造(commit b536f6f)
维护做完,接下来是「把博客变好用」。参考对象是另一个重度改造过的 Fuwari 站点:
NOTE只借鉴功能面,不照搬代码。 参考仓库仍用旧式
src/content/config.ts,依赖版本比本站更旧(astro 5.7.9 / Biome 1.9.4),还塞了大量个人硬编码:Umami / GA 统计、AdSense、防盗链 Base64、收款码、经纬度、ICP 备案号、双 CDN……这些一律不引入。所有功能按本站的 Content Layer API 与现有组件重写。
4.1 配置中心:featureConfig
新增的开关与文案全部集中在 src/config.ts 的 featureConfig,类型定义在 src/types/config.ts:
/** * 站点附加功能(参考 Besty0728/fuwari 的「整个博客改造」)。 * 这里只放可安全公开的配置;第三方统计 / 广告 / 防盗链 / 收款码等个人化硬编码一律不引入。 */export const featureConfig: FeatureConfig = { readingProgress: { enable: true }, mermaid: { enable: true }, recentPosts: { enable: true, count: 5 }, forceDark: { enable: true, defaultOn: false }, randomBackground: { enable: true, defaultOn: false, opacity: 0.25 }, musicPlayer: { enable: true, defaultOn: false, // 默认使用一段免版权的公共音频;如要换成自己的,只需改这个 URL。 title: "Ambient", src: "https://cdn.pixabay.com/download/audio/2022/03/15/audio_c8c8a73467.mp3?filename=ambient-piano-logo-165357.mp3", }, hot: { enable: true }, works: { enable: true }, apps: { enable: true, items: [ /* Fuwari / Astro 文档 / p1ume 源码 */ ] }, labels: { /* 23 条中文文案,单一配置源 */ },};为什么文案放在labels里模板的
Translation = { [K in I18nKey]: string }要求每个新词都同步进 7 个语言文件,成本很高且容易漏。所以把「最近更新」「作品」「应用」「热门文章」「强制暗黑」「随机背景」「背景音乐」「发布时间 / 更新时间 / 字数」,以及导航入口名、空状态与「功能已关闭」提示这类面向访客的新文案统一收进featureConfig.labels,页面直接读,不去动src/i18n/languages/*。原有 i18n 机制完全保留。这样全站中文文案只有一个来源:导航、
/works/、/apps/、/hot/的空态与禁用态,改一处labels就全部生效,不会出现「英文导航配中文页面」的割裂感。
4.2 客户端配置桥与 site-features.js
服务端配置怎么传给浏览器?加一个 src/components/ConfigCarrier.astro,把所有开关写成 data-*:
---import { featureConfig, siteConfig } from "../config";---
<div id="config-carrier" data-hue={siteConfig.themeColor.hue} data-feature-force-dark={String(featureConfig.forceDark.enable)} data-feature-force-dark-default={String(featureConfig.forceDark.defaultOn)} data-feature-random-bg={String(featureConfig.randomBackground.enable)} data-feature-random-bg-opacity={String(featureConfig.randomBackground.opacity)} data-feature-music={String(featureConfig.musicPlayer.enable)} data-feature-music-title={featureConfig.musicPlayer.title} data-feature-music-src={featureConfig.musicPlayer.src} data-feature-reading-progress={String(featureConfig.readingProgress.enable)} data-feature-mermaid={String(featureConfig.mermaid.enable)} data-label-random-bg={featureConfig.labels.randomBg} data-label-force-dark={featureConfig.labels.forceDark} data-label-music={featureConfig.labels.music} data-label-controls={featureConfig.labels.controls}></div>功能逻辑放在 public/scripts/site-features.js(338 行,纯原生 JS、零 npm 依赖),以 is:inline 引入、不参与打包。它开头就读这份 carrier:
/* * 站点附加功能(参考 Besty0728/fuwari 的「整个博客改造」,按本仓库重写)。 * 纯原生 JS,无 npm 依赖;通过 #config-carrier 的 data-* 读取功能开关。 * 该脚本位于 public/ 下、以 is:inline 方式引入,不参与打包,在 Swup 页面切换时只需 * 监听 page:view 重新处理新内容即可。 */(() => { "use strict"; function cfg() { const c = document.getElementById("config-carrier"); if (!c) return {}; return { forceDark: c.dataset.featureForceDark === "true", randomBg: c.dataset.featureRandomBg === "true", randomBgOpacity: Number.parseFloat(c.dataset.featureRandomBgOpacity || "0.25"), mermaid: c.dataset.featureMermaid === "true", /* ... */ }; } /* ... */ if (window.swup?.hooks) { window.swup.hooks.on("page:view", onPageView); } else { document.addEventListener("swup:enable", () => { window.swup?.hooks?.on("page:view", onPageView); }); }})();因为本站用 Swup 做无刷新跳转,脚本必须监听 page:view 重新绑定事件、重绘图表、重算进度条,否则换页后功能就失效了。开关状态用 localStorage 记忆,主题切换用 MutationObserver 观察 html.class 来触发 Mermaid 重绘。
功能清单:
| 功能 | 实现要点 |
|---|---|
| 阅读进度条 | scrollTop / (scrollHeight - clientHeight),滚动时更新宽度 |
| Mermaid 图表 | 从 CDN 动态 import() mermaid@11,theme: dark ? "dark" : "default";因 Expressive Code 会把代码拆成 .ec-line,需逐行拼回源码再渲染 |
| 最近更新 | widget/RecentPosts.astro,按 updated ?? published 排序取前 5 条 |
| 强制暗黑 | 优先级高于 theme=auto/light/dark,可单独覆盖 |
| 随机背景 | 用 --random-bg-image 生成 linear-gradient(135deg, hsla(...), ...),透明度可配 |
| 背景音乐 | <audio> + play().catch(),默认关闭,URL 可换 |
| 热门排序 | 按 data-words / data-published / data-updated 重排 #hot-list,并 ?sort= 同步到地址栏 |
| 显示控制面板 | 齿轮按钮 + 三个开关 + 三个排序按钮,事件委托,不侵入现有组件 |
4.3 新页面与内容类型
本次一共新增 5 条路由(加上原 51 页共 56 页):
| 路由 | 说明 |
|---|---|
/works/ | 作品集列表,getCollection("works") 按 published 倒序 |
/works/fuwari-blog/、/works/music-panel/ | 作品详情,getStaticPaths + render(entry) |
/apps/ | 应用/常用链接,读 featureConfig.apps.items,外链自动 target="_blank" |
/hot/ | 热门文章,按发布时间 / 更新时间 / 字数排序 |
为此在 src/content.config.ts 里加了第三个集合 works:
/** 作品集:参照参考仓库的 works 内容类型,按 Content Layer glob loader 定义。 */const works = defineCollection({ loader: glob({ base: "./src/content/works", pattern: "**/*.{md,mdx}" }), schema: z.object({ title: z.string(), description: z.string().optional().default(""), published: z.date(), updated: z.date().optional(), tags: z.array(z.string()).optional().default([]), link: z.string().optional().default(""), image: z.string().optional().default(""), draft: z.boolean().optional().default(false), status: z.string().optional().default(""), }),});
export const collections = { posts, spec, works };友链页面也顺手改成数据驱动——以前是 friends.astro 里一大段硬编码数组,现在在 src/content/friends/ 下放 JSON 即可:
// 友链改为数据驱动:新增友链只需在 src/content/friends/ 下放一个 JSON 文件。const friendModules = import.meta.glob("../content/friends/*.json", { eager: true, import: "default",}) as unknown as Record<string, Friend>;const items: Friend[] = Object.values(friendModules);Friend 接口还是 title / imgurl / desc / siteurl / tags,卡片样式和原来的 <Waline /> 都没动。
4.4 导航栏
新页面要能从导航进。把 navBarConfig 挪到 featureConfig 之后定义,入口名统一读 labels、并按各自的 enable 开关条件展开:
export const navBarConfig: NavBarConfig = { links: [ LinkPreset.Home, LinkPreset.Archive, LinkPreset.About, LinkPreset.Friends, { name: featureConfig.labels.navMusic, // "音乐" url: "http://music.alger.fun/#/", external: true, }, ...(featureConfig.works.enable ? [{ name: featureConfig.labels.works, url: "/works/" }] // "作品" : []), ...(featureConfig.apps.enable ? [{ name: featureConfig.labels.apps, url: "/apps/" }] // "应用" : []), ...(featureConfig.hot.enable ? [{ name: featureConfig.labels.hot, url: "/hot/" }] // "热门文章" : []), ],};NOTE三个入口刻意用普通
NavBarLink对象,不去扩LinkPreset枚举。因为Navbar.astro对 number 类型的项会走LinkPresets[item]映射;加枚举还得同步改i18nKey.ts、7 个语言文件和constants/link-presets.ts,用普通对象更直接。名字统一取自
featureConfig.labels,与四个中文 preset(主页 / 归档 / 关于 / 友链)和各自落地页标题保持一致,站点lang: zh_CN下不再出现「英文导航配中文页面」的割裂。用...(enable ? [...] : [])条件展开,是为了让「配置里关掉的功能」连导航入口一起消失;对应地,/works/、/apps/、/hot/三个页面也会显示「该功能已关闭。」而不是一个空列表。navBarConfig放在featureConfig之后定义,是为了避开const的暂时性死区(TDZ)——它引用了后者的labels与enable。上面这段
navBarConfig是整包升级当时的版本,里面还保留着当时的「音乐」外链;该外链已在后续维护中移除,文中其余提到/works/music-panel/的地方也同属当时状态,详见文末「本文的范围」。
4.5 组件插在哪里
ConfigCarrier+GlobalFeatures放在layouts/Layout.astro的<slot />之后——也就是 Swup 容器外面,这样换页时它们不会被替换掉;
<ConfigCarrier /> <slot /> <GlobalFeatures />RecentPosts插进widget/SideBar.astro的#sidebar-sticky:
<RecentPosts class="onload-animation" style="animation-delay: 100ms" />4.6 用一张图收尾
下面这个流程图就是用本次加的 Mermaid 功能渲染的——你在页面上看到的是 SVG 图表,而不是一段代码:
flowchart LR A[修复 CI 质量门禁] --> B[一次性整合依赖升级] B --> C[迁移 Content Layer API] C --> D[全站功能改造] D --> E[暗黑模式可读性修复] E --> F[本地验证 + CI + Vercel 发布]CAUTION第一次跑
pnpm astro check时报了 2 个错:RecentPosts缺styleprop、works详情页的<Markdown>缺必填class。补齐后 68 files 0 errors / 0 warnings / 0 hints。新组件别省 Props 默认值,类型检查比浏览器更早发现问题。
五、暗黑模式可读性修复(commit 00bebbb + 77878ce)
功能上线后自己切到夜间模式检查,发现新加的几个控件「看不清」。这部分单独拿出来说,因为根因挺有意思。
5.1 根因:模板没有基础文字色
src/styles/main.css 里只有一组文字工具类带 dark: 变体:
.text-90 { @apply text-black/90 dark:text-white/90 }.text-75 { @apply text-black/75 dark:text-white/75 }.text-50 { @apply text-black/50 dark:text-white/50 }/* ... */而卡片本身只管背景、不管文字:
.card-base { @apply rounded-[var(--radius-large)] overflow-hidden bg-[var(--card-bg)] transition;}全仓库没有任何 body / html 的基础文字色。所以凡是没包 text-* 类的文字,暗黑模式下就继承浏览器默认的纯黑 rgb(0, 0, 0),压在 --card-bg: oklch(.23 .015 250) 这种深色卡片上自然看不清。反而是激活态的排序按钮因为写了 .hot-sort.is-on { color: white } 一直正常——「有显式色就没事,没显式色就出事」,正好印证这个判断。
5.2 修法一:悬浮面板与排序按钮
在 src/components/GlobalFeatures.astro 的 <style is:global> 里给两个容器补明暗两套显式色:
#feature-controls-panel { display: flex; /* 模板未设基础文字色,默认继承纯黑;暗黑模式必须显式给出可读文字色 */ color: rgba(0, 0, 0, 0.75);}.dark #feature-controls-panel { color: rgba(255, 255, 255, 0.75);}
/* 排序按钮同时出现在悬浮面板与 /hot/ 页面,需独立给出明暗两套文字色 */.hot-sort { color: rgba(0, 0, 0, 0.75);}.dark .hot-sort { color: rgba(255, 255, 255, 0.75);}
/* 激活态白字优先级需同时压过明暗两套基础色 */.hot-sort.is-on,.dark .hot-sort.is-on { color: #ffffff;}.hot-sort 同时被悬浮面板里的 3 个按钮和 /hot/ 页面里的 3 个按钮用到,所以写成全局规则一次覆盖。
5.3 修法二:Waline 评论区
全站复核时,/friends/ 还剩 2 个黑字:「评论」计数和排序标题,来自模板自带的 Waline 组件。
NOTE这不是本次改造引入的回归。
<Waline />早在基线097787d的src/pages/friends.astro里就存在,我这次只改了友链数据来源,没碰第 73 行的评论组件。
Waline 是 @waline/client@v3,src/components/Waline/Waline.astro 里配了 dark: "html.dark",样式从 https://unpkg.com/@waline/client@v3/dist/waline.css 加载。实测发现:
- 覆盖
--waline-color/--waline-text-color这些变量无效; - 只有给
#waline容器设基础color才生效; wl-count/wl-meta-head在亮色和暗色下都算成rgb(0, 0, 0)。
所以加一条最小 scoped 规则:
/* * Waline 评论组件(模板自带的 @waline/client@v3)在暗黑模式下未给容器设置 * 基础文字色,评论计数、排序标题等标签会继承纯黑,压在深色背景上看不清。 * 用 #waline 容器 ID 提升特异度,只作用于评论区域,不影响站点其它文字; * 自带显式颜色的控件(如绿底主按钮)仍保留原有配色。 */.dark #waline { color: rgba(255, 255, 255, 0.75);}为什么不硬编码.wl-count之类借
#waline这个容器 ID 提特异度,属于 Waline 公开的挂载点,升级客户端版本不会静默失效;而.wl-count/.wl-meta-head是内部类名,改版就可能变。另外这条规则只在.dark下生效,亮色模式完全不受影响。
「提交」按钮刻意保留原样:它是绿底 #27ae60 + 黑字,对比度 7.31:1;改成白字会掉到约 2.6:1,属于负优化。而且它由 Waline 自带的 .wl-btn.primary 显式 color 控制,本来也不会被容器 color 覆盖。
关于验证方法这次没有靠截图判断(有些环境看不了图),而是在真实页面里用
getComputedStyle逐元素测颜色:先看--card-bg是否加载、.text-90探针是否为rgba(255,255,255,0.9),确认样式表真的生效了,再用 TreeWalker 扫全页可见文字节点、把 rgb 三通道都小于 60 的记为「黑字」。Waline 那条规则做了 A/B 可逆验证:基线 2 个黑字 → 加规则 1 个 → 移除探针回到 2 个,证明不是测量伪影。顺便排除了几个疑点:applyTheme争抢darkclass、Mermaid 重绘竞态、随机背景发灰,都不是原因。
CAUTION别用隐藏 iframe +
document.write去测样式。 那种方式里样式表根本没加载,会返回--card-bg: ""、满屏假黑字,看着像一堆 bug,其实全是测量伪影。要测就用真实 tab 导航到真实 URL,或者用同源 iframe 的iframe.src = origin + path并等load完成后再测。
六、验证与发布
6.1 本地验证
每次提交前都跑完整套:
| 检查 | 结果 |
|---|---|
pnpm exec biome ci ./src | 78 files, No fixes applied, EXIT=0 |
pnpm astro check | Result (68 files): 0 errors / 0 warnings / 0 hints |
pnpm build | 56 pages built,pagefind v1.6.0 Indexed 25 pages / 22499 words |
| URL diff | 51/51 既有路由全部保留,LOST_COUNT=0 |
node --check | site-features.js 语法通过 |
新增的 5 条路由:/apps/、/hot/、/works/、/works/fuwari-blog/、/works/music-panel/。
NOTE构建产物里 CSS 会被压缩,例如
rgba(255, 255, 255, 0.75)会变成#ffffffbf。所以核对某条规则是否上线时,别直接搜原始字符串;用压缩后的等价写法(或按选择器前缀)去搜。本次产物/_astro/_page_.c7zMAKFv.css里能找到.dark #waline{color:#ffffffbf}。
6.2 CI / Vercel / 生产站
git push origin main 会自动触发 Vercel 重部署。四个提交最终状态:
- GitHub check-runs:5/5 success(
Astro Check for Node.js 22/23、Astro Build for Node.js 22/23、Code quality); - Vercel:
state=success; - 生产站 https://p1ume.vercel.app/ 上
/、/works/、/apps/、/hot/等全部 HTTP 200; - 6 个新页面暗黑模式逐页复核:
/apps/、/friends/、/works/、/works/fuwari-blog/、/works/music-panel/、/hot/全部cssOk: true、黑字数 0(/friends/仅剩刻意保留的绿色「提交」按钮),Mermaid 图表在/works/fuwari-blog/已渲染成 SVG。
NOTE推送时如果碰到
fatal: could not read Username for 'https://github.com'(某些环境重启后 git 凭证脚本会失效),可以用一次性的内联 Basic 头推送,不要往全局 config 里写死凭证:Terminal window TOK=$(tr -d '\r\n' < /c/Users/p1ume_/AppData/Local/Temp/ghtok.txt)AUTH=$(printf 'x-access-token:%s' "$TOK" | base64 | tr -d '\r\n')GIT_TERMINAL_PROMPT=0 git -c credential.helper= -c "http.extraHeader=Authorization: Basic $AUTH" push origin main
小结
四个提交已经全部同步到 GitHub 主仓库,CI 与 Vercel 全绿,仓库 main...origin/main 干净:
34bf148 chore: revive maintenance — fix CI gate, upgrade deps, migrate content layerb536f6f feat: blog-wide enhancements inspired by Besty0728/fuwari00bebbb fix: readable feature controls and sort labels in dark mode77878ce fix: readable Waline comment labels in dark mode这次之后,模板自带的深浅色切换、Swup、TOC、置顶、KaTeX、Admonitions、代码块增强、GitHub 卡片、PhotoSwipe、pagefind、系列、Waline、显示设置、字数与阅读时长都还在,另外补上了阅读进度、Mermaid、作品集、应用页、热门排序、最近更新、强制暗黑、随机背景、背景音乐与显示控制面板。
暂时没做的事
Astro 7与@astrojs/svelte 9.x要求engines.node >= 22.23.4,和本站node >= 18的承诺冲突,所以这轮没有升。相关前置条件记在track-b-prerequisites.md里,只留了文档、没有改代码。等哪天决定把最低 Node 版本抬上去,再单独开一轮。
如果你也在维护一个久未更新的 Fuwari 站点,建议就按这个顺序来:先让 CI 变绿 → 再一次性整合依赖 → 再动内容层 → 最后加功能 → 每一步都用构建产物和真实页面验收。跳过前面两步直接堆功能,后面排查会非常痛苦。
本文的范围本文只记录这次「整包升级」的四个提交。此后站点还在持续做风格与文案的统一打磨(导航入口统一成中文、空态与禁用态文案收进
labels、友链申请格式与 JSON 数据驱动对齐、给早期教程补「适用版本说明」等),这些属于日常维护、提交会继续累加,但都不改变本文描述的架构与思路。再往后还做过一轮外观与内容整理:移除了导航栏的「音乐」外链与「音乐面板」这条作品(
/works/music-panel/随之下线,所以上文的 5 条新路由、作品详情示例与暗黑复核列表里出现的这个地址,写的都是当时状态);/works/、/apps/与作品详情页换成了统一的图标卡片 / 胶囊按钮 / 显式明暗色 token 设计——详见src/styles/feature-pages.css与src/components/FeatureCard.astro。要看当前状态,以 GitHub 主仓库和线上站点为准。