标题后的 # 锚点:补上缺失的样式,并修跳转定位与无障碍
问题
- 插件 src/plugins/satteri/autolink-headings.mjs 会在每个标题末尾追加
<a class="anchor"><span class="anchor-icon">#</span></a>,但项目里从来
没给 .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) 的作用域也确认无误
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"updatedAt": "2026-09-17T08:50:20.126Z",
|
||||
"updatedAt": "2026-09-17T10:36:53.871Z",
|
||||
"source": "weread",
|
||||
"books": [
|
||||
{
|
||||
|
||||
@@ -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: "#" }],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -805,6 +805,53 @@ a.card-github:hover .github-logo {
|
||||
}
|
||||
}
|
||||
|
||||
/* ——— 文章标题末尾的 # 锚点 ———
|
||||
*
|
||||
* 由 src/plugins/satteri/autolink-headings.mjs 生成:
|
||||
* <h2 id="slug">标题<a class="anchor" href="#slug"><span class="anchor-icon">#</span></a></h2>
|
||||
*
|
||||
* 插件一直在追加这个锚点,但项目里从来没给它写过样式,所以那个 # 是
|
||||
* 以纯文本形式常驻显示在每一个标题后面的。这里按通行做法改成「悬停才浮现」。
|
||||
*/
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* ——— 首屏彩蛋:那句话出自王小波《万寿寺》 ———
|
||||
*
|
||||
* 那句话本身不做任何变化(不加下划线、不变色、不可点),保持纯排版。
|
||||
|
||||
Reference in New Issue
Block a user