I. 主题文件
II. 主题安装
2.1 首次安装(2选1)
参考👉 Installation · hugo-PaperMod Wiki
1git submodule add --depth=1 https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
2.2 更换旧主题(2选1)
2.2.1 删除旧主题子模块
先删除当前的主题子模块引用。假设当前主题是 old-theme
,你可以执行以下命令:
1git submodule deinit -f themes/old-theme
2rm -rf .git/modules/themes/old-theme
3git rm -f themes/old-theme
4git commit -m "Remove old theme"
上述命令将会移除 Git 子模块引用,并且在版本控制中提交该变动。
2.2.2 添加新主题子模块
接下来,你可以添加新的主题子模块。如 PaperMod
主题,可以执行:
1git submodule add --depth=1 https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
2git submodule update --init --recursive
2.2.3 更新网站配置文件
在你的 config.toml 或 config.yaml 中修改 theme 配置项为新的主题名称。例如:
1theme = "PaperMod"
2.3 主题更新
在 Hugo 站点根目录下运行:
1git submodule update --remote --merge
📢 注意:
如果是从其他地方 copy | clone 过来的站点,有可能遇到(参考👉 从零开始搭建个人博客网站系列 - 知乎 ):
1WARN found no layout file for "html" for kind "home": You should create a template file which matches Hugo Layouts Lookup Rules for this combination.
这个时候需要重新 clone 一下主题(参考👉 Installation · adityatelange/hugo-PaperMod Wiki ):
1git submodule update --init --recursive
2.4 查看版本
1cd themes/PaperMod/
2git describe --tags
III. 主题配置
3.1 站点目录
1.(site root)
2├── assets
3│ └── css
4│ └── extended
5│ └── blank.css
6├── content
7│ ├── posts
8│ │ ├── _index.md
9│ │ ├── emoji-support.md
10│ │ ├── markdown-syntax.md
11│ │ └── math-typesetting.md
12│ ├── archives.md
13│ └── search.md
14├── i18n
15│ └── en.yaml
16├── layouts
17│ ├── _default
18│ │ ├── _markup
19│ │ | └── render-link.html
20│ │ ├── archives.html
21│ │ └── terms.html
22│ └── partials
23│ └── toc.html
24├── static
25│ ├── android-chrome-192x192.png
26│ ├── android-chrome-512x512.png
27│ ├── apple-touch-icon.png
28│ ├── favicon-16x16.png
29│ ├── favicon-32x32.png
30│ ├── favicon.ico
31│ └── papermod-cover.png
32├── themes
33| └── PaperMod
34├── hugo.toml.bak
35└── config.yml
3.2 站点配置
3.2.1 备份旧配置
1hugo.toml.bak
3.2.2 配置新站点
官方示例
配置 config.yml
,内容如下:
1baseURL: "https://examplesite.com/"
2title: ExampleSite
3paginate: 5
4theme: PaperMod
5
6enableRobotsTXT: true
7buildDrafts: false
8buildFuture: false
9buildExpired: false
10
11minify:
12 disableXML: true
13 minifyOutput: true
14
15params:
16 env: production # to enable google analytics, opengraph, twitter-cards and schema.
17 title: ExampleSite
18 description: "ExampleSite description"
19 keywords: [Blog, Portfolio, PaperMod]
20 author: Me
21 # author: ["Me", "You"] # multiple authors
22 images: ["<link or path of image for opengraph, twitter-cards>"]
23 DateFormat: "January 2, 2006"
24 defaultTheme: auto # dark, light
25 disableThemeToggle: false
26
27 ShowReadingTime: true
28 ShowShareButtons: true
29 ShowPostNavLinks: true
30 ShowBreadCrumbs: true
31 ShowCodeCopyButtons: false
32 ShowWordCount: true
33 ShowRssButtonInSectionTermList: true
34 UseHugoToc: true
35 disableSpecial1stPost: false
36 disableScrollToTop: false
37 comments: false
38 hidemeta: false
39 hideSummary: false
40 showtoc: false
41 tocopen: false
42
43 assets:
44 # disableHLJS: true # to disable highlight.js
45 # disableFingerprinting: true
46 favicon: "<link / abs url>"
47 favicon16x16: "<link / abs url>"
48 favicon32x32: "<link / abs url>"
49 apple_touch_icon: "<link / abs url>"
50 safari_pinned_tab: "<link / abs url>"
51
52 label:
53 text: "Home"
54 icon: /apple-touch-icon.png
55 iconHeight: 35
56
57 # profile-mode
58 profileMode:
59 enabled: false # needs to be explicitly set
60 title: ExampleSite
61 subtitle: "This is subtitle"
62 imageUrl: "<img location>"
63 imageWidth: 120
64 imageHeight: 120
65 imageTitle: my image
66 buttons:
67 - name: Posts
68 url: posts
69 - name: Tags
70 url: tags
71
72 # home-info mode
73 homeInfoParams:
74 Title: "Hi there \U0001F44B"
75 Content: Welcome to my blog
76
77 socialIcons:
78 - name: x
79 url: "https://x.com/"
80 - name: stackoverflow
81 url: "https://stackoverflow.com"
82 - name: github
83 url: "https://github.com/"
84
85 analytics:
86 google:
87 SiteVerificationTag: "XYZabc"
88 bing:
89 SiteVerificationTag: "XYZabc"
90 yandex:
91 SiteVerificationTag: "XYZabc"
92
93 cover:
94 hidden: true # hide everywhere but not in structured data
95 hiddenInList: true # hide on list pages and home
96 hiddenInSingle: true # hide on single page
97
98 editPost:
99 URL: "https://github.com/<path_to_repo>/content"
100 Text: "Suggest Changes" # edit text
101 appendFilePath: true # to append file path to Edit link
102
103 # for search
104 # https://fusejs.io/api/options.html
105 fuseOpts:
106 isCaseSensitive: false
107 shouldSort: true
108 location: 0
109 distance: 1000
110 threshold: 0.4
111 minMatchCharLength: 0
112 limit: 10 # refer: https://www.fusejs.io/api/methods.html#search
113 keys: ["title", "permalink", "summary", "content"]
114menu:
115 main:
116 - identifier: categories
117 name: categories
118 url: /categories/
119 weight: 10
120 - identifier: tags
121 name: tags
122 url: /tags/
123 weight: 20
124 - identifier: example
125 name: example.org
126 url: https://example.org
127 weight: 30
128# Read: https://github.com/adityatelange/hugo-PaperMod/wiki/FAQs#using-hugos-syntax-highlighter-chroma
129pygmentsUseClasses: true
130markup:
131 highlight:
132 noClasses: false
133 # anchorLineNos: true
134 # codeFences: true
135 # guessSyntax: true
136 # lineNos: true
137 # style: monokai
3.3 模式选择
参考👉 Features · hugo-PaperMod Wiki
Hugo PaperMod 有:常规模式(Regular Mode
)、首页信息模式(Home-Info Mode
)、个人资料模式(Profile Mode
) 三种模式可供选择。
这里选择“个人资料模式(Profile Mode
)”模式:
1params:
2 profileMode:
3 enabled: true
4 title: "<Title>" # optional default will be site title
5 subtitle: "This is subtitle"
6 imageUrl: "<image link>" # optional
7 imageTitle: "<title of image as alt>" # optional
8 imageWidth: 120 # custom size
9 imageHeight: 120 # custom size
10 buttons:
11 - name: Archive
12 url: "/archive"
13 - name: Github
14 url: "https://github.com/"
15
16 socialIcons: # optional
17 - name: "<platform>"
18 url: "<link>"
19 - name: "<platform 2>"
20 url: "<link2>"
💡 Tips:
请为imageUrl
设定一个值,如:imageUrl: "apple-touch-icon.png"
这里apple-touch-icon.png
须为实际存在的图像文件,文件默认存放在站点../static
目录下。
3.4 自定义导航
3.4.1 增加导航菜单
这里修改站点配置文件,增加 archives
,search
导航菜单,并调整显示顺序。
1menu:
2 main:
3 - identifier: archives
4 name: archives
5 url: archives/
6 weight: 10
7 - identifier: categories
8 name: categories
9 url: categories/
10 weight: 20
11 - identifier: tags
12 name: tags
13 url: tags/
14 weight: 30
15 - identifier: search
16 name: search
17 url: search/
18 weight: 40
3.4.2 创建菜单页面
为新增加的导航菜单创建页面。在站点 ../content/
目录增加两个文件。
1.(site root)
2└── content
3 ├── archives.md
4 └── search.md
archives.md
文件内容如下:
1---
2title: "存档"
3layout: "archives"
4url: "/archives"
5summary: "archives"
6---
search.md
文件内容如下:
1---
2title: "搜索"
3layout: "search"
4placeholder: 输入关键字,然后回车 ...
5---
3.5 中文翻译
💡 Tips:
为了展示中文效果,建议在站点../content/posts/
目录下至少发布三篇文章,每篇文章元数据包括文章的“分类”及“标签”,如下:
1.(site root)
2└── content
3 └── posts
4 ├── emoji-support.md
5 ├── markdown-syntax.md
6 └── math-typesetting.md
3.5.1 导航菜单
直接在站点配置文件 config.yml
中修改,如下:
1menu:
2 main:
3 - identifier: archives
4 name: 归档
5 url: archives/
6 weight: 10
7 - identifier: categories
8 name: 分类
9 url: categories/
10 weight: 20
11 - identifier: tags
12 name: 标签
13 url: tags/
14 weight: 30
15 - identifier: search
16 name: 搜索
17 url: search/
18 weight: 40
3.5.2 页面元素
参考👉 PaperMod 主题配置 - 修改 html 模板
1、首先汉化的是分类、标签页中文显示。创建 ../layouts/_default/terms.html
文件,内容如下:
1{{- define "main" }}
2
3{{- if .Title }}
4<header class="page-header">
5 {{- if eq .Title "Categories" }}
6 <h1>{{ "分类" }}</h1>
7 {{- end }}
8 {{- if eq .Title "Tags" }}
9 <h1>{{ "标签" }}</h1>
10 <!-- <h1>🔖{{ .Title }}</h1> -->
11 {{- end }}
12 <!-- <h1>{{ .Title }}</h1> -->
13 {{- if .Description }}
14 <div class="post-description">
15 {{ .Description }}
16 </div>
17 {{- end }}
18</header>
19{{- end }}
20
21<!-- 原始 -->
22
23<ul class="terms-tags">
24 {{- $type := .Type }}
25 {{- range $key, $value := .Data.Terms.Alphabetical }}
26 {{- $name := .Name }}
27 {{- $count := .Count }}
28 {{- with $.Site.GetPage (printf "/%s/%s" $type $name) }}
29 <li>
30 <a href="{{ .Permalink }}">{{ .Name }} <sup><strong><sup>{{ $count }}</sup></strong></sup> </a>
31 </li>
32 {{- end }}
33 {{- end }}
34</ul>
35
36{{- end }}{{/* end main */ -}}
2、汉化文章页 <<PREV
, NEXT>>
等页面元素,修改站点配置文件 config.yml
,内容如下:
1# 语言设置
2defaultContentLanguage: "zh"
3
4# 单语言,必须在此处,以下设置之前
5languages:
6 zh:
7 # RFC 5646 语言标记, Hugo 使用此值填充内置 RSS 模板中的语言元素和内置别名模板中 html 元素的 lang 属性
8 languageCode: "zh-CN"
9 # 语言名称,通常在渲染语言切换器时使用
10 languageName: "中文"
3.5.3 Posts 页
汉化 exampleSite/posts/
页面 Posts 显示为“文章”。在站点 ../content/posts/
下创建 _index.md
文件,内容如下:
1---
2title: "文章"
3---
3.6 文章目录(大纲)
1、在站点配置文件 config.yml
中启用目录(大纲),如下:
1params:
2 # ...
3 showtoc: true # 显示目录
4 tocopen: true # 自动展开目录
2、PaperMod 主题默认将文章目录放在顶部,不方便阅读时跳转,修改为侧边目录。创建 toc.html
,toc.css
两个文件,目录结构如下:
1.(site root)
2├── assets
3│ └── css
4│ └── extended
5│ └── toc.css
6└── layouts
7 └── partials
8 └── toc.html
toc.html
内容如下:
1{{- $headers := findRE "<h[1-6].*?>(.|\n])+?</h[1-6]>" .Content -}}
2{{- $has_headers := ge (len $headers) 1 -}}
3{{- if $has_headers -}}
4<aside id="toc-container" class="toc-container wide">
5 <div class="toc">
6 <details {{if (.Param "TocOpen") }} open{{ end }}>
7 <summary accesskey="c" title="(Alt + C)">
8 <span class="details">{{- i18n "toc" | default "Table of Contents" }}</span>
9 </summary>
10
11 <div class="inner">
12 {{- $largest := 6 -}}
13 {{- range $headers -}}
14 {{- $headerLevel := index (findRE "[1-6]" . 1) 0 -}}
15 {{- $headerLevel := len (seq $headerLevel) -}}
16 {{- if lt $headerLevel $largest -}}
17 {{- $largest = $headerLevel -}}
18 {{- end -}}
19 {{- end -}}
20
21 {{- $firstHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers 0) 1) 0)) -}}
22
23 {{- $.Scratch.Set "bareul" slice -}}
24 <ul>
25 {{- range seq (sub $firstHeaderLevel $largest) -}}
26 <ul>
27 {{- $.Scratch.Add "bareul" (sub (add $largest .) 1) -}}
28 {{- end -}}
29 {{- range $i, $header := $headers -}}
30 {{- $headerLevel := index (findRE "[1-6]" . 1) 0 -}}
31 {{- $headerLevel := len (seq $headerLevel) -}}
32
33 {{/* get id="xyz" */}}
34 {{- $id := index (findRE "(id=\"(.*?)\")" $header 9) 0 }}
35
36 {{- /* strip id="" to leave xyz, no way to get regex capturing groups in hugo */ -}}
37 {{- $cleanedID := replace (replace $id "id=\"" "") "\"" "" }}
38 {{- $header := replaceRE "<h[1-6].*?>((.|\n])+?)</h[1-6]>" "$1" $header -}}
39
40 {{- if ne $i 0 -}}
41 {{- $prevHeaderLevel := index (findRE "[1-6]" (index $headers (sub $i 1)) 1) 0 -}}
42 {{- $prevHeaderLevel := len (seq $prevHeaderLevel) -}}
43 {{- if gt $headerLevel $prevHeaderLevel -}}
44 {{- range seq $prevHeaderLevel (sub $headerLevel 1) -}}
45 <ul>
46 {{/* the first should not be recorded */}}
47 {{- if ne $prevHeaderLevel . -}}
48 {{- $.Scratch.Add "bareul" . -}}
49 {{- end -}}
50 {{- end -}}
51 {{- else -}}
52 </li>
53 {{- if lt $headerLevel $prevHeaderLevel -}}
54 {{- range seq (sub $prevHeaderLevel 1) -1 $headerLevel -}}
55 {{- if in ($.Scratch.Get "bareul") . -}}
56 </ul>
57 {{/* manually do pop item */}}
58 {{- $tmp := $.Scratch.Get "bareul" -}}
59 {{- $.Scratch.Delete "bareul" -}}
60 {{- $.Scratch.Set "bareul" slice}}
61 {{- range seq (sub (len $tmp) 1) -}}
62 {{- $.Scratch.Add "bareul" (index $tmp (sub . 1)) -}}
63 {{- end -}}
64 {{- else -}}
65 </ul>
66 </li>
67 {{- end -}}
68 {{- end -}}
69 {{- end -}}
70 {{- end }}
71 <li>
72 <a href="#{{- $cleanedID -}}" aria-label="{{- $header | plainify -}}">{{- $header | safeHTML -}}</a>
73 {{- else }}
74 <li>
75 <a href="#{{- $cleanedID -}}" aria-label="{{- $header | plainify -}}">{{- $header | safeHTML -}}</a>
76 {{- end -}}
77 {{- end -}}
78 <!-- {{- $firstHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers 0) 1) 0)) -}} -->
79 {{- $firstHeaderLevel := $largest }}
80 {{- $lastHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers (sub (len $headers) 1)) 1) 0)) }}
81 </li>
82 {{- range seq (sub $lastHeaderLevel $firstHeaderLevel) -}}
83 {{- if in ($.Scratch.Get "bareul") (add . $firstHeaderLevel) }}
84 </ul>
85 {{- else }}
86 </ul>
87 </li>
88 {{- end -}}
89 {{- end }}
90 </ul>
91 </div>
92 </details>
93 </div>
94</aside>
95<script>
96 let activeElement;
97 let elements;
98
99 document.addEventListener('DOMContentLoaded', function (event) {
100 checkTocPosition();
101
102 elements = document.querySelectorAll('h1[id],h2[id],h3[id],h4[id],h5[id],h6[id]');
103 if (elements.length > 0) {
104 // Make the first header active
105 activeElement = elements[0];
106 const id = encodeURI(activeElement.getAttribute('id')).toLowerCase();
107 document.querySelector(`.inner ul li a[href="#${id}"]`).classList.add('active');
108 }
109
110 // Add event listener for the "back to top" link
111 const topLink = document.getElementById('top-link');
112 if (topLink) {
113 topLink.addEventListener('click', (event) => {
114 // Prevent the default action
115 event.preventDefault();
116
117 // Smooth scroll to the top
118 window.scrollTo({ top: 0, behavior: 'smooth' });
119 });
120 }
121 }, false);
122
123 window.addEventListener('resize', function(event) {
124 checkTocPosition();
125 }, false);
126
127 window.addEventListener('scroll', () => {
128 // Get the current scroll position
129 const scrollPosition = window.pageYOffset || document.documentElement.scrollTop;
130
131 // Check if the scroll position is at the top of the page
132 if (scrollPosition === 0) {
133 return;
134 }
135
136 // Ensure elements is a valid NodeList
137 if (elements && elements.length > 0) {
138 // Check if there is an object in the top half of the screen or keep the last item active
139 activeElement = Array.from(elements).find((element) => {
140 if ((getOffsetTop(element) - scrollPosition) > 0 &&
141 (getOffsetTop(element) - scrollPosition) < window.innerHeight / 2) {
142 return element;
143 }
144 }) || activeElement;
145
146 elements.forEach(element => {
147 const id = encodeURI(element.getAttribute('id')).toLowerCase();
148 const tocLink = document.querySelector(`.inner ul li a[href="#${id}"]`);
149 if (element === activeElement){
150 tocLink.classList.add('active');
151
152 // Ensure the active element is in view within the .inner container
153 const tocContainer = document.querySelector('.toc .inner');
154 const linkOffsetTop = tocLink.offsetTop;
155 const containerHeight = tocContainer.clientHeight;
156 const linkHeight = tocLink.clientHeight;
157
158 // Calculate the scroll position to center the active link
159 const scrollPosition = linkOffsetTop - (containerHeight / 2) + (linkHeight / 2);
160 tocContainer.scrollTo({ top: scrollPosition, behavior: 'smooth' });
161 } else {
162 tocLink.classList.remove('active');
163 }
164 });
165 }
166 }, false);
167
168 const main = parseInt(getComputedStyle(document.body).getPropertyValue('--article-width'), 10);
169 const toc = parseInt(getComputedStyle(document.body).getPropertyValue('--toc-width'), 10);
170 const gap = parseInt(getComputedStyle(document.body).getPropertyValue('--gap'), 10);
171
172 function checkTocPosition() {
173 const width = document.body.scrollWidth;
174
175 if (width - main - (toc * 2) - (gap * 4) > 0) {
176 document.getElementById("toc-container").classList.add("wide");
177 } else {
178 document.getElementById("toc-container").classList.remove("wide");
179 }
180 }
181
182 function getOffsetTop(element) {
183 if (!element.getClientRects().length) {
184 return 0;
185 }
186 let rect = element.getBoundingClientRect();
187 let win = element.ownerDocument.defaultView;
188 return rect.top + win.pageYOffset;
189 }
190
191</script>
192{{- end }}
toc.css
内容如下:
1:root {
2 --nav-width: 1380px;
3 --article-width: 650px;
4 --toc-width: 300px;
5}
6
7.toc {
8 margin: 0 2px 40px 2px;
9 border: 1px solid var(--border);
10 background: var(--entry);
11 border-radius: var(--radius);
12 padding: 0.4em;
13}
14
15.toc-container.wide {
16 position: absolute;
17 height: 100%;
18 border-right: 1px solid var(--border);
19 left: calc((var(--toc-width) + var(--gap)) * -1);
20 top: calc(var(--gap) * 2);
21 width: var(--toc-width);
22}
23
24.wide .toc {
25 position: sticky;
26 top: var(--gap);
27 border: unset;
28 background: unset;
29 border-radius: unset;
30 width: 100%;
31 margin: 0 2px 40px 2px;
32}
33
34.toc details summary {
35 cursor: zoom-in;
36 margin-inline-start: 20px;
37 padding: 12px 0;
38}
39
40.toc details[open] summary {
41 font-weight: 500;
42}
43
44.toc-container.wide .toc .inner {
45 margin: 0;
46}
47
48.active {
49 font-size: 110%;
50 font-weight: 600;
51}
52
53.toc ul {
54 list-style-type: circle;
55}
56
57.toc .inner {
58 margin: 0 0 0 20px;
59 padding: 0px 15px 15px 20px;
60 font-size: 16px;
61
62 /*目录显示高度*/
63 max-height: 83vh;
64 overflow-y: auto;
65}
66
67.toc .inner::-webkit-scrollbar-thumb { /*滚动条*/
68 background: var(--border);
69 border: 7px solid var(--theme);
70 border-radius: var(--radius);
71}
72
73.toc li ul {
74 margin-inline-start: calc(var(--gap) * 0.5);
75 list-style-type: none;
76}
77
78.toc li {
79 list-style: none;
80 font-size: 0.95rem;
81 padding-bottom: 5px;
82}
83
84.toc li a:hover {
85 color: var(--secondary);
86}
3.7 新标签打开链接
创建 ../layouts/_default/_markup/render-link.html
文件,内容如下:
1<a href="{{ .Destination | safeURL }}"{{ with .Title}} title="{{ . }}"{{ end }}{{ if strings.HasPrefix .Destination "http" }} target="_blank" rel="noopener"{{ end }}>{{ .Text | safeHTML }}</a>
该方法自动为所有文章内的链接加上了 target="_blank" rel="noopener"
。
但是站内链接也都加上了 target="_blank" rel="noopener"
。
站内链接建议写 path:
1[title](/foo/bar/)
2↓ 会被简单解析为: ↓
3<a href="/foo/bar/">title</a>
3.8 Waline 评论系统
参考👉 在Hugo-PaperMod中加入Waline评论区 | Drifting Boats
3.8.1 引入 Waline 客户端
在站点目录创建评论页面 ../layouts/partials/comments.html
,内容如下:
1{{- /* Comments area start */ -}}
2{{- /* to add comments read => https://gohugo.io/content-management/comments/ */ -}}
3
4<!-- layouts/partials/comments.html -->
5{{- if .Site.Params.comments }}
6 <!-- 评论容器 -->
7 <div class="waline-container" data-path="{{ .Permalink | relURL }}"></div>
8 <link href="https://unpkg.com/@waline/client@v3/dist/waline.css" rel="stylesheet" />
9
10 <!-- 初始化 Waline 的脚本 -->
11 <script>
12 document.addEventListener("DOMContentLoaded", () => {
13
14 // 初始化 Waline
15 const walineInit = () => {
16 import('https://unpkg.com/@waline/client@v3/dist/waline.js').then(({ init }) => {
17 const walineContainers = document.querySelectorAll('.waline-container[data-path]');
18 walineContainers.forEach(container => {
19 if (!container.__waline__) {
20 const path = container.getAttribute('data-path');
21 container.__waline__ = init({
22 el: container,
23 serverURL: '{{ .Site.Params.waline.serverURL }}',
24 lang: '{{ .Site.Params.waline.lang }}',
25 visitor: '{{ .Site.Params.waline.visitor | default "匿名者" }}',
26 emoji: [
27 {{- range .Site.Params.waline.emoji }}
28 '{{ . }}',
29 {{- end }}
30 ],
31 requiredMeta: [
32 {{- range .Site.Params.waline.requiredMeta }}
33 '{{ . }}',
34 {{- end }}
35 ],
36 locale: {
37 admin: '{{ .Site.Params.waline.locale.admin }}',
38 placeholder: '{{ .Site.Params.waline.locale.placeholder }}',
39 },
40 path: path,
41 dark: '{{ .Site.Params.waline.dark | default "body.dark" }}',
42
43 // 个性化显示
44 commentSorting: 'hottest', // 评论列表排序方式
45 wordLimit: 500, // 评论字数限制
46 search: false, // 自定义搜索功能
47 reaction: false, // 文章表情互动功能
48 imageUploader: false, // 自定义图片上传
49 });
50 }
51 });
52 }).catch(error => {
53 console.error("Waline 初始化失败:", error);
54 });
55 };
56
57 walineInit();
58 });
59 </script>
60{{- end }}
61
62{{- /* Comments area end */ -}}
3.8.2 修改站点配置
修改 hugo.yaml
站点配置文件:
- comments 值改为 true
- 增加 Waline 配置
1params:
2 # ......
3 comments: true
4 # ......
5 waline:
6 serverURL: "https://comments.example.com" # 绑定为你自己的 Waline 服务端地址
7 lang: "zh-CN"
8 visitor: "匿名者" # 当不输入昵称时显示的名称
9 requiredMeta: [nick] # 必填项,包括 nick|mail|link
10 emoji:
11 [
12 'https://cdn.jsdelivr.net/gh/walinejs/emojis/weibo',
13 # 'https://cdn.jsdelivr.net/gh/walinejs/emojis/qq'
14 ]
15 locale:
16 {
17 admin: "博主", # 作者标签文案
18 placeholder: "仅填写昵称即可发表回复。\n填写邮箱可收到回复提醒。\n评论区支持 Markdown 语法及预览。\n" # 评论输入框占位文案内容
19 }
20 dark: 'body.dark' # 适配暗黑模式
3.9 版权声明
参考👉 PaperMod 添加文章版权声明 | Tofuwine’s Blog
3.9.1 版权页面
站点目录下创建 ../layouts/partials/copyright.html
文件,内容如下:
1<div class="pe-copyright">
2 <img src="/imgs/cc/cc.svg" width="75" height="75" align="right" />
3 <!-- <hr> -->
4 <blockquote>
5 {{ if .Param "reposted" }}
6 <p><strong>本文为转载内容,原文信息如下:</strong></p>
7 <p><strong>原文标题:</strong>{{- .Param "repostedTitle" -}}</p>
8 <p><strong>原文作者:</strong>{{- .Param "repostedAuthor" -}}</p>
9 <p><strong>原文链接:</strong><a href="{{- .Param "repostedLink" -}}" target="_blank">{{- .Param "repostedLink" -}}</a></p>
10 <p><strong>版权声明:</strong>如有侵权,请<a href="mailto://{{ .Param "contactEmail" }}">联系本站</a>删除。</p>
11 {{ else }}
12 <p><strong>文章标题:</strong>{{ .Title }}</p>
13 <p><strong>本文作者:</strong>{{ .Param "author" }}</p>
14 <p><strong>本文链接:</strong><a href="{{ .Permalink }}" target="_blank">{{ .Permalink }}</a></p>
15 <p><strong>版权声明:</strong>本网站所有文章除特别声明外,均采用 <a href="{{- .Param "licenseLink" -}}" target="_blank">{{- .Param "licenseName" -}}</a> 许可协议。转载请注明出处!</p>
16 {{ end }}
17 </blockquote>
18</div>
📌 说明:
本示例版权水印文件存放路径../static/imgs/cc/cc.svg
3.9.2 添加样式
在站点目录下创建 ../assets/css/extended/copyright.css
文件,内容如下:
1.pe-copyright {
2 margin-top: 50px; /* 上边距 */
3 font-size: 14px;
4 border: 3px solid #4A4A4A;
5}
6
7.pe-copyright hr {
8 border-style: dashed;
9 color: #e26c56;
10}
11
12.pe-copyright blockquote {
13 margin: 10px 0; /* 外边距:上下边距为 10px,左右为 0 */
14 padding: 10px 10px; /* 内边距:上下边距为 20px,左右为 10px */
15}
16
17.pe-copyright a {
18 box-shadow: 0 1px;
19 box-decoration-break: clone;
20 -webkit-box-decoration-break: clone;
21}
3.9.3 加入文章页
将 PaperMod 主题目录下 ../themes/PaperMod/layouts/_default/single.html
文件复制到站点目录 ../layouts/_default/single.html
,编辑 single.html
文件,在 footer
节点上添加如下内容:
1<article class="post-single">
2 {{- if .Content }}
3 <div class="post-content">
4 ...
5 </div>
6 {{- end }}
7
8 <!-- 加入版权声明 -->
9 {{ if .Param "enableCopyright" }}
10 {{ partial "copyright.html" . }}
11 {{ end }}
12
13 <footer class="post-footer">
14 ...
15 </footer>
16</article>
3.9.4 启用版权声明
在站点配置文件 config.yml
中加入以下配置:
1params:
2 # ...
3 enableCopyright: true # 启用版权声明
3.9.4.1 原创文章
原创文章需在文章 frontmatter
中添加以下参数:(以下仅为示例,请根据实际自行修改)
1---
2author: <Author>
3licenseLink: "https://creativecommons.org/licenses/by-nc/4.0/"
4licenseName: "CC BY-NC 4.0"
5---
也可直接在站点配置文件 config.yml
中加入以下配置:
1params:
2 # ...
3 author: <Author>
4 licenseLink: "https://creativecommons.org/licenses/by-nc/4.0/deed.zh-hans"
5 licenseName: "CC BY-NC-SA"
6 contactEmail: <userName@example.com>
其中 author
为 Hugo-PaperMod 已有参数。
3.9.4.2 转载文章
转载文章需在文章 frontmatter
中添加以下参数:
1---
2reposted: true
3repostedTitle: "修改为原文章标题"
4repostedAuthor: "修改为原文章作者名"
5repostedLink: "修改为原文章链接"
6contactEmail: your email
7---
其中 contactEmail
参数可在 hugo 配置中指定全局默认值:
1params:
2 # ...
3 contactEmail: <Your Email>
3.10 代码块优化
3.10.1 代码块高亮
修改站点配置文件 config.yml
,如下:
1params:
2 # ...
3 assets:
4 disableHLJS: true # to disable highlight.js
5
6# Read: https://github.com/adityatelange/hugo-PaperMod/wiki/FAQs#using-hugos-syntax-highlighter-chroma
7pygmentsUseClasses: false
8markup:
9 goldmark:
10 renderer:
11 unsafe: true
12 highlight:
13 noClasses: true
14 anchorLineNos: false
15 codeFences: true
16 guessSyntax: true
17 lineNos: true
18 style: monokai
19 lineNumbersInTable: false
20
21 # 参数说明:https://gohugo.io/functions/transform/highlight/
22 # lineNos:是否在每行开头显示数字。 默认为 false
23 # hl_Lines:以空格分隔的列表,用于强调高亮代码中的行。 要强调第 2、3、4 和 7 行,请将该值设为 2-4 7。 该选项独立于行无起始选项。
24 # lineNoStart=1:第一行开头要显示的数字。 如果 lineNos 为 false 则与此无关。 默认值为 1
25 # anchorLineNos:是否将每个行号渲染为 HTML 锚点元素,并将周围 span 元素的 id 属性设置为行号。 如果 lineNos 为 false,则与此无关。 默认为 false
26 # lineAnchors:在将行号作为 HTML 锚点元素呈现时,将此值预置到周围 span 元素的 id 属性中。 当页面包含两个或多个代码块时,这将提供唯一的 id 属性。 如果 lineNos 或 anchorLineNos 为 false,则与此无关。
27 # hl_inline:是否在没有包装容器的情况下渲染突出显示的代码。默认为 false
28 # codeFences:是否高亮显示有栅栏的代码块。 默认为 true
29 # guessSyntax:如果 LANG 参数为空或设置为没有相应词法的语言,是否自动检测语言。 如果无法自动检测语言,则退回到纯文本词法。 默认为 false
30 # lineNumbersInTable:是否在 HTML 表格的两个单元格中显示高亮显示的代码。 左侧表格单元格包含行号,右侧表格单元格包含代码。 如果 lineNos 为 false 则无关。 默认为 true
31 # noClasses:是否使用内联 CSS 样式而不是外部 CSS 文件。 要使用外部 CSS 文件,请将此值设为 false,并使用 hugo gen chromastyles 命令生成 CSS 文件。 默认值为 true
3.10.2 代码块展开/折叠
1、复制主题目录 ../themes/PaperMod/layouts/partials/footer.html
文件,粘贴到站点根目录 ../layouts/partials/
下。 编辑 footer.html
,内容如下:
1{{- if not (.Param "hideFooter") }}
2<footer class="footer">
3 {{- if not site.Params.footer.hideCopyright }}
4 {{- if site.Copyright }}
5 <span>{{ site.Copyright | markdownify }}</span>
6 {{- else }}
7 <span>© {{ now.Year }} <a href="{{ "" | absLangURL }}">{{ site.Title }}</a></span>
8 {{- end }}
9 {{- print " · "}}
10 {{- end }}
11
12 {{- with site.Params.footer.text }}
13 {{ . | markdownify }}
14 {{- print " · "}}
15 {{- end }}
16
17 <span>
18 Powered by
19 <a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> &
20 <a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener" target="_blank">PaperMod</a>
21 </span>
22</footer>
23{{- end }}
24
25{{- if (not site.Params.disableScrollToTop) }}
26<a href="#top" aria-label="go to top" title="Go to Top (Alt + G)" class="top-link" id="top-link" accesskey="g">
27 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor">
28 <path d="M12 6H0l6-6z" />
29 </svg>
30</a>
31{{- end }}
32
33{{- partial "extend_footer.html" . }}
34
35<script>
36 let menu = document.getElementById('menu')
37 if (menu) {
38 menu.scrollLeft = localStorage.getItem("menu-scroll-position");
39 menu.onscroll = function () {
40 localStorage.setItem("menu-scroll-position", menu.scrollLeft);
41 }
42 }
43
44 document.querySelectorAll('a[href^="#"]').forEach(anchor => {
45 anchor.addEventListener("click", function (e) {
46 e.preventDefault();
47 var id = this.getAttribute("href").substr(1);
48 if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
49 document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView({
50 behavior: "smooth"
51 });
52 } else {
53 document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView();
54 }
55 if (id === "top") {
56 history.replaceState(null, null, " ");
57 } else {
58 history.pushState(null, null, `#${id}`);
59 }
60 });
61 });
62
63</script>
64
65{{- if (not site.Params.disableScrollToTop) }}
66<script>
67 var mybutton = document.getElementById("top-link");
68 window.onscroll = function () {
69 if (document.body.scrollTop > 800 || document.documentElement.scrollTop > 800) {
70 mybutton.style.visibility = "visible";
71 mybutton.style.opacity = "1";
72 } else {
73 mybutton.style.visibility = "hidden";
74 mybutton.style.opacity = "0";
75 }
76 };
77
78</script>
79{{- end }}
80
81{{- if (not site.Params.disableThemeToggle) }}
82<script>
83 document.getElementById("theme-toggle").addEventListener("click", () => {
84 if (document.body.className.includes("dark")) {
85 document.body.classList.remove('dark');
86 localStorage.setItem("pref-theme", 'light');
87 } else {
88 document.body.classList.add('dark');
89 localStorage.setItem("pref-theme", 'dark');
90 }
91 })
92
93</script>
94{{- end }}
95
96{{- if (and (eq .Kind "page") (ne .Layout "archives") (ne .Layout "search") (.Param "ShowCodeCopyButtons")) }}
97<script>
98 document.querySelectorAll('pre > code').forEach((codeblock) => {
99 const container = codeblock.parentNode.parentNode;
100
101 const copybutton = document.createElement('button');
102 copybutton.classList.add('copy-code');
103 copybutton.innerHTML = '{{- i18n "code_copy" | default "copy" }}';
104
105 function copyingDone() {
106 copybutton.innerHTML = '{{- i18n "code_copied" | default "copied!" }}';
107 setTimeout(() => {
108 copybutton.innerHTML = '{{- i18n "code_copy" | default "copy" }}';
109 }, 2000);
110 }
111
112 copybutton.addEventListener('click', (cb) => {
113 if ('clipboard' in navigator) {
114 // 不包含样式的span的内容拼接起来,也是代码块的内容
115 let x = codeblock.getElementsByTagName("span");
116 let noLineNumContent = "";
117 for (i = 0; i < x.length; i++) {
118 if (x[i].style.display || x[i].style.color);
119 else noLineNumContent += x[i].textContent;
120 }
121 navigator.clipboard.writeText(noLineNumContent);
122 copyingDone();
123 return;
124 }
125
126 const range = document.createRange();
127 range.selectNodeContents(codeblock);
128 const selection = window.getSelection();
129 selection.removeAllRanges();
130 selection.addRange(range);
131 try {
132 document.execCommand('copy');
133 copyingDone();
134 } catch (e) { };
135 selection.removeRange(range);
136 });
137
138 if (container.classList.contains("highlight")) {
139 container.appendChild(copybutton);
140 } else if (container.parentNode.firstChild == container) {
141 // td containing LineNos
142 } else if (codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName == "TABLE") {
143 // table containing LineNos and code
144 codeblock.parentNode.parentNode.parentNode.parentNode.parentNode.appendChild(copybutton);
145 } else {
146 // code blocks not having highlight as parent class
147 codeblock.parentNode.appendChild(copybutton);
148 }
149 });
150</script>
151
152<script>
153 document.querySelectorAll('pre > code').forEach((codeblock) => {
154 const container = codeblock.parentNode.parentNode;
155
156 const unfoldbtn = document.createElement('button');
157 unfoldbtn.classList.add('unfoldbtn');
158 unfoldbtn.innerHTML = '展开';
159
160 unfoldbtn.addEventListener('click', (cb) => {
161 if (container.firstChild.firstChild.classList.contains('unfold')) {
162 container.firstChild.firstChild.classList.remove('unfold');
163 unfoldbtn.innerHTML = '展开';
164 } else {
165 container.firstChild.firstChild.classList.add('unfold');
166 unfoldbtn.innerHTML = '折叠';
167 }
168 });
169
170 if (container.classList.contains("highlight")) {
171 container.appendChild(unfoldbtn);
172 }
173 });
174</script>
175
176{{- end }}
2、复制主题目录 ../themes/PaperMod/assets/css/extended/blank.css
文件,粘贴到站点根目录 ../assets/css/extended/
下。 编辑 blank.css
,内容如下:
1/*
2This is just a placeholder blank stylesheet so as to support adding custom styles budled with theme's default styles
3
4Read https://github.com/adityatelange/hugo-PaperMod/wiki/FAQs#bundling-custom-css-with-themes-assets for more info
5*/
6
7.highlight:hover {
8 .unfoldbtn {
9 display: block;
10 }
11}
12
13.unfoldbtn {
14 display: none;
15 position: absolute;
16 top: 4px;
17 right: 18px;
18 color: rgba(255, 255, 255, .8);
19 background: rgba(78, 78, 78, .8);
20 border-radius: var(--radius);
21 padding: 0 5px;
22 font-size: 14px;
23 user-select: none;
24}
25
26code {
27 max-height: 13rem;
28}
29
30.unfold {
31 max-height: none;
32}
33
34.copy-code {
35 right: 58px;
36}
3.11 查看原图
参考👉 Hugo 主题配置 | 夜云泊
1、修改站点配置文件 config.yml
,如下:
1params:
2 # ...
3 # User-defined parameters
4 fancybox: true # 启用图片放大功能
2、创建 ../layouts/_default/_markup/render-image.html
文件,内容如下:
1 {{if .Page.Site.Params.fancybox }}
2 <div class="post-img-view">
3 <a data-fancybox="gallery" href="{{ .Destination | safeURL }}">
4 <img src="{{ .Destination | safeURL }}" alt="{{ .Text }}" {{ with .Title}} title="{{ . }}"{{ end }} />
5 </a>
6 </div>
7 {{ end }}
3、编辑 ../layouts/partials/footer.html
文件,加入以下内容:
1{{if .Page.Site.Params.fancybox }}
2<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script>
3<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.css" />
4<script src="https://cdn.jsdelivr.net/gh/fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.js"></script>
5{{ end }}
3.12 数学公式
参考👉 在Hugo PaperMod主题中加入数学支持的最简方式 - 微控圈(MCU Loop)
1、在站点目录下创建 ../layouts/partials/math.html
文件,内容如下:
1<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.2/dist/katex.min.css" integrity="sha384-bYdxxUwYipFNohQlHt0bjN/LCpueqWz13HufFEV1SUatKs1cm4L6fFgCi1jT643X" crossorigin="anonymous">
2<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.2/dist/katex.min.js" integrity="sha384-Qsn9KnoKISj6dI8g7p1HBlNpVx0I8p1SvlwOldgi3IorMle61nQy4zEahWYtljaz" crossorigin="anonymous"></script>
3<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.2/dist/contrib/auto-render.min.js" integrity="sha384-+VBxd3r6XgURycqtZ117nYw44OOcIax56Z4dCRWbxyPt0Koah1uHoK0o4+/RRE05" crossorigin="anonymous"></script>
4<script>
5 document.addEventListener("DOMContentLoaded", function() {
6 renderMathInElement(document.body, {
7 // customised options
8 // • auto-render specific keys, e.g.:
9 delimiters: [
10 {left: '$$', right: '$$', display: true},
11 {left: '$', right: '$', display: false}
12 ],
13 // • rendering keys, e.g.:
14 throwOnError : false
15 });
16 });
17</script>
2、复制主题目录 ../themes/PaperMod/layouts/partials/extend_head.html
文件,粘贴到站点根目录 ../layouts/partials/
下。 编辑 extend_head.html
,内容如下:
1{{- /* Head custom content area start */ -}}
2{{- /* Insert any custom code (web-analytics, resources, etc.) - it will appear in the <head></head> section of every page. */ -}}
3{{- /* Can be overwritten by partial with the same name in the global layouts. */ -}}
4{{ if or .Params.math .Site.Params.math }}
5{{ partial "math.html" . }}
6{{ end }}
7{{- /* Head custom content area end */ -}}
3、通过在文章 front matter 中设置 math
属性 true/false
来按需加载数学公式资源。
1---
2title: 文章标题
3date:
4tags:
5math: true
6---
3.13 其它配置
3.13.1 不显示面包屑
在站点配置文件 config.yml
中关闭,如下:
1params:
2 # ...
3 ShowBreadCrumbs: false
3.13.2 日期格式化
修改站点配置文件 config.yml
,如下:
1params:
2 # ...
3 DateFormat: "2006-01-02" # 日期格式化
4 ShowFullTextinRSS: true # RSS 输出全文
3.13.3 修改文章作者
修改站点配置文件 config.yml
,如下:
1params:
2 # ...
3 author: <YourName>
3.13.4 表格优化
参考👉 折腾 Hugo & PaperMod 主题 - Dvel’s Blog
编辑 ../assets/css/extended/blank.css
文件,添加以下内容:
1/* GitHub 样式的表格 */
2.post-content table tr {
3 border: 1px solid #979da3 !important;
4}
5.post-content table tr:nth-child(2n),
6.post-content thead {
7 background-color: var(--code-bg);
8}
9.post-content table th {
10 border: 1px solid #979da3 !important;
11}
12.post-content table td {
13 border: 1px solid #979da3 !important;
14}
3.14 搜索+分类+标签集成(可选)
参考 👉 PaperMod 搜索页展示标签列表 | loyayz
将 PaperMod 主题目录下 ../themes/PaperMod/layouts/_default/search.html
文件复制到站点目录 ../layouts/_default/search.html
,编辑 search.html
文件,内容如下:
1{{- define "main" }}
2
3<header class="page-header">
4 <h1>{{- (printf "%s " .Title ) | htmlUnescape -}}
5 <!-- 取消放大镜图标 -->
6 <!-- <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none"
7 stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
8 <circle cx="11" cy="11" r="8"></circle>
9 <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
10 </svg> -->
11 </h1>
12 {{- if .Description }}
13 <div class="post-description">
14 {{ .Description }}
15 </div>
16 {{- end }}
17 {{- if not (.Param "hideMeta") }}
18 <div class="post-meta">
19 {{- partial "translation_list.html" . -}}
20 </div>
21 {{- end }}
22</header>
23
24<div id="searchbox">
25 <input id="searchInput" autofocus placeholder="{{ .Params.placeholder | default (printf "%s ↵" .Title) }}"
26 aria-label="search" type="search" autocomplete="off" maxlength="64">
27 <ul id="searchResults" aria-label="search results"></ul>
28</div>
29
30<!-- 显示分类: -->
31{{- if not (.Param "hideCategories")}}
32{{- $taxonomies := .Site.Taxonomies.categories }}
33{{- if gt (len $taxonomies) 0 }}
34<h2 style="margin-top: 32px">{{- (.Param "categoriesTitle") | default "🧩分类" }}</h2>
35<ul class="terms-tags">
36 {{- range $name, $value := $taxonomies }}
37 {{- $count := .Count }}
38 {{- with site.GetPage (printf "/categories/%s" $name) }}
39 <li>
40 <a href="{{ .Permalink }}">{{ .Name }} <sup><strong><sup>{{ $count }}</sup></strong></sup> </a>
41 </li>
42 {{- end }}
43 {{- end }}
44</ul>
45{{- end }}
46{{- end }}
47
48<!-- 显示标签: -->
49{{- if not (.Param "hideTags") }}
50{{- $taxonomies := .Site.Taxonomies.tags }}
51{{- if gt (len $taxonomies) 0 }}
52<h2 style="margin-top: 32px">{{- (.Param "tagsTitle") | default "🏷️标签" }}</h2>
53<ul class="terms-tags">
54 {{- range $name, $value := $taxonomies }}
55 {{- $count := .Count }}
56 {{- with site.GetPage (printf "/tags/%s" $name) }}
57 <li>
58 <a href="{{ .Permalink }}">{{ .Name }} <sup><strong><sup>{{ $count }}</sup></strong></sup> </a>
59 </li>
60 {{- end }}
61 {{- end }}
62</ul>
63{{- end }}
64{{- end }}
65
66{{- end }}{{/* end main */}}
💡 Tips :
将”分类“和”标签“移至搜索页后,就可以取消独立展示入口,以保持菜单整洁。
3.15 增加“说说”页面(可选)
参考1👉 hugo-Papermod添加瞬间Moments页面 | Drifting Boats
参考2👉 Hugo 添加瞬间页 | Tofuwine’s Blog
3.15.1 创建页面模板
在站点目录创建页面模板 ../layouts/moments/list.html
,内容如下:
1{{ define "main" }}
2
3 <!-- 如果需要引入 moments.css,请保持路径一致或根据自己项目结构调整 -->
4 {{ $css := resources.Get "css/extended/moments.css" | minify | fingerprint }}
5 <link
6 crossorigin="anonymous"
7 href="{{ $css.RelPermalink }}"
8 integrity="{{ $css.Data.Integrity }}"
9 rel="stylesheet"
10 />
11
12 <!-- 引入 Waline 的 CSS -->
13 <link
14 rel="stylesheet"
15 href="https://unpkg.com/@waline/client@v3/dist/waline.css"
16 />
17
18 {{ $dateformat := .Params.DateFormat }}
19
20 <article class="post-single">
21 <header class="page-header">
22 <!-- 可根据需求添加页面标题、描述等 -->
23 <!-- <h1>{{ .Title }}</h1> -->
24 </header>
25
26 <div class="tags-filter">
27 <ul>
28 <li><a href="#" class="tag-filter all-tags">全部</a></li> <!-- "全部"选项 -->
29 {{ $tags := slice }} <!-- 用于存储所有标签 -->
30
31 {{ range .Pages }}
32 {{ range .Params.tags }}
33 {{ if not (in $tags .) }}
34 {{ $tags = $tags | append . }}
35 {{ end }}
36 {{ end }}
37 {{ end }}
38
39 <!-- 按字母顺序排序标签 -->
40 {{ $tags = $tags | sort }}
41
42 {{ range $tags }}
43 <li><a href="#" class="tag-filter">{{ . }}</a></li> <!-- 标签项 -->
44 {{ end }}
45 </ul>
46 </div>
47
48 <div class="post-content">
49 <div class="moments-list">
50 {{ range .Pages }}
51 {{ if .Content }}
52 <!-- 卡片容器 -->
53 <div class="moment-card">
54 <!-- 头部:头像 + 作者名 -->
55 <div class="moment-header">
56 <div class="left-content">
57 <img
58 src="{{ site.Params.label.avatar }}"
59 alt="{{ site.Params.author }}"
60 class="moment-avatar"
61 >
62 <span class="moment-author">
63 {{ site.Params.author }}
64 </span>
65 </div>
66 </div>
67
68 <!-- 动态主体内容(Hugo 渲染后的 .Content) -->
69 <div class="moment-body">
70 <div class="moment-content-wrapper">
71 {{ .Content | safeHTML }}
72 </div>
73 <div class="moment-loading">
74 <div class="loading-spinner"></div>
75 <div class="skeleton-content">
76 <div class="skeleton-line" style="width: 90%"></div>
77 <div class="skeleton-line" style="width: 75%"></div>
78 <div class="skeleton-line" style="width: 60%"></div>
79 </div>
80 </div>
81 <div class="moment-error" style="display: none;">
82 <span>图片加载失败</span>
83 <button onclick="retryLoad(this)">重试</button>
84 </div>
85 </div>
86
87 <!-- 标签(如果有) -->
88 {{ if .Params.tags }}
89 <div class="moment-tags">
90 {{ range $tag := .Params.tags }}
91 <span class="moment-tag">{{ $tag }}</span>
92 {{ end }}
93 </div>
94 {{ end }}
95
96 <!-- 底部:时间 + 评论按钮 -->
97 <div class="moment-bottom">
98 <div class="moment-time">
99 <span>
100 {{ .Param "date" | time.Format (default site.Params.DateFormat $dateformat) }}
101 </span>
102 </div>
103 <!-- 如果没有 hideComment 参数,则显示评论按钮 -->
104 {{ if not (.Param "hideComment") }}
105 <button
106 class="moment-comment-btn"
107 onclick="showComment(this)"
108 data-slug="{{ .Param "slug" }}"
109 data-path="{{ .Param "slug" }}"
110 >
111 <!-- 评论图标 SVG -->
112 <svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M281.535354 387.361616c-31.806061 0-57.664646 26.763636-57.664647 59.733333 0 32.969697 25.858586 59.733333 57.664647 59.733334s57.664646-26.763636 57.664646-59.733334c0-33.09899-25.858586-59.733333-57.664646-59.733333z m230.529292 0c-31.806061 0-57.664646 26.763636-57.664646 59.733333 0 32.969697 25.729293 59.733333 57.664646 59.733334 31.806061 0 57.535354-26.763636 57.535354-59.733334 0-33.09899-25.858586-59.733333-57.535354-59.733333z m230.4 0c-31.806061 0-57.664646 26.763636-57.664646 59.733333 0 32.969697 25.858586 59.733333 57.664646 59.733334s57.664646-26.763636 57.664647-59.733334c-0.129293-33.09899-25.858586-59.733333-57.664647-59.733333z m115.2-270.222222H166.335354c-63.612121 0-115.2 53.527273-115.2 119.59596v390.981818c0 65.939394 52.751515 126.836364 117.785858 126.836363h175.579798c30.513131 32.581818 157.220202 149.979798 157.220202 149.979798 5.559596 5.818182 14.739394 5.818182 20.29899 0 0 0 92.832323-91.410101 153.212121-149.979798h179.717172c65.034343 0 117.785859-60.89697 117.785859-126.836363V236.606061c0.129293-65.939394-51.458586-119.466667-115.070708-119.466667z m57.535354 510.577778c0 32.969697-27.668687 67.620202-60.250505 67.620202H678.335354c-21.462626 0-40.727273 21.979798-40.727273 21.979798l-124.121212 114.941414-124.121212-114.941414s-23.660606-21.979798-43.830303-21.979798H168.921212c-32.581818 0-60.250505-34.650505-60.250505-67.620202V236.606061c0-32.969697 25.729293-59.733333 57.664647-59.733334h691.329292c31.806061 0 57.535354 26.763636 57.535354 59.733334v391.111111z m0 0"></path></svg>
113 <!-- 评论按钮文字 -->
114 <span class="comment-text">评论 ({{ .Params.comments_count | default "0" }})</span>
115 </button>
116 {{ end }}
117 </div>
118
119 <!-- 评论容器:点击按钮后会在这里渲染 Waline 评论 -->
120 <div
121 class="waline-container"
122 id="waline-{{ .Param "slug" }}"
123 data-path="{{ .Param "slug" }}"
124 ></div>
125 </div>
126 {{ end }}
127 {{ end }}
128 </div>
129 </div>
130 </article>
131
132 <!-- JavaScript 代码 -->
133 <script>
134 document.addEventListener('DOMContentLoaded', function() {
135 // 图片懒加载和预加载处理
136 const imageObserver = new IntersectionObserver((entries, observer) => {
137 entries.forEach(entry => {
138 if (entry.isIntersecting) {
139 const img = entry.target;
140 const wrapper = img.closest('.moment-content-wrapper');
141 const loadingEl = wrapper.nextElementSibling;
142 const errorEl = loadingEl.nextElementSibling;
143
144 // 显示加载状态
145 loadingEl.style.display = 'flex';
146 errorEl.style.display = 'none';
147
148 // 创建新的Image对象用于预加载
149 const tempImg = new Image();
150 tempImg.onload = function() {
151 img.src = img.dataset.src;
152 img.classList.add('loaded');
153 loadingEl.style.display = 'none';
154 observer.unobserve(img);
155 };
156 tempImg.onerror = function() {
157 loadingEl.style.display = 'none';
158 errorEl.style.display = 'block';
159 };
160 tempImg.src = img.dataset.src;
161 }
162 });
163 }, {
164 rootMargin: '50px 0px',
165 threshold: 0.1
166 });
167
168 // 处理所有图片元素
169 document.querySelectorAll('.moment-content-wrapper').forEach(wrapper => {
170 const loadingEl = wrapper.nextElementSibling;
171 const images = wrapper.querySelectorAll('img');
172
173 if (images.length === 0) {
174 loadingEl.style.display = 'none';
175 return;
176 }
177
178 let loadedImages = 0;
179 images.forEach(img => {
180 if (img.src) {
181 img.dataset.src = img.src;
182 img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; // 透明占位图
183 imageObserver.observe(img);
184
185 // 监听图片加载完成
186 img.onload = () => {
187 loadedImages++;
188 if (loadedImages === images.length) {
189 loadingEl.style.display = 'none';
190 }
191 };
192 }
193 });
194 });
195
196 // 重试加载功能
197 window.retryLoad = function(button) {
198 const errorEl = button.closest('.moment-error');
199 const loadingEl = errorEl.previousElementSibling;
200 const wrapper = loadingEl.previousElementSibling;
201 const img = wrapper.querySelector('img');
202
203 errorEl.style.display = 'none';
204 loadingEl.style.display = 'flex';
205
206 const tempImg = new Image();
207 tempImg.onload = function() {
208 img.src = img.dataset.src;
209 img.classList.add('loaded');
210 loadingEl.style.display = 'none';
211 };
212 tempImg.onerror = function() {
213 loadingEl.style.display = 'none';
214 errorEl.style.display = 'block';
215 };
216 tempImg.src = img.dataset.src;
217 };
218
219 // 新增分页相关变量
220 let currentPage = 1;
221 const pageSize = {{ .Site.Params.moments.pageSize | default 8 }};
222 let filteredMoments = [];
223 let isLoading = false;
224 let isAllLoaded = false;
225 let loadingTimeout = null;
226 const loadingHint = document.createElement('div');
227 loadingHint.className = 'scroll-hint-container';
228 loadingHint.innerHTML = '<div class="loading-hint">加载更多...</div>';
229 document.querySelector('.moments-list').after(loadingHint);
230
231 // 显示分页内容的方法
232 function displayMoments() {
233 const end = currentPage * pageSize;
234 const totalItems = filteredMoments.length;
235
236 // 优化显示逻辑,只处理新增的内容
237 const start = (currentPage - 1) * pageSize;
238 filteredMoments.slice(start, end).forEach(moment => {
239 moment.style.display = 'block';
240 // 触发图片懒加载重新检查
241 moment.querySelectorAll('img[data-src]').forEach(img => {
242 imageObserver.observe(img);
243 });
244 });
245
246 // 更新底部提示
247 const hasMore = end < totalItems;
248 isAllLoaded = !hasMore;
249
250 if (totalItems === 0) {
251 loadingHint.innerHTML = '<div class="end-divider">暂无内容</div>';
252 } else if (isAllLoaded) {
253 loadingHint.innerHTML = '<div class="end-divider">———— · 已到底部 · ————</div>';
254 } else {
255 loadingHint.innerHTML = '<div class="loading-hint">加载更多...</div>';
256 }
257 loadingHint.style.display = 'block';
258 }
259
260 // 优化的滚动事件处理
261 function checkScroll() {
262 if (isLoading || isAllLoaded || filteredMoments.length === 0) return;
263
264 const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
265 const threshold = 200; // 增加阈值,提前开始加载
266 const end = currentPage * pageSize;
267
268 if (end < filteredMoments.length && scrollTop + clientHeight >= scrollHeight - threshold) {
269 isLoading = true;
270 loadingHint.innerHTML = '<div class="loading-hint"><div class="loading-spinner"></div>加载中...</div>';
271
272 // 清除之前的超时
273 if (loadingTimeout) {
274 clearTimeout(loadingTimeout);
275 }
276
277 // 使用 requestAnimationFrame 和防抖优化性能
278 requestAnimationFrame(() => {
279 loadingTimeout = setTimeout(() => {
280 currentPage++;
281 displayMoments();
282 isLoading = false;
283 loadingTimeout = null;
284 }, 300);
285 });
286 }
287 }
288
289 // 点击标签时筛选逻辑
290 const tags = document.querySelectorAll('.tag-filter'); // 获取所有标签
291 const moments = document.querySelectorAll('.moment-card'); // 获取所有 moment 卡片
292 const allTags = document.querySelector('.all-tags'); // 获取"全部"按钮
293 const momentTags = document.querySelectorAll('.moment-tag'); // 获取所有卡片内的标签
294
295 // 默认选中"全部"标签
296 if (allTags) {
297 allTags.classList.add('selected');
298 }
299
300 // 点击标签时进行筛选
301 tags.forEach(tag => {
302 tag.addEventListener('click', function(e) {
303 e.preventDefault();
304 const selectedTag = tag.textContent.trim();
305
306 filteredMoments = Array.from(moments).filter(moment => {
307 const momentTags = moment.querySelectorAll('.moment-tag');
308 return selectedTag === '全部' ||
309 Array.from(momentTags).some(t => t.textContent === selectedTag);
310 });
311
312 currentPage = 1;
313 displayMoments();
314 window.scrollTo(0, 0); // 筛选后回到顶部
315
316 tags.forEach(t => t.classList.remove('selected'));
317 tag.classList.add('selected');
318 });
319 });
320
321 // 为卡片内的标签添加点击事件
322 momentTags.forEach(tag => {
323 tag.style.cursor = 'pointer';
324 tag.addEventListener('click', function() {
325 const tagText = this.textContent.trim();
326 // 找到对应的顶部标签并触发点击
327 tags.forEach(headerTag => {
328 if (headerTag.textContent.trim() === tagText) {
329 headerTag.click();
330 }
331 });
332 });
333 });
334
335 // 初始加载
336 filteredMoments = Array.from(moments);
337 displayMoments();
338 // 确保提示容器正确插入
339 const existingHint = document.querySelector('.scroll-hint-container');
340 if (!existingHint) {
341 const hintContainer = document.createElement('div');
342 hintContainer.className = 'scroll-hint-container';
343 document.querySelector('.moments-list').after(hintContainer);
344 }
345 window.addEventListener('scroll', checkScroll);
346 });
347 </script>
348
349 <!-- 在页面底部引入 Waline 评论脚本并初始化 -->
350 <script type="module">
351 import { init } from 'https://unpkg.com/@waline/client@v3/dist/waline.js';
352
353 const walineParams = {
354 /* 这里根据你自己的 Waline 配置进行调整 */
355 serverURL: '{{ .Site.Params.waline.serverURL }}',
356 lang: '{{ .Site.Params.waline.lang | default "zh-CN" }}',
357 visitor: '{{ .Site.Params.waline.visitor | default "匿名者" }}',
358 emoji: [
359 {{- range .Site.Params.waline.emoji }}
360 '{{ . }}',
361 {{- end }}
362 ],
363 requiredMeta: [
364 {{- range .Site.Params.waline.requiredMeta }}
365 '{{ . }}',
366 {{- end }}
367 ],
368 locale: {
369 admin: '{{ .Site.Params.waline.locale.admin | default "作者本人" }}',
370 placeholder: '{{ .Site.Params.waline.locale.placeholder | default "🍗所以我配有一条评论吗!" }}',
371 },
372 dark: '{{ .Site.Params.waline.dark | default "html.dark" }}',
373 };
374
375 // 点击"添加评论"按钮时,显示对应卡片下的评论区
376 window.showComment = function(element) {
377 const slug = element.getAttribute('data-slug');
378 const path = element.getAttribute('data-path');
379 const commentElement = document.getElementById('waline-' + slug);
380
381 // 如果已激活则清空
382 if (commentElement.classList.contains('active')) {
383 commentElement.classList.remove('active');
384 commentElement.innerHTML = '';
385 return;
386 }
387
388 // 移除其它所有已激活评论区
389 const allComments = document.querySelectorAll('.waline-container');
390 allComments.forEach(el => {
391 el.classList.remove('active');
392 el.innerHTML = '';
393 });
394
395 // 激活当前评论区
396 commentElement.classList.add('active');
397
398 // 初始化 Waline
399 init({
400 el: commentElement,
401 serverURL: walineParams.serverURL,
402 lang: walineParams.lang,
403 visitor: walineParams.visitor,
404 emoji: walineParams.emoji,
405 requiredMeta: walineParams.requiredMeta,
406 locale: walineParams.locale,
407 path: path,
408 dark: walineParams.dark,
409
410 // 个性化显示
411 commentSorting: 'hottest', // 评论列表排序方式
412 wordLimit: 500, // 评论字数限制
413 search: false, // 自定义搜索功能
414 reaction: false, // 文章表情互动功能
415 imageUploader: false, // 自定义图片上传
416 });
417 }
418 </script>
419
420 <!-- 获取评论数 -->
421 <script>
422 document.addEventListener('DOMContentLoaded', function() {
423 // 获取所有评论按钮
424 const commentBtns = document.querySelectorAll('.moment-comment-btn');
425
426 // 确保有找到评论按钮
427 if (commentBtns.length > 0) {
428 commentBtns.forEach(button => {
429 const slug = button.getAttribute('data-slug'); // 获取按钮对应的 slug
430 const commentText = button.querySelector('.comment-text'); // 获取按钮中的评论文本
431 const serverURL = '{{ .Site.Params.waline.serverURL }}'; // 获取 Waline 服务器地址
432
433 // 输出调试信息,查看是否有多个按钮
434 console.log(`Processing button with slug: ${slug}`);
435
436 if (slug && commentText) {
437 // 假设你有一个获取评论数量的 API 或接口
438 fetch(`${serverURL}/api/comment?type=count&url=${slug}`)
439 .then(response => response.json())
440 .then(data => {
441 if (commentText) {
442 // 从 API 返回的数据中提取评论数
443 const commentCount = data.data && data.data[0] ? data.data[0] : 0; // 如果没有数据或评论数,默认为 0
444
445 // 输出调试信息,查看评论数
446 console.log(`Fetched comment count: ${commentCount}`);
447
448 // 更新评论按钮上的评论数量
449 commentText.textContent = `评论 (${commentCount})`; // 更新评论数
450 }
451 })
452 .catch(error => {
453 console.error('Error fetching comment count:', error);
454 if (commentText) {
455 // 如果 API 请求失败,保持评论数为 0
456 commentText.textContent = `评论 (0)`;
457 }
458 });
459 } else {
460 console.error('Slug or commentText missing for button:', button);
461 }
462 });
463 } else {
464 console.error('No comment buttons found');
465 }
466 });
467 </script>
468
469{{ end }}
3.15.2 Build options
创建构建选项文件 ../content/moments/_index.md
,内容如下:
1---
2title: "说说"
3DateFormat: 2006-01-02 15:04
4build:
5 render: always
6cascade:
7 - build:
8 list: local
9 publishResources: false
10 render: never
11---
3.15.3 定义页面样式
自定义页面样式 ../assets/css/extended/moments.css
,内容如下:
1/*
2 全局:亮色模式下的默认值
3 说明:PaperMod 会在 <html> 或 <body> 上添加 .dark 类切换暗色,
4 所以这里只需定义默认(亮色)和暗色时的变量即可。
5*/
6:root {
7 --card-bg: #fff;
8 --card-text: #333;
9 --tag-bg: #f2f4f5;
10 --tag-text: #333;
11 --comment-btn-color: #999;
12 --time-color: #999;
13 --tag-filter-bg: #fff;
14 --tag-filter-text: #333;
15 --tag-filter-hover-bg: #55ac68;
16 --gradient-mask: linear-gradient(180deg, rgba(255, 255, 255, 0), #ffffff 100%);
17}
18
19/* 暗色模式下的变量 */
20.dark {
21 --card-bg: rgb(46, 46, 51);
22 --card-text: rgb(196, 196, 197); /* 对比度够高的浅色文字 */
23 --tag-bg: rgb(65, 66, 68); /* 比卡片再浅一些,或自己喜欢的颜色 */
24 --tag-text: #eee;
25 --comment-btn-color: #aaa;
26 --time-color: #ccc;
27 --tag-filter-bg: #555;
28 --tag-filter-text: #ddd;
29 --tag-filter-hover-bg: #55ac68;
30 --gradient-mask: linear-gradient(180deg, rgba(46, 46, 51, 0), rgb(46, 46, 51) 100%);
31 --tag-filter-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); /* 新增暗色阴影变量 */
32}
33
34/* 单列容器:依旧保持 vertical 布局 */
35.moments-list {
36 display: flex;
37 flex-direction: column;
38 gap: 1.5rem;
39 margin-top: 1.5rem;
40}
41
42/* 卡片:使用CSS变量 */
43.moment-card {
44 width: 100%;
45 max-width: 800px; /* 每张卡片最多800px */
46 margin: 0 auto; /* 居中 */
47 position: relative;
48 background: var(--card-bg);
49 color: var(--card-text);
50 border-radius: 8px;
51 padding: 1rem;
52 box-shadow: 0 2px 8px rgba(0,0,0,0.06);
53 display: none; /* 初始隐藏,由JS控制显示 */
54 flex-direction: column;
55 justify-content: space-between;
56}
57
58.moment-card.hidden {
59 display: none; /* 隐藏元素并且不占据空间 */
60}
61
62/* 头像 & 作者 */
63.moment-header {
64 position: relative;
65 display: flex; /* 水平排列头像、昵称和按钮 */
66 align-items: center; /* 垂直居中 */
67 justify-content: space-between; /* 头像和昵称左对齐,按钮右对齐 */
68}
69
70.moment-header .left-content {
71 display: flex;
72 align-items: center;
73 gap: 1rem; /* 固定间距,控制头像和昵称之间的距离 */
74}
75
76.moment-avatar{
77 width: 40px;
78 height: 40px;
79 object-fit: cover; /* 保证图片不变形 */
80 border-radius: 4px;
81 transition: transform 1s ease; /* 平滑的旋转过渡效果 */
82 cursor: pointer; /* 鼠标悬停时显示为可点击 */
83}
84
85.moment-avatar:active {
86 transform: rotate(360deg); /* 点击时旋转一圈 */
87}
88
89.moment-author {
90 font-weight: 600;
91 font-size: 16px;
92 /* 跟随卡片文字颜色 */
93 color: var(--card-text);
94}
95
96/* 主体内容 */
97.moment-body {
98 font-size: 16px;
99 color: var(--card-text);
100 padding: 0;
101 line-height: 1.5;
102}
103
104.moment-body p {
105 margin-bottom: 0.6rem; /* 默认值可能是 1rem,改为 0.75rem 或更小 */
106 line-height: 1.6; /* 确保行间距仍然易读 */
107}
108
109.moment-body ol {
110 padding-left: 1rem; /* 调整左侧内边距,避免序号超出范围 */
111 margin-left: 0; /* 确保整体左对齐 */
112 list-style-position: inside; /* 确保序号在内容外部显示 */
113}
114
115.image-row {
116 display: grid;
117 gap: 0.4rem; /* 默认图片之间的间距 */
118 margin: 1rem 0; /* 上下与其他内容的间距 */
119}
120
121.image-row-1col {
122 grid-template-columns: repeat(1, 1fr); /* 单列布局 */
123}
124
125.image-row-2col {
126 grid-template-columns: repeat(2, 1fr); /* 双列布局 */
127 gap: 0.4rem; /* 自定义两列间距 */
128}
129
130.image-row-3col {
131 grid-template-columns: repeat(3, 1fr); /* 三列布局 */
132 gap: 0.2rem; /* 自定义三列间距 */
133}
134
135.image-row img {
136 width: 100%; /* 图片宽度占满格子 */
137 aspect-ratio: 1 / 1; /* 强制为正方形 */
138 object-fit: cover; /* 裁切图片内容,居中显示 */
139 display: block; /* 避免周围多余间隙 */
140 border-radius: 2px; /* 可选:增加圆角 */
141}
142
143/* 标签 */
144.moment-tags {
145 margin-bottom: 1rem;
146}
147
148.moment-tag {
149 display: inline-block;
150 padding: 0.3em 0.6em;
151 margin-right: 0.5em;
152 margin-top: 1.2em;
153 background: var(--tag-bg);
154 color: var(--tag-text);
155 font-size: 14px;
156 border-radius: 4px;
157}
158
159/* 底部:时间 + 评论按钮 */
160.moment-bottom {
161 display: flex;
162 align-items: center;
163 justify-content: space-between;
164}
165
166.moment-time span {
167 font-size: 14px;
168 color: var(--time-color); /* 比主体更淡一点 */
169}
170
171/* 评论按钮 */
172.moment-comment-btn {
173 background: none;
174 border: none;
175 cursor: pointer;
176 padding: 0;
177 display: inline-flex;
178 align-items: center;
179 /* 使用变量 color */
180 color: var(--comment-btn-color);
181 transition: color 0.2s;
182 font-size: 14px;
183}
184
185.moment-comment-btn:hover {
186 /* 可在暗色下也进行不同程度的 hover 颜色变化 */
187 color: var(--card-text);
188}
189
190.moment-comment-btn svg {
191 width: 18px;
192 height: 18px;
193 fill: currentColor;
194}
195
196.moment-comment-btn .comment-text {
197 margin-left: 0.3em;
198}
199
200/* 顶部筛选标签样式 */
201.tags-filter {
202 margin-bottom: 20px;
203}
204
205.tags-filter ul {
206 list-style: none;
207 padding: 0;
208 display: flex;
209 flex-wrap: wrap;
210 gap: 10px;
211}
212
213.tags-filter li {
214 margin: 0;
215}
216
217.tags-filter .tag-filter {
218 text-decoration: none;
219 color: var(--tag-filter-text);
220 cursor: pointer;
221 padding: 8px 16px;
222 background-color: var(--tag-filter-bg);
223 border-radius: 20px;
224 font-size: 14px;
225 transition: all 0.3s ease;
226 font-weight: normal;
227 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); /* 缩小垂直偏移,降低透明度 */
228}
229
230.tag-filter.selected {
231 background-color: var(--tag-filter-hover-bg);
232 color: #fff;
233 font-weight: bold;
234 box-shadow: 0 2px 4px rgba(85, 172, 104, 0.1); /* 减少扩散半径和透明度 */
235}
236
237.tag-filter:hover {
238 background-color: var(--tag-filter-hover-bg);
239 color: #fff;
240 transform: translateY(-1px);
241 box-shadow: 0 2px 6px rgba(85, 172, 104, 0.15); /* 保持微移效果但降低阴影强度 */
242}
243
244go.moment-loading {
245 display: none;
246 flex-direction: column;
247 align-items: center;
248 justify-content: center;
249 padding: 1rem;
250 gap: 0.5rem;
251}
252
253.loading-spinner {
254 width: 24px;
255 height: 24px;
256 border: 2px solid var(--card-text);
257 border-top-color: transparent;
258 border-radius: 50%;
259 animation: spin 1s linear infinite;
260}
261
262@keyframes spin {
263 to { transform: rotate(360deg); }
264}
265
266.loading-hint {
267 text-align: center;
268 padding: 0.5rem 0;
269 color: var(--card-text);
270 margin: 0 auto;
271 max-width: 600px;
272 opacity: 0.7;
273 font-size: 0.9em;
274 transition: opacity 0.3s ease;
275}
276
277.loading-hint[style*="block"],
278.end-divider {
279 display: block !important;
280}
281
282.end-hint {
283 text-align: center;
284 padding: 2rem 0;
285 position: relative;
286}
287
288.end-divider {
289 color: #dcdcdc;
290 font-size: 0.9em;
291 text-align: center;
292 width: 100%;
293 padding: 2rem 0;
294 margin: 0 auto;
295}
296
297/* 修复提示容器样式(新增) */
298.scroll-hint-container {
299 width: 100%;
300 max-width: 800px;
301 margin: 0 auto;
302 padding: 1rem;
303 font-size: 0.95em;
304}
305
306.skeleton-content {
307 width: 100%;
308 padding: 0.5rem 0;
309}
310
311.skeleton-line {
312 height: 16px;
313 background: linear-gradient(90deg, var(--card-bg) 25%, rgba(128, 128, 128, 0.1) 50%, var(--card-bg) 75%);
314 background-size: 200% 100%;
315 animation: loading 1.5s infinite;
316 border-radius: 4px;
317 margin-bottom: 0.5rem;
318}
319
320@keyframes loading {
321 0% { background-position: 200% 0; }
322 100% { background-position: -200% 0; }
323}
3.15.4 新增一条说说
在 moments
目录下创建一个 md
文档,frontmatter
参考如下:
1---
2date: 2024-03-13T09:05:00+08:00
3slug: "change to your moment slug"
4tags:
5 - Apple
6draft: false
7---
8enter your moment content.
参数说明:
date
是可选的。在瞬间左下角显示的时间。(建议显示指定该值,如果你未配置此项,也可能显示时间,因为赋值方式为.Param "date"
)slug
是必须的。它涉及到与评论绑定,建议使用 UUID 或随机数来保证不重复。tags
是可选的。标记这条瞬间的标签,可以为多个。(注意,此标签与文章标签无关)hideComment
是可选的。如果为true
则不会在这个瞬间的右下角显示评论按钮。
3.15.5 头像问题
头像如果不能正常显示,需在站点配置文件中添加一项参数,如下:
1params:
2 # ......
3 label:
4 # ......
5 avatar: "/imgs/avatar.png" # 修改为你的实际图像路径