From ef72d52de41efcca0f7d3e43b3ecc589c413dcc2 Mon Sep 17 00:00:00 2001 From: dsh Date: Thu, 17 Sep 2026 18:37:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=87=E9=A2=98=E5=90=8E=E7=9A=84=20#=20?= =?UTF-8?q?=E9=94=9A=E7=82=B9=EF=BC=9A=E8=A1=A5=E4=B8=8A=E7=BC=BA=E5=A4=B1?= =?UTF-8?q?=E7=9A=84=E6=A0=B7=E5=BC=8F=EF=BC=8C=E5=B9=B6=E4=BF=AE=E8=B7=B3?= =?UTF-8?q?=E8=BD=AC=E5=AE=9A=E4=BD=8D=E4=B8=8E=E6=97=A0=E9=9A=9C=E7=A2=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题 - 插件 src/plugins/satteri/autolink-headings.mjs 会在每个标题末尾追加 #,但项目里从来 没给 .anchor / .anchor-icon 写过任何样式(源码和产物 CSS 都查过,零条规则) - 结果是那个 # 以纯文本形式常驻显示在每一个标题后面,看起来像 Markdown 语法错误 - 另外标题有 id 却没有 scroll-margin-top,点 # 跳过去标题顶部紧贴视口上沿, 正好藏在 69px 高的 sticky 导航栏下面 改法 - global.css 补样式:默认 opacity:0 隐藏,鼠标悬停标题(或键盘聚焦锚点)才浮现, 悬停锚点本身转朱红;触屏没有 hover,直接 display:none(跨标题跳转用目录即可) - 补 :is(h1..h6)[id] { scroll-margin-top: calc(var(--header-h,69px) + 1.25rem) } - 插件里给锚点补无障碍名称:aria-label="链接到「标题文字」",图标加 aria-hidden。 否则读屏会连着念二十几个「#,链接」,完全不知道跳到哪 踩到的坑(已写进操作指南) Astro 的文章缓存只在 markdown 内容变化时失效,改插件代码不会重新渲染, 必须先 rm -rf node_modules/.astro .astro。第一次改完插件产物没变就是这个原因。 验证:清缓存重建后,产物 HTML 里锚点带上了 aria-label 与 aria-hidden; 产物 CSS 里 .anchor 的 5 条规则 + scroll-margin-top 均正确, @media(hover:none) 的作用域也确认无误 --- docs/操作指南.md | 13 +++++++ src/data/reading.json | 2 +- src/plugins/satteri/autolink-headings.mjs | 26 ++++++++++++- src/styles/global.css | 47 +++++++++++++++++++++++ 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/docs/操作指南.md b/docs/操作指南.md index fe4b0a6..3d50be1 100644 --- a/docs/操作指南.md +++ b/docs/操作指南.md @@ -299,6 +299,19 @@ sudo sysctl fs.inotify.max_user_watches=524288 如果连空态提示都看不到,检查 `src/data/reading.json` 是否存在(这个文件要提交进 git)。 +**改了 markdown 插件却不生效** + +Astro 会把渲染好的文章缓存到 `node_modules/.astro`,而这个缓存**只按 markdown +文件内容算 key,不看插件代码**。所以改完 `src/plugins/satteri/*.mjs` 之后, +已经渲染过的文章不会重新跑插件 —— 你会以为改动没生效,其实是吃了缓存。 + +```bash +rm -rf node_modules/.astro .astro && pnpm build +``` + +(这个坑是给标题锚点补 `aria-label` 时踩到的:插件明明改了, +产物里的 HTML 却还是老样子。) + **`pnpm dev` 启动时满屏刷红** 历史上踩过两个独立的原因,都已修好,这里记下来免得再犯: diff --git a/src/data/reading.json b/src/data/reading.json index 5bd62be..6f2d0ee 100644 --- a/src/data/reading.json +++ b/src/data/reading.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-09-17T08:50:20.126Z", + "updatedAt": "2026-09-17T10:36:53.871Z", "source": "weread", "books": [ { diff --git a/src/plugins/satteri/autolink-headings.mjs b/src/plugins/satteri/autolink-headings.mjs index 16822f9..ee2b0f2 100644 --- a/src/plugins/satteri/autolink-headings.mjs +++ b/src/plugins/satteri/autolink-headings.mjs @@ -32,6 +32,18 @@ export const autolinkHeadings = defineHastPlugin({ if (typeof id !== "string" || id.length === 0) return if (hasAnchor(node)) return + // 取标题自身的文字,用作锚点的无障碍名称(递归收集,标题里可能有 code/strong) + const collectText = (children) => + (children || []) + .map((child) => { + if (child.type === "text") return child.value + if (Array.isArray(child.children)) return collectText(child.children) + return "" + }) + .join("") + .trim() + const headingText = collectText(node.children) + // 脚注区那个视觉隐藏的 "脚注" 标题不需要 # 锚点 let parent = ctx.parent(node) while (parent) { @@ -44,12 +56,22 @@ export const autolinkHeadings = defineHastPlugin({ ctx.appendChild(node, { type: "element", tagName: "a", - properties: { className: ["anchor"], href: `#${id}` }, + properties: { + className: ["anchor"], + href: `#${id}`, + // 锚点内容只有一个 #,不补名称的话读屏会念「#,链接」, + // 连着二十几个标题听下来完全不知道跳到哪 + ...(headingText ? { "aria-label": `链接到「${headingText}」` } : {}), + }, children: [ { type: "element", tagName: "span", - properties: { className: ["anchor-icon"], "data-pagefind-ignore": "" }, + properties: { + className: ["anchor-icon"], + "data-pagefind-ignore": "", + "aria-hidden": "true", + }, children: [{ type: "text", value: "#" }], }, ], diff --git a/src/styles/global.css b/src/styles/global.css index fceb9e0..7b2834c 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -805,6 +805,53 @@ a.card-github:hover .github-logo { } } +/* ——— 文章标题末尾的 # 锚点 ——— + * + * 由 src/plugins/satteri/autolink-headings.mjs 生成: + *

标题#

+ * + * 插件一直在追加这个锚点,但项目里从来没给它写过样式,所以那个 # 是 + * 以纯文本形式常驻显示在每一个标题后面的。这里按通行做法改成「悬停才浮现」。 + */ + +.anchor { + margin-left: 0.35em; + color: var(--muted-foreground); + text-decoration: none; + opacity: 0; + transition: opacity 200ms ease, color 200ms ease; +} + +.anchor-icon { + font-weight: 400; +} + +/* 鼠标悬停标题、或键盘 Tab 聚焦到锚点时浮现 */ +:is(h1, h2, h3, h4, h5, h6):hover > .anchor, +:is(h1, h2, h3, h4, h5, h6):focus-within > .anchor, +.anchor:focus-visible { + opacity: 1; +} + +.anchor:hover { + color: var(--primary); +} + +/* 触屏没有 hover,常驻显示就是原来那副样子,所以也藏起来。 + 手机上跨标题跳转用右侧目录就够。 */ +@media (hover: none) { + .anchor { + display: none; + } +} + +/* 点 # 跳转时标题别被 sticky 导航栏盖住。 + 原来完全没有这个偏移,跳过去标题顶部紧贴视口上沿, + 正好藏在 69px 高的导航栏下面。 */ +:is(h1, h2, h3, h4, h5, h6)[id] { + scroll-margin-top: calc(var(--header-h, 69px) + 1.25rem); +} + /* ——— 首屏彩蛋:那句话出自王小波《万寿寺》 ——— * * 那句话本身不做任何变化(不加下划线、不变色、不可点),保持纯排版。