想把文章的标签放到网页右侧,做法是仿照 PaperMod 已有的左侧目录(TOC)侧边栏机制,在后面又加上了分类和系列,做成三个独立卡片,随页面滚动固定不动。本文记录实现过程和踩的坑。
- 文章的标签、分类、系列显示在页面右侧;
- 宽屏时浮出到正文右侧并固定,窄屏(手机)时退回文档流,以卡片的形式跟在正文之后;
- 三个模块独立成卡片,随页面滚动时在屏幕中的位置不变。
思路:复用 TOC 的侧边栏机制#
PaperMod 的目录(toc.html)在宽屏下是这么定位的:.toc-container.wide 用 position: absolute 挂在文章元素上,偏移到文章左侧外面,内部再用 position: sticky 让目录随滚动固定。右侧侧边栏完全照抄这套,只是方向相反:
1
2
3
4
5
6
7
| .tags-aside.wide {
position: absolute;
width: var(--toc-width); /* 300px */
right: calc((var(--toc-width) + var(--gap)) * -1); /* 平移到文章右侧外面 */
top: calc(var(--gap) * 2);
height: 100%; /* 高度覆盖整个文章,sticky 才能全程生效 */
}
|
(窄屏下不挂 wide,这条规则不生效,侧边栏就退回文档流。)
关键点:aside 的高度必须是 100%(等于文章高度),内部的 sticky 才有足够的"可移动空间",否则滚动到一半卡片就跟着跑了。
模板改动(layouts/single.html)#
1. 顶部定义 taxonomy 变量#
1
2
3
| {{- $tags := .Language.Params.Taxonomies.tag | default "tags" }}
{{- $categories := .Language.Params.Taxonomies.category | default "categories" }}
{{- $series := .Language.Params.Taxonomies.series | default "series" }}
|
2. 目录之后插入侧边栏#
放在 partial "toc.html" 后面、正文之前。三个卡片用 tags-aside-sticky 包起来整体 sticky,哪个有内容就显示哪个:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| {{- if or .Params.tags .Params.categories .Params.series }}
<aside id="side-aside" class="tags-aside">
<div class="tags-aside-sticky">
{{- if .Params.categories }}
<div class="tags-aside-box">
<div class="tags-aside-title">分类</div>
<ul class="post-tags">
{{- range ($.GetTerms $categories) }}
<li><a href="{{ .Permalink }}">{{ .LinkTitle }}</a></li>
{{- end }}
</ul>
</div>
{{- end }}
{{- if .Params.tags }}
<div class="tags-aside-box">
<div class="tags-aside-title">标签</div>
<ul class="post-tags">
{{- range ($.GetTerms $tags) }}
<li><a href="{{ .Permalink }}">{{ .LinkTitle }}</a></li>
{{- end }}
</ul>
</div>
{{- end }}
{{- if .Params.series }}
<div class="tags-aside-box">
<div class="tags-aside-title">系列</div>
<ul class="post-tags">
{{- range ($.GetTerms $series) }}
<li><a href="{{ .Permalink }}">{{ .LinkTitle }}</a></li>
{{- end }}
</ul>
</div>
{{- end }}
</div>
</aside>
{{- end }}
|
3. 底部脚本:和左侧目录共用一份宽屏判断#
判断条件是「文档宽 − 文章宽 − 2 × 侧边栏宽 − 4 × 间距 > 0」。这份判定左侧目录同样要用,而且两侧必须同时进入或退出 wide,否则会出现左边目录浮出、右边标签栏还留在文档流里的不对称。所以判定只留一份,两个 aside 在同一次遍历里一起切:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| <script>
(function () {
// 左侧目录(#toc-container)与右侧标签栏(#side-aside)必须同时进入 / 退出 wide,
// 否则两侧会不对称。所以宽度判定只保留这一份,两个元素在同一次遍历里一起切。
// 元素在 DOMContentLoaded 时才查,脚本放哪儿都不影响。
const style = getComputedStyle(document.body);
const main = parseInt(style.getPropertyValue('--article-width'), 10);
const toc = parseInt(style.getPropertyValue('--toc-width'), 10);
const gap = parseInt(style.getPropertyValue('--gap'), 10);
function updateAsideWide() {
const wide = (document.body.scrollWidth - main - (toc * 2) - (gap * 4)) > 0;
['toc-container', 'side-aside'].forEach((id) => {
const aside = document.getElementById(id);
if (aside) aside.classList.toggle('wide', wide);
});
}
document.addEventListener('DOMContentLoaded', updateAsideWide);
window.addEventListener('resize', updateAsideWide);
})();
</script>
|
--article-width、--toc-width、--gap 是 CSS 变量,站点里自定义过(见下),脚本从 getComputedStyle(document.body) 读出来计算,不用写死数字。
这段脚本早期还负责隐藏 footer 里的标签胶囊(document.querySelector('.post-footer .post-tags'))。后来 footer 改成只放上下篇导航和分享按钮,那几行每次都取到 null,成了死代码,已经删掉;现在分类和标签只出现在右侧卡片里。
样式(assets/css/extended/toc.css)#
侧边栏样式追加在目录样式后面:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| /* ===== 文章标签侧边栏(右侧,与目录对称) =====
窄屏(无 .wide):以文档流展示在正文之后;
宽屏(.wide):绝对定位到正文右侧。 */
.tags-aside {
display: block;
width: 100%;
margin-top: calc(var(--gap) * 1.5);
}
.tags-aside.wide {
position: absolute;
width: var(--toc-width);
right: calc((var(--toc-width) + var(--gap)) * -1);
top: calc(var(--gap) * 2);
height: 100%;
margin-top: 0;
border-left: 1px solid var(--border);
padding-left: calc(var(--gap) * 0.5);
}
.tags-aside-sticky {
position: sticky;
/* 吸顶导航栏固定后,标签栏停留位置下移到导航栏之下 */
top: calc(var(--header-height) + var(--gap));
}
.tags-aside-box {
border: 1px solid var(--border);
background: var(--entry);
border-radius: var(--radius);
padding: 0.8em 1em 1em;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.10);
}
/* 暗色模式下阴影不明显,适当加深 */
[data-theme="dark"] .tags-aside-box {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
}
.tags-aside-title {
font-weight: 600;
font-size: 0.95rem;
color: var(--secondary);
margin-bottom: 0.7em;
}
.tags-aside-box + .tags-aside-box {
margin-top: var(--gap);
}
|
(卡片里胶囊的间距与悬停配色在 series.css 的 .tags-aside .post-tags 里。)
站点根部的 CSS 变量(toc.css 开头就有,这里一并记录):
1
2
3
4
5
| :root {
--nav-width: 1380px;
--article-width: 650px;
--toc-width: 300px;
}
|
宽屏阈值 文章宽 + 2 × 侧边栏宽 + 4 × 间距 = 650 + 600 + 96 = 1346px 左右,文档宽度大于这个值才会让左右两条侧边栏一起浮出。
过程中的几个决定#
- 卡片顺序:分类在最上面、标签居中、系列在最下面(系列卡片内部是带 1/N 进度和「当前」标记的导航列表,样式在
series.css); - 卡片底色:尝试过
--code-bg(跟代码块太像)、淡蓝灰、米黄、带绿的浅色,最后觉得都不如默认的白色配边框和阴影干净,改回了 --entry; - 整体 sticky:一开始把
position: sticky 写在每张卡片上,结果是三张卡片轮流吸附、各自固定,后来包了一层 tags-aside-sticky 容器,三张卡片作为一个整体固定; - 只在有对应 frontmatter 时渲染:
if .Params.categories 这样的判断,没有分类的文章不会出现空卡片。
PaperMod 的左右侧边栏本质上都是 position: absolute 定位 + 内部 sticky 固定这套组合,摸清 TOC 的实现后右侧照抄就行。两个容易漏的点:aside 高度要 100%,以及 sticky 容器要包住所有卡片。宽屏/窄屏的切换靠一段几行的 JS 判断,而且左右两条侧边栏共用同一份判定,必须同时进出 wide,否则两侧会不对称。