I. 主题文件

Hugo PaperMod - GitHub

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 增加导航菜单

这里修改站点配置文件,增加 archivessearch 导航菜单,并调整显示顺序。

 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、汉化文章页 <<PREVNEXT>> 等页面元素,修改站点配置文件 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 文章目录(大纲)

参考👉 Hugo侧边目录 | 3rd’s Blog

1、在站点配置文件 config.yml 中启用目录(大纲),如下:

1params:
2  # ...
3  showtoc: true  # 显示目录
4  tocopen: true  # 自动展开目录

2、PaperMod 主题默认将文章目录放在顶部,不方便阅读时跳转,修改为侧边目录。创建 toc.htmltoc.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 新标签打开链接

参考👉 设置以新标签打开链接 - Dvel’s Blog

创建 ../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 站点配置文件:

  1. comments 值改为 true
  2. 增加 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 代码块优化

参考👉 Hugo以及PaperMod主题的配置 | 似水

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>&copy; {{ 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&nbsp;" .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"  # 修改为你的实际图像路径   

IV. 参考文档

  1. 田少晗的个人博客

  2. 周鑫的个人博客

  3. 似水

  4. 夜云泊

  5. 3rd’s Blog

  6. Dvel’s Blog

  7. Tofuwine’s Blog

  8. loyayz

  9. Drifting Boats

  10. 微控圈(MCU Loop)