装修一下旧时代的船,才能开门迎接更多的客人。

前言

搭建完博客之后,总是会觉得这不满意,然后在搭建过程中,用相同的建站工具和主题搜索,会发现网上有很多大佬已经搭建好的站点,就会这个功能也想要,那个功能也想要。在这里,我记录了一些我搭建过程中进行基础功能拓展和美化的过程。希望可以帮助到有缘人~

以下教程为进阶版的简略教程,需要你有比较扎实的编程基础(或者不爱思考的脑袋),很多教程来自网上其他大佬,由于看了太多的教程,链接引用不全请见谅,如果有大佬看到了可以联系我加上。

以下修改html和css的代码都不建议在themes文件夹直接修改,html可以在layout目录下新建同名文件(复制过来改),css可以在assets目录下新建,最终构建都会打包到public中并使用自定义的覆盖主题文件夹中的内容。

好了,接下来就是博客裁缝的展示环节了。

拓展功能

评论

Gisucs

选用Gisucs作为评论服务,最开始我的选择是Gisucs,但是后来我放弃了。Github作为存储虽然是全免费,非常优雅,但是要求评论者拥有一个Github账号对于用户体验实在是不太友好。

Github配置

参考链接: https://giscus.app/zh-CN#repository

  1. 在某个Github公开仓库开启discussion功能(settings进入)
  2. 在Github安装Gisucs APP
  3. 记录生成的配置和代码,需要配置到自己的项目中

Hugo配置

参考链接: https://www.tofuwine.cn/posts/610b75f5/#pe-comments

  1. 添加文件 layouts/partials/comments.html,输入以下内容
layouts/partials/comments.html
 1<div class="comments-title" id="tw-comment-title">
 2    <p class="x-comments-title">{{- .Param "giscus.discussionTitle" }}</p>
 3    <p style="font-size: 1rem">{{- .Param "giscus.discussionSubtitle" }} </p>
 4</div>
 5<div id="tw-comment"></div>
 6<script>
 7    const getStoredTheme = () => localStorage.getItem("pref-theme") === "dark" ? "{{ .Site.Params.giscus.darkTheme }}" : "{{ .Site.Params.giscus.lightTheme }}";
 8    const setGiscusTheme = () => {
 9        const sendMessage = (message) => {
10            const iframe = document.querySelector('iframe.giscus-frame');
11            if (iframe) {
12                iframe.contentWindow.postMessage({giscus: message}, 'https://giscus.app');
13            }
14        }
15        sendMessage({setConfig: {theme: getStoredTheme()}})
16    }
17
18    document.addEventListener("DOMContentLoaded", () => {
19        const giscusAttributes = {
20            "src": "https://giscus.app/client.js",
21            "data-repo": "{{ .Site.Params.giscus.repo }}",
22            "data-repo-id": "{{ .Site.Params.giscus.repoId }}",
23            "data-category": "{{ .Site.Params.giscus.category }}",
24            "data-category-id": "{{ .Site.Params.giscus.categoryId }}",
25            "data-mapping": "{{ .Site.Params.giscus.mapping  }}",
26            "data-strict": "{{ .Site.Params.giscus.strict  }}",
27            "data-reactions-enabled": "{{ .Site.Params.giscus.reactionsEnabled}}",
28            "data-emit-metadata": "{{ .Site.Params.giscus.emitMetadata  }}",
29            "data-input-position": "{{ .Site.Params.giscus.inputPosition }}",
30            "data-theme": getStoredTheme(),
31            "data-lang": "{{ .Site.Params.giscus.lang }}",
32            "data-loading": "lazy",
33            "crossorigin": "anonymous",
34            "async": "",
35        };
36
37        // 动态创建 giscus script
38        const giscusScript = document.createElement("script");
39        Object.entries(giscusAttributes).forEach(
40                ([key, value]) => giscusScript.setAttribute(key, value));
41        document.querySelector("#tw-comment").appendChild(giscusScript);
42
43        // 页面主题变更后,变更 giscus 主题
44        const themeSwitcher = document.querySelector("#theme-toggle");
45        if (themeSwitcher) {
46            themeSwitcher.addEventListener("click", setGiscusTheme);
47        }
48        const themeFloatSwitcher = document.querySelector("#theme-toggle-float");
49        if (themeFloatSwitcher) {
50            themeFloatSwitcher.addEventListener("click", setGiscusTheme);
51        }
52    });
53</script>
  1. 添加文件 assets/css/extended/comments.css,输入以下内容
assets/css/extended/comments.css
 1/* giscus 评论组件 */
 2.comments-title {
 3    margin-top: 2rem;
 4    margin-bottom: 2rem;
 5    display: block;
 6    text-align: center;
 7}
 8
 9.x-comments-title {
10    display: block;
11    font-size: 1.25em;
12    font-weight: 700;
13    padding: 1.5rem 0 .5rem;
14}
  1. hugo.yaml 中添加内容,其中 {{}} 包裹的需要替换成自己的内容,内容在Gisucs会生成
hugo.yaml
 1  giscus:
 2    repo: "{{ REPO }}"
 3    repoId: "{ REPO_ID }"
 4    category: "Announcements"
 5    categoryId: "{{ CATEGORYID }}"
 6    mapping: "pathname"
 7    strict: "0"
 8    reactionsEnabled: "1"
 9    emitMetadata: "0"
10    inputPosition: "bottom"
11    lightTheme: "light"
12    darkTheme: "dark"
13    lang: "zh-CN"
14    discussionTitle: 欢迎来到评论区
15    discussionSubtitle: 感谢您的耐心阅读!如需交流,请留个评论吧!
  1. archetypes/default.md 中添加配置 comments: true 来开启评论区,已有的博客也需要修改这个来开启

Twikoo

Twikoo 官网

因为Gisucs要求评论者具有Github账号有点使用者不太友好,所以后面更换了一个Twikoo

Twikoo的整体可以看作是一个CSS的架构,C-Hugo站点的静态JS代码,S-云函数,S-MonogoDB数据库。Twikoo的官方是懂我们这些穷鬼的,提供了很多免费部署的教程。我这边就按照官方推荐度最高的方法进行部署。MonogoDB选择MongoDB Atlas,有500MB的免费额度。Netlify部署云函数,每月 125,000 请求次数和 100 小时函数计算时长,看上去是完全够了。

MongoDB Atlas 申请资源

MongoDB Atlas

按照Twikoo官网指导,申请一个账号和500MB空间的MonogoDB数据库。

  1. 注册MongoDB Atlas账号
  2. 无脑下一步,创建一个MonogoDB实例,这里创建数据库用户的时候密码记住,后面要用
  3. 修改Network Access为 0.0.0.0 允许所有IP访问
  4. 在connect中查看代码示例,选Node.js可以看到连接串,把数据库用户密码替换进去,把整个URL复制下来,等部署云函数的时候有用

Netlify 部署云函数

Netlify

  1. 注册Netlify账号,注册的时候需要提供身份验证(有点恶心人了),密码需要设置得复杂一点,不然直接不给登录也不提示,我通过忘记密码改了一个很复杂的。(后来证明我第二次登录就忘记了)
  2. Fork官方的仓到自己的Github,从仓库部署站点,环境变量中填入 MONGODB_URI 值为MonoDB Atlas中复制出来的URL
  3. 点xxx.netlify.app进入页面,看到云函数正常运行就是部署成功了

前端部署

需要把前端JS嵌入到Hugo的Html页面中,在 layout/comments.html 文件中添加代码 (如果有其他评论组件请自行删除)

layout/comments.html
1<div id="tcomment"></div>
2<script src="https://cdn.jsdelivr.net/npm/twikoo@1.6.40/dist/twikoo.all.min.js"></script>
3<script>
4twikoo.init({
5  envId: 'https://xxx.netlify.app/.netlify/functions/twikoo', 
6  el: '#tcomment', // 容器元素
7})
8</script>

使用配置

  1. 进入管理员界面,首次进入配置密码
  2. 配置邮箱通知,Hotmail配置了无法测试通过,暂时没找到原因,后来配置了一个163的邮箱(需要填授权码,听说qq更方便一点,gmail没有尝试)
  3. 反垃圾配置,使用默认的反垃圾服务 akismet,支付一个0元账单就可以获得一个APPKey,填入配置里面就好
  4. 头像服务,默认的 weavatar,注册一个账号,上传一个自己喜欢的头像。但是这边遇到了一个比较奇怪的问题,同时输入昵称和邮箱,头像就会读取失败(但是后来发现生产上好像可以),所以我在通用设置里面把必填字段改成了只填邮箱,因为昵称填QQ号会自动生成qq邮箱。

侧面显示目录

参考资料: https://www.zhouxin.space/logs/introduce-side-toc-and-reading-percentage-to-papermod/

  1. 创建文件 layouts/partials/toc.html 输入以下代码(代码全是抄的,别问我为什么这么写)
layouts/partials/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 }}
  1. 创建 assets/css/extended/toc.css 文件,并输入以下内容(也是抄的,感谢大佬,赞美大佬)
assets/css/extended/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}
  1. 同时在archetypes/default.yml中添加参数
archetypes/default.yml
1showToc: true # 显示目录
2TocOpen: true # 打开目录

PV/UV

不蒜子Busuanzi

使用不蒜子Busuanzi进行站点访问统计。 参考链接: https://blog.kanikig.xyz/hugo-busuanzi/

  1. 添加文件 layouts/parials/head.html 从主题文件夹 themes/layouts/parials/head.html 中拷贝内容并修改,在Styles上方添加一个if块
layouts/parials/head.html
 1...
 2{{- if site.Params.analytics.naver.SiteVerificationTag }}
 3<meta name="naver-site-verification" content="{{ site.Params.analytics.naver.SiteVerificationTag }}">
 4{{- end }}
 5
 6<!-- 以下If块为新增内容 -->
 7{{- if .Site.Params.busuanzi.enable -}}
 8  <script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
 9  <meta name="referrer" content="no-referrer-when-downgrade">
10{{- end -}}
11
12{{- /* Styles */}}
13...
  1. 添加文件 layouts/parials/footer.html 从主题文件夹 themes/layouts/parials/footer.html 中拷贝内容并修改,在footer标签内添加一个if块
layouts/parials/footer.html
 1...
 2{{- if not (.Param "hideFooter") }}
 3<footer class="footer">
 4    {{- if not site.Params.footer.hideCopyright }}
 5    {{- if site.Copyright }}
 6    <span>{{ site.Copyright | markdownify }}</span>
 7    {{- else }}
 8    <span>&copy; {{ now.Year }} <a href="{{ "" | absLangURL }}">{{ site.Title }}</a></span>
 9    {{- end }}
10    {{- print " · "}}
11    {{- end }}
12
13    {{- with site.Params.footer.text }}
14    {{ . | markdownify }}
15    {{- print " · "}}
16    {{- end }}
17
18    <!-- 以下IF块为新增内容 -->
19    {{ if .Site.Params.busuanzi.enable -}}
20    <div class="busuanzi-footer">
21        <span id="busuanzi_container_site_pv">
22            本站总访问量<span id="busuanzi_value_site_pv"></span>23        </span>
24        <span id="busuanzi_container_site_uv">
25            本站访客数<span id="busuanzi_value_site_uv"></span>人次
26        </span>
27    </div>
28    {{- end -}}
29
30
31    <span>
32        Powered by
33        <a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> &
34        <a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener" target="_blank">PaperMod</a>
35    </span>
36</footer>
37...
38{{- end }}
  1. 添加文件 layouts/_defaults/single.html 从主题文件夹 themes/layouts/_defaults/single.html 中拷贝内容并修改,在post-meta内添加一个if块
layouts/_defaults/single.html
 1...
 2    <div class="post-meta">
 3      {{- partial "post_meta.html" . -}}
 4      {{- partial "translation_list.html" . -}}
 5      {{- partial "edit_post.html" . -}}
 6      {{- partial "post_canonical.html" . -}}
 7
 8      <!-- 以下If块为新增内容 -->
 9      {{ if .Site.Params.busuanzi.enable -}}
10      <div class="meta-item">&nbsp·&nbsp
11        <span id="busuanzi_container_page_pv">本文阅读量<span id="busuanzi_value_page_pv"></span></span>
12      </div>
13      {{- end }}
14
15    </div>
16...
  1. hugo.html文件中的param节点中添加开关
hugo.html
1params:
2  busuanzi:
3      enable: true

Google Analyze

只需要在 hugo.yaml 中添加以下代码即可,id更换为自己在Google Analytic中申请的跟踪ID

hugo.html
1services:
2    googleAnalytics:
3        id: "G-XXXXXXXXXX"

时间线

添加一个根据时间线排列的归档页面,写的多写的久才有质感。多写写吧!

  1. 添加 content/archive.md 文件,输入以下内容
content/archive.md
1---
2title: ""
3layout: "archives"
4url: "/archives/"
5summary: archives
6---
  1. hugo.html 中添加按钮,在 menu.main 节点下新增以下内容
hugo.html
1    - identifier: archive
2      name: 时间轴
3      url: /archives/
4      weight: 11

留言板

利用评论系统,新增一个Layout来构建一个留言板页面。

  1. 创建 layouts/message.html 文件输入一下代码(代码从 single.html 中删除了很多东西,简化得到,顺便做了一下标题居中,这回是自己写的了):
layouts/message.html
 1{{- define "main" }}
 2<article class="post-single">
 3  <header class="post-header">
 4    <h1 class="post-title entry-hint-parent">
 5      <div style="margin-left: auto;margin-right: auto;">
 6        {{ .Title }}
 7      </div>
 8    </h1>
 9  </header>
10
11  {{- if .Content }}
12  <div class="post-content">
13    {{ .Content }}
14  </div>
15  {{- end }}
16
17  <footer class="post-footer">
18    <div id="tcomment"></div>
19    <script src="https://cdn.jsdelivr.net/npm/twikoo@1.6.40/dist/twikoo.all.min.js"></script>
20    <script>
21      twikoo.init({
22        envId: 'xxx',
23        el: '#tcomment',
24      })
25    </script>
26  </footer>
27</article>
28{{- end }}
  1. 在命令行中创建一个新的md文件(不要直接新建文件,我试了,编译的时候不会生成文件,会导致404),输入 hugo new message.md,并在其中输入以下代码
content/message.md
1---
2title: "💬 留言板"
3date: '2024-12-13T21:14:22+08:00'
4draft: false # 默认为草稿模式
5layout: "message"
6comments: true # 评论
7---
  1. hugo.yaml 中新增首页目录,添加以下代码
hugo.yaml
1    - identifier: message
2      name: 💬 留言板
3      url: /message/
4      weight: 21

添加最近修改时间

复制主题中的 post_meta.htmllayout/partials/post_meta.html 新增最近修改时间的内容,并修改发布时间的内容和国际化,

  1. layout/partials/post_meta.html 输入以下代码
layout/partials/post_meta.html
 1{{- $scratch := newScratch }}
 2
 3{{- if not .Date.IsZero -}}
 4  {{- $dateStr := .Date.Format (default "January 2, 2006" site.Params.DateFormat) -}}
 5  {{- $translatedPublished := i18n "published" -}}
 6  {{- $dateHTML := printf "<span title='%s'>%s</span>" (.Date) $dateStr -}}
 7  {{- $scratch.Add "meta" (slice (printf "%s %s" $translatedPublished $dateHTML)) }}
 8{{- end }}
 9
10{{- if not .Lastmod.IsZero -}}
11  {{- $lastmodStr := .Lastmod.Format (default "January 2, 2006" site.Params.DateFormat) -}}
12  {{- $translatedLastModified := i18n "last_modified" -}}
13  {{- $lastmodHTML := printf "<span title='%s'>%s</span>" (.Lastmod) $lastmodStr -}}
14  {{- $scratch.Add "meta" (slice (printf "%s %s" $translatedLastModified $lastmodHTML)) }}
15{{- end }}
16
17{{- if (.Param "ShowReadingTime") -}}
18{{- $scratch.Add "meta" (slice (i18n "read_time" .ReadingTime | default (printf "%d min" .ReadingTime))) }}
19{{- end }}
20
21{{- if (.Param "ShowWordCount") -}}
22{{- $scratch.Add "meta" (slice (i18n "words" .WordCount | default (printf "%d words" .WordCount))) }}
23{{- end }}
24
25{{- if not (.Param "hideAuthor") -}}
26{{- with (partial "author.html" .) }}
27{{- $scratch.Add "meta" (slice .) }}
28{{- end }}
29{{- end }}
30
31{{- with ($scratch.Get "meta") }}
32{{- delimit . "&nbsp;·&nbsp;" | safeHTML -}}
33{{- end -}}
  1. 在i18n文件夹的对应目录添加翻译
  2. hugo.yaml 中新增配置 frontmatte.lastmod: ['lastmod', ':git', 'date', 'publishDate'] 用于指定lastmod变量的赋值顺序

自定义域名

首先需要有一个域名,在对应的域名服务商添加一条解析记录,解析类型:CNAME,解析值为 xxx.github.io (xxx替换为自己的Github用户名)

再添加文件 static/CNAME ,文件的内容为自定义的域名。没有这个文件,访问域名会出现404。

友链

参考: https://aimerneige.com/zh/post/web/add-friend-link-in-papermod-blog/

这个是没有脑子直接抄的

  1. 创建一个文件 layouts/partials/friends.html 输入以下代码
layouts/partials/friends.html
 1<style type="text/css">
 2    .friends {
 3        --link-count-per-row: 1;
 4    }
 5
 6    @media screen and (min-width: 576px) {
 7        .friends {
 8            --link-count-per-row: 2;
 9        }
10    }
11
12    @media screen and (min-width: 768px) {
13        .friends {
14            --link-count-per-row: 3;
15        }
16    }
17
18    .friends {
19        display: grid;
20        grid-template-columns: repeat(var(--link-count-per-row), 1fr);
21        grid-gap: 16px;
22    }
23
24    /* 空间占位 */
25    .friend-skeleton {
26        height: 280px;
27        display: inline-block;
28        position: relative;
29    }
30
31    .friend {
32        height: 100%;
33        width: 100%;
34        position: absolute;
35        top: 0;
36        left: 0;
37        transition: 0.67s cubic-bezier(0.19, 1, 0.22, 1);
38        border-radius: var(--radius);
39        box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2),
40            0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12) !important;
41        overflow: hidden;
42        display: flex;
43        flex-direction: column;
44        justify-content: flex-start;
45        align-items: center;
46    }
47
48    .friend:hover {
49        transform: translateY(-8px);
50        box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2),
51            0 5px 8px 0 rgba(0, 0, 0, 0.14), 0 1px 14px 0 rgba(0, 0, 0, 0.12) !important;
52    }
53
54    .friend-avatar {
55        object-fit: cover;
56        width: 100%;
57        height: 180px;
58        margin: 0 !important;
59        border-radius: 0 !important;
60    }
61    .friend-content {
62        text-align: center;
63        flex: 1;
64        width: 100%;
65        padding: 16px;
66        background: var(--entry);
67        transform: translate3d(0, 0, 0);
68    }
69
70    .friend-name {
71        font-size: 1.2rem;
72        font-weight: bold;
73        transform: inherit;
74    }
75
76    .friend-description {
77        font-size: 0.8rem;
78        color: var(--secondary);
79        transform: translate3d(0, 0, 0);
80    }
81</style>
82<div class="friends">
83    {{ range .Site.Data.friends }}
84    <div class="friend-skeleton">
85        <a href="{{ .link }}" target="_blank">
86            <div class="friend">
87                <img class="friend-avatar" src="{{ .image }}" />
88                <div class="friend-content">
89                    <div class="friend-name">{{ .title }}</div>
90                    <div class="friend-description">{{ .intro }}</div>
91                </div>
92            </div>
93        </a>
94    </div>
95    {{ end }}
96</div>
97<!-- style code by https://github.com/fissssssh -->
98<!-- view https://github.com/fissssssh/fissssssh.github.io for more detail -->
  1. 创建 layouts/_default/friends.html 文件,输入一下代码,用于创建新的界面
layouts/_default/friends.html
 1{{- define "main" }}
 2<article class="post-single">
 3  <header class="post-header">
 4    {{ partial "breadcrumbs.html" . }}
 5    <h1 class="post-title entry-hint-parent">
 6      {{ .Title }}
 7    </h1>
 8  </header>
 9  {{- $isHidden := (.Param "cover.hiddenInSingle") | default (.Param "cover.hidden") | default false }}
10  {{- partial "cover.html" (dict "cxt" . "IsSingle" true "isHidden" $isHidden) }}
11  {{- if (.Param "ShowToc") }}
12  {{- partial "toc.html" . }}
13  {{- end }}
14  <div style="height: 40px;"></div>
15  {{ .Content }}
16  <div style="height: 40px;"></div>
17  {{ partial "friends.html" . }}
18  <div style="height: 40px;"></div>
19  {{- if (.Param "comments") }}
20  {{- partial "comments.html" . }}
21  {{- end }}
22</article>
23{{- end }}{{/* end main */}}
  1. 创建 content/posts/friends.md 文件,输入以下内容
content/posts/friends.md
1---
2title: "🧑‍🤝‍🧑 Zanks的朋友们 🧑‍🤝‍🧑"
3date: '2024-01-01T00:00:00+08:00'
4author: "Zanks"
5layout: "friends"
6comments: true # 评论
7---
8
9排名不分先后,按时间顺序添加。如需新增请留言,或通过其他方式联系我。
  1. 创建 data/friends.yml 文件,用于存储友链的数据,以下仅为示范
data/friends.yml
1- title: "伞"
2  intro: "一只咸鱼的学习记录"
3  link: "https://umb.ink/"
4  image: "https://avatars.githubusercontent.com/u/53655863?v=4"
5- title: "HelloWorld的小博客"
6  intro: "这里是一个小白的博客"
7  link: "https://mzdluo123.github.io/"
8  image: "https://avatars.githubusercontent.com/u/23146087?v=4"

全站文章和字数统计

参考:https://huuuuuuo-github-io.vercel.app/post/hugo%E6%80%BB%E5%AD%97%E6%95%B0%E7%BB%9F%E8%AE%A1/

可以将以下代码加入到任何喜欢的页面,我放在了About页面

[ html ]
 1{{$scratch := newScratch}}
 2{{ range (where .Site.RegularPages "Section" "posts" )}}
 3{{$scratch.Add "total" .WordCount}}
 4{{ end }}
 5<div>
 6	<div>
 7	  📚 文章数:
 8	</div>
 9	<div>
10	  {{ len (where .Site.RegularPages "Section" "posts") }}
11	</div>
12  </div>
13  <div>
14	<div>
15	  ✍️ 总字数:
16	</div>
17	<div>
18	  {{ div ($scratch.Get "total") 1000.0 | lang.FormatNumber 2 }}K
19	</div>
20</div>

右侧边栏

底部悬浮按钮

参考了一个个性化的PaperMod主题 https://github.com/tofuwine/PaperMod-PE 实现的效果是在页面的右侧添加了一个前往评论区的按钮,重写了官方回到顶部的按钮(添加了阅读进度)

  1. static/js 目录下引入两个文件分别用于实现按钮的动态样式,实现逻辑来自PaperMod-PE源码。
static/js/go-comment.js
 1// go-comment.js
 2document.addEventListener('scroll', function () {
 3    const comment = document.getElementById("comments-title");
 4    const bottomToComment = document.getElementById("comments-link")
 5    const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
 6    const offsetTop = comment.offsetTop
 7    const scrollTop = document.documentElement.scrollTop
 8    const top = offsetTop - scrollTop
 9    if (top <= viewPortHeight + 100) {
10        bottomToComment.style.visibility = "hidden";
11        bottomToComment.style.opacity = "0";
12    } else {
13        bottomToComment.style.visibility = "visible";
14        bottomToComment.style.opacity = "1";
15    }
16})
static/js/go-top.js
 1// go-top.js
 2let menu = document.getElementById('menu')
 3if (menu) {
 4    menu.scrollLeft = Number(localStorage.getItem("menu-scroll-position"));
 5    menu.onscroll = function () {
 6        localStorage.setItem("menu-scroll-position", menu.scrollLeft.toString());
 7    }
 8}
 9
10  
11
12window.onscroll = function () {
13    const goTopButton = document.getElementById("top-link");
14    if (document.body.scrollTop > 800 || document.documentElement.scrollTop > 800) {
15        goTopButton.style.visibility = "visible";
16        goTopButton.style.opacity = "1";
17    } else {
18        goTopButton.style.visibility = "hidden";
19        goTopButton.style.opacity = "0";
20    }
21};
22
23document.addEventListener('scroll', function (e) {
24    const readProgress = document.getElementById("pe-read-progress");
25    const scrollHeight = document.documentElement.scrollHeight;
26    const clientHeight = document.documentElement.clientHeight;
27    const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
28    readProgress.innerText = ((scrollTop / (scrollHeight - clientHeight)).toFixed(2) * 100).toFixed(0);
29})
  1. 删除原有回到顶部的按钮,或通过配置disableScrollToTop关闭(代码位于 footer.html
  2. 添加 right_aside.html 文件添加右侧边栏的代码
layouts/partials/right_aside.html
 1<aside>
 2 <!--右侧按钮-->
 3    <div class="right-side-btns">
 4        {{- /* Scroll To Comment */ -}}
 5        <script src="/js/go-comment.js"></script>
 6        <a href="#comments-title" class="right-side-btn" id="comments-link"
 7            style="visibility: visible; opacity: 1;padding: 0.6rem;">
 8            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 29.338 29.338" fill="currentColor">
 9                <path d="M27.184,1.605H2.156C0.967,1.605,0,2.572,0,3.76v17.572c0,1.188,0.967,2.155,2.156,2.155h13.543
10        l5.057,3.777c0.414,0.31,0.842,0.468,1.268,0.468c0.789,0,1.639-0.602,1.637-1.923v-2.322h3.523c1.188,0,2.154-0.967,2.154-2.155
11        V3.76C29.338,2.572,28.371,1.605,27.184,1.605z M27.34,21.332c0,0.085-0.068,0.155-0.154,0.155h-5.523v3.955l-5.297-3.956H2.156
12        c-0.086,0-0.154-0.07-0.154-0.155V3.759c0-0.085,0.068-0.155,0.154-0.155v0.001h25.029c0.086,0,0.154,0.07,0.154,0.155
13        L27.34,21.332L27.34,21.332z M5.505,10.792h4.334v4.333H5.505C5.505,15.125,5.505,10.792,5.505,10.792z M12.505,10.792h4.334v4.333
14        h-4.334V10.792z M19.505,10.792h4.334v4.333h-4.334V10.792z" />
15            </svg>
16        </a>
17        <script src="/js/go-top.js"></script>
18        <a href="#top" class="right-side-btn" id="top-link" style="text-align: center;">
19            <span id="pe-read-progress"></span>
20        </a>
21    </div>
22</aside>
  1. 添加CSS asset/css/extended/right_side.css 实现样式
asset/css/extended/right_side.css
 1:root {
 2    --text-color: #bdbdbd;
 3    --text-hover-color: #373737ac;
 4    --right-side-width: 300px;
 5}
 6.dark {
 7    --text-color: #bdbdbd;
 8    --text-hover-color: #373737ac;
 9}
10.right-side-btns,
11.right-side-btn {
12    display: flex;
13    flex-direction: column;
14    align-items: center;
15    justify-content: center;
16}
17.right-side-btns {
18    position: fixed;
19    right: 1rem;
20    z-index: 999;
21    bottom: 2rem;
22    gap: 1rem;
23}
24.right-side-btn {
25    width: 2.4rem;
26    height: 2.4rem;
27    padding: 0.3rem;
28    border-radius: 50%;
29    background: var(--tertiary);
30    color: var(--secondary);
31    transition: all 0.3s ease;
32    font-size: 0.9rem;
33}
34.right-side-btn:hover {
35    background-color: var(--text-color);
36    color: var(--text-hover-color);
37    outline: 0;
38}

作者信息栏

此部分代码在网上没找到样例可以抄,大部分都是靠着本人一边GPT一边理解自己实现的。 实现了一个可以悬浮在右侧边栏的框,展示头像、社交信息和最近更新的文章。

  1. 添加一个在 right-side.html 中的<aside>节点下添加以下代码
layouts/partials/right_aside.html
 1    <div class="author-info">
 2        <!-- 头像 -->
 3        <img src="/homepage.jpg" class="author-avatar" />
 4        <!-- 社交信息 -->
 5        <div>
 6            <div class="social-icons">
 7                {{- range site.Params.socialIcons }}
 8                <a href="{{ trim .url " " | safeURL }}" target="_blank" rel="noopener noreferrer me"
 9                    title="{{ (.title | default .name) | title }}">
10                    {{ partial "svg.html" . }}
11                </a>
12                {{- end }}
13            </div>
14         </div>
15        <!-- 最近更新的文章 -->
16        <div class="last-mod">
17            <div>
18                <div class="custom-divider"></div>
19                <div class="last-mod-title">最近更新</div>
20                <div class="custom-divider"></div>
21            </div>
22            <div class="last-mod-table">
23                {{ range first 3 (where .Site.RegularPages "Section" "posts").ByDate.Reverse }}
24                <a class="last-mod-item" href="{{ .RelPermalink }}">
25                    <div class="last-mod-item-tile">{{ .Title }} </div>
26                    <div class="last-mod-item-time"> ----- 于 {{.Lastmod.Format (default "January 2, 2006"
27                        site.Params.DateFormat)}} 由 {{ .Params.author }} 更新</div>
28                </a>
29                {{ end }}
30            </div>
31        </div>
32    </div>
  1. asset/css/extended/right_side.css 添加样式
asset/css/extended/right_side.css
 1.right-side {
 2    position: absolute;
 3    height: 100%;
 4    width: 25%;
 5    border-left: 1px solid var(--border);
 6    right: calc((var(--right-side-width) + var(--gap)) * -1);
 7    top: calc(var(--gap) * 2);
 8    width: var(--right-side-width);
 9}
10.author-info {
11    display: flex;
12    flex-direction: column; /* 设置主轴方向为纵向 */
13    justify-content: center; /* 在主轴上居中对齐 */
14    align-items: center; /* 在交叉轴上居中对齐 */
15    margin-top: 0px;
16    margin-left: 25px;
17    margin-right: 10px;
18    position: sticky;
19    top: var(--gap);
20    border: 2px solid var(--text-hover-color); /* 边框宽度为2px,样式为实线,颜色为黑色 */
21    box-shadow: 0 2px 4px var(--text-hover-color); /* 水平偏移0px,垂直偏移4px,模糊半径8px,颜色为黑色且透明度为0.1 */
22}
23
24
25.author-avatar {
26    height: 128px;
27    width: 128px;
28    margin-top: 20px;
29    border-radius: 50%;
30}
31
32.last-mod {
33    width: 90%;
34    margin-top: 10px;
35}
36
37.last-mod-table {
38    display: flex;
39    flex-direction: column;
40    align-items: center;
41    justify-content: center;
42    margin-top: 5px;
43}
44
45.last-mod-title {
46    text-align: center;
47    font-weight: bold;
48}
49.last-mod-item {
50    width: 100%;
51    display: 1;
52    font-size: 0.9rem;
53    border: 2px solid var(--text-hover-color);
54    border-radius: 10px;
55    padding: 4px;
56    padding-left: 7px;
57    padding-right: 7px;
58    margin-bottom: 10px ;
59    transition: transform 0.5s ease;
60}
61.last-mod-item:hover {
62    border: 2px solid var(--text-color);
63}
64.last-mod-item-time {
65    font-size: 0.7rem;
66    text-align: right;
67}
68.custom-divider {
69    width: 100%;
70    height: 1px;
71    background: repeating-linear-gradient(
72      to right,
73      #e0e0e0,
74      #e0e0e0 33%,
75      transparent 0,
76      transparent 67%,
77      #e0e0e0 0,
78      #e0e0e0 100%
79    );
80  }

代码显示优化

代码来自 tofuwine 的个性化主题,仅css略作修改:

tofuwine/PaperMod-PE
layouts/_default/_markup/render-codeblock.html
 1{{ $defaultFoldMod := .Page.Param "codeBlockFoldMode" | default false }}
 2
 3{{- $lang := .Type | default "text" }}
 4{{- $title := .Attributes.title | default "" }}
 5{{- $u_lang := .Attributes.lang | default ""  }}
 6{{- $hideLang := .Attributes.hide | default false  }}
 7{{ $foldMode := .Attributes.fold | default $defaultFoldMod }}
 8
 9{{- if and (.Attributes.force) (ne $u_lang "")  }}
10{{- $lang = $u_lang }}
11{{- end }}
12
13
14<div class="pe-code-block-wrap {{ if ne $foldMode "disable" }} pe-code-details {{ end }} {{ if not $foldMode }} open {{ end }} scrollable">
15    <div class="pe-code-block-header pe-code-details-summary">
16        <div class="pe-code-block-header-left">
17            {{ if ne $foldMode "disable" }}
18            <i class="arrow fas fa-chevron-right fa-fw pe-code-details-icon" aria-hidden="true"></i>
19            {{ end }}
20            {{ if not $hideLang }}
21            <span>
22            {{- if ne $u_lang "" }}
23            {{- $u_lang -}}
24            {{- else }}{{- $lang -}}{{- end }}
25            </span>
26            {{ end }}
27        </div>
28        <div class="pe-code-block-header-center">
29            <span>
30            {{- if ne $title "" }}
31            {{- $title }}
32            {{ end }}
33            </span>
34        </div>
35        <div class="pe-code-block-header-right">
36            {{ if ne $foldMode "disable" }}
37            <i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i>
38            {{ end }}
39            <button class="pe-code-copy-button">
40                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" class="pe-icon"><path fill="currentColor" fill-rule="evenodd" d="M7 5a3 3 0 0 1 3-3h9a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3h-2v2a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3v-9a3 3 0 0 1 3-3h2zm2 2h5a3 3 0 0 1 3 3v5h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-9a1 1 0 0 0-1 1zM5 9a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1v-9a1 1 0 0 0-1-1z" clip-rule="evenodd"></path></svg>
41            </button>
42        </div>
43    </div>
44    <div class="pe-code-details-content scrollable">
45        {{ highlight .Inner $lang .Options }}
46    </div>
47</div>
layouts/_default/_markup/render-codeblock-mermaid.html
1<pre class="mermaid">
2    {{- .Inner | safeHTML }}
3  </pre>
4  {{ .Page.Store.Set "hasMermaid" true }}
assets/css/extended/code.css
  1:root {
  2    --pe-primary-hover-color: #777777;
  3    /* 代码块标题色 */
  4    --pe-code-block-header-color: var(--primary);
  5    /* 代码块标题背景色 */
  6    --pe-code-block-header-bg-color: #ededed;
  7    /* 代码块文本颜色 */
  8    --pe-code-block-color: #979797;
  9    /* 代码块背景色 */
 10    --pe-code-block-bg-color: #f5f5f5;
 11    /* 代码块复制按钮字体颜色 */
 12    --pe-copy-code-color: #fff;
 13    /* 代码块复制按钮背景色 */
 14    --pe-copy-code-bg-color: #979797;
 15    --pe-scrollbar-bg-color: rgb(163, 163, 165);
 16    --pe-scrollbar-hover-bg-color: rgb(113, 113, 117);
 17    --copy-btn-hover-color: #4489f9;
 18    --code-color: #ff8a8a;
 19}
 20
 21.dark {
 22    /* 代码块标题色 */
 23    --pe-code-block-header-color: var(--primary);
 24    /* 代码块标题背景色 */
 25    --pe-code-block-header-bg-color: #20252B;
 26    /* 代码块文本颜色 */
 27    --pe-code-block-color: rgba(255, 255, 255, 0.7);
 28    /* 代码块背景色 */
 29    --pe-code-block-bg-color: #272C34;
 30
 31    --pe-copy-code-color: rgba(255, 255, 255, 0.7);
 32    --pe-copy-code-bg-color: #414244;
 33
 34    --pe-scrollbar-bg-color: rgb(113, 113, 117);
 35    --pe-scrollbar-hover-bg-color: rgb(163, 163, 165);
 36    --copy-btn-hover-color: #b3d0ff;
 37}
 38
 39.post-content a:hover {
 40    color: var(--copy-btn-hover-color);
 41}
 42
 43/* 代码样式 */
 44.post-content code {
 45    margin: unset;
 46    padding: .3rem .4rem;
 47    line-height: 1.5;
 48    background: var(--code-bg);
 49    border-radius: .5rem;
 50    font-size: 0.875em;
 51    font-family: Consolas, sans-serif;
 52    color: var(--code-color);
 53}
 54
 55/* 代码块样式 */
 56.pe-code-block-wrap {
 57    border-radius: var(--radius);
 58    margin: var(--content-gap) auto;
 59    background-color: var(--pe-code-block-header-bg-color);
 60    font-family: Consolas, sans-serif;
 61    overflow: hidden;
 62}
 63
 64.pe-code-block-header {
 65    display: flex;
 66    width: 100%;
 67    align-items: center;
 68    color: var(--pe-code-block-header-color);
 69    justify-content: space-between;
 70    padding: .4rem 1rem;
 71    font-size: 0.875rem;
 72}
 73
 74.pe-code-block-header-left {
 75    text-align: left;
 76    display: flex;
 77    align-items: baseline;
 78    gap: .2rem;
 79}
 80
 81.pe-code-block-header-center {
 82    text-align: center;
 83}
 84
 85.pe-code-block-header-right {
 86    line-height: 1rem;
 87    text-align: right;
 88    width: 2rem;
 89    display: flex;
 90    justify-content: flex-end;
 91}
 92
 93.post-content .highlight:not(table) {
 94    margin: unset;
 95    background: var(--pe-code-block-bg-color) !important;
 96    border-radius: unset;
 97}
 98
 99.post-content pre code {
100    background-color: var(--pe-code-block-bg-color) !important;
101    font-size: 0.875rem;
102    color: var(--pe-code-block-color);
103    border-radius: unset;
104}
105
106.pe-icon {
107    width: 1.6rem;
108    height: 1.6rem;
109}
110
111.copy-code:hover {
112    background: var(--pe-primary-hover-color);
113}
114
115.chroma .lnt {
116    padding: 0 0 0 1.2rem !important;
117}
118
119/* 滚动条 */
120.post-content :not(table) ::-webkit-scrollbar-thumb {
121    border: .2rem solid var(--pe-code-block-bg-color);
122    background: var(--pe-scrollbar-bg-color);
123}
124
125.post-content :not(table) ::-webkit-scrollbar-thumb:hover {
126    background: var(--pe-scrollbar-hover-bg-color);
127}
128
129.pe-code-details-content::-webkit-scrollbar {
130    width: .8rem;
131}
132
133.pe-code-details-content::-webkit-scrollbar-track {
134    background: var(--pe-code-block-bg-color); /* Background of the scrollbar track */
135}
136
137.pe-code-details-content::-webkit-scrollbar-thumb {
138    border: .2rem solid var(--pe-code-block-bg-color);
139    background: var(--pe-scrollbar-bg-color);
140}
141
142.pe-code-details-content::-webkit-scrollbar-thumb:hover {
143    background: var(--pe-scrollbar-hover-bg-color);
144}
145
146.pe-code-details-content::-webkit-scrollbar-corner {
147    background: var(--pe-code-block-bg-color);
148}
149
150table.lntable {
151    overflow-x: unset;
152}
153
154.pe-code-block-container pre {
155    margin: unset;
156}
157
158.pe-code-details .pe-code-details-summary:hover {
159    cursor: pointer;
160}
161
162.pe-code-details i.pe-code-details-icon {
163    color: var(--content);
164    -webkit-transition: transform 0.2s ease;
165    -moz-transition: transform 0.2s ease;
166    -o-transition: transform 0.2s ease;
167    transition: transform 0.2s ease;
168}
169
170.dark .pe-code-details i.pe-code-details-icon {
171    color: var(--content);
172}
173
174.pe-code-details .pe-code-details-content {
175    max-height: 0;
176    overflow-y: hidden;
177    -webkit-transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) -0.1s;
178    -moz-transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) -0.1s;
179    -o-transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) -0.1s;
180    transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) -0.1s;
181}
182
183.pe-code-details.open i.pe-code-details-icon {
184    -webkit-transform: rotate(90deg);
185    -moz-transform: rotate(90deg);
186    -ms-transform: rotate(90deg);
187    -o-transform: rotate(90deg);
188    transform: rotate(90deg);
189}
190
191.pe-code-details.open .pe-code-details-content {
192    max-height: 80vh;
193    -webkit-transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s;
194    -moz-transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s;
195    -o-transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s;
196    transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s;
197}
198
199.pe-code-details.scrollable .pe-code-details-content{
200    overflow: auto;
201}
202
203.scrollable {
204    overflow: auto;
205}
206
207.pe-code-details .fa-chevron-right:before {
208    content: "\f105";
209}
210
211.pe-code-details .fa-ellipsis-h:before {
212    content: "\f141";
213}
214
215.pe-code-details.open .fa-ellipsis-h:before {
216    content: "";
217}
218
219.pe-code-details .pe-code-copy-button {
220    display: none;
221}
222
223.pe-code-details.open .pe-code-copy-button {
224    display: inherit;
225}
226
227.pe-code-copy-button:hover {
228    color: var(--copy-btn-hover-color);
229}

最后,在 footer.htmlextend_footer.html 中引入脚本实现折叠动画

[ html ]
 1<script>
 2document.querySelectorAll('pre > code').forEach((codeBlock) => {
 3    const codeBlockWrap = codeBlock.closest('.pe-code-block-wrap')
 4    const copyButton = codeBlockWrap.querySelector('button')
 5
 6    function copyingDone() {
 7        copyButton.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="pe-icon"><path fill-rule="evenodd" clip-rule="evenodd" d="M18.0633 5.67375C18.5196 5.98487 18.6374 6.607 18.3262 7.06331L10.8262 18.0633C10.6585 18.3093 10.3898 18.4678 10.0934 18.4956C9.79688 18.5234 9.50345 18.4176 9.29289 18.2071L4.79289 13.7071C4.40237 13.3166 4.40237 12.6834 4.79289 12.2929C5.18342 11.9023 5.81658 11.9023 6.20711 12.2929L9.85368 15.9394L16.6738 5.93664C16.9849 5.48033 17.607 5.36263 18.0633 5.67375Z" fill="currentColor"></path></svg>'
 8        setTimeout(() => {
 9            copyButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" class="pe-icon"><path fill="currentColor" fill-rule="evenodd" d="M7 5a3 3 0 0 1 3-3h9a3 3 0 0 1 3 3v9a3 3 0 0 1-3 3h-2v2a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3v-9a3 3 0 0 1 3-3h2zm2 2h5a3 3 0 0 1 3 3v5h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-9a1 1 0 0 0-1 1zM5 9a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1v-9a1 1 0 0 0-1-1z" clip-rule="evenodd"></path></svg>'
10        }, 2000);
11    }
12
13    copyButton.addEventListener('click', (cb) => {
14        // 防止触发 details-summary 点击事件
15        cb.stopPropagation();
16        if ('clipboard' in navigator) {
17            navigator.clipboard.writeText(codeBlock.textContent).then(() => {
18                copyingDone();
19            })
20            return;
21        }
22        const range = document.createRange();
23        range.selectNodeContents(codeBlock);
24        const selection = window.getSelection();
25        selection.removeAllRanges();
26        selection.addRange(range);
27        try {
28            document.execCommand('copy');
29            copyingDone();
30        } catch (e) {
31        }
32        selection.removeRange(range);
33    });
34});
35
36let peCodeDetails = document.getElementsByClassName('pe-code-details')
37for (let element of peCodeDetails) {
38    const peCodeSummary = element.getElementsByClassName('pe-code-details-summary')[0];
39    if (peCodeSummary) {
40        peCodeSummary.addEventListener('click', () => {
41            if (element.classList.contains('open')) {
42                element.classList.remove('open');
43                element.classList.remove('scrollable');
44            } else {
45                element.classList.add('open');
46                setTimeout(() => {
47                    element.classList.add('scrollable');
48                }, 800);
49            }
50        }, false);
51    }
52}
53</script>

ShortCode拓展

Hugo可以通过添加ShortCode来拓展Markdown的渲染能力,类似于组件。 所有的ShortCode定义都可以按照在 layouts/shortcode 中添加 html 文件和在 assets/css/extended 下新增 css 文件的方式来实现。如需JS则在 static/js 中添加js并在html中引入。

使用方式:

[ markdown ]
1<shortcode>
2content
3</shortcode>

为了方便管理,我新建了一个shortcode目录用于存放shortcode的css代码,需要在 extend_head.html 中引入。引入代码如下:

layouts/shortcode/extended.html
1{{ $styles := resources.Match "css/extended/shortcode/*.css" }}
2{{ $style := $styles | resources.Concat "assets/css/shortcode.css" | minify | fingerprint }}
3<link rel="stylesheet" href="{{ $style.RelPermalink }}" integrity="{{ $style.Data.Integrity }}" crossorigin="anonymous">

admonition

代码来自 YazidLee/hugo-backup 仅略作修改

YazidLee/hugo-backup

添加以下文件

layoutsshortcodes/admonition.html
 1{{- $inner := .Inner | .Page.RenderString -}}
 2
 3{{- $iconMap := dict "note" "fas fa-pencil-alt fa-fw" -}}
 4{{- $iconMap  = dict "abstract" "fas fa-list-ul fa-fw" | merge $iconMap -}}
 5{{- $iconMap  = dict "info" "fas fa-info-circle fa-fw" | merge $iconMap -}}
 6{{- $iconMap  = dict "tip" "fas fa-lightbulb fa-fw" | merge $iconMap -}}
 7{{- $iconMap  = dict "success" "fas fa-check-circle fa-fw" | merge $iconMap -}}
 8{{- $iconMap  = dict "question" "fas fa-question-circle fa-fw" | merge $iconMap -}}
 9{{- $iconMap  = dict "warning" "fas fa-exclamation-triangle fa-fw" | merge $iconMap -}}
10{{- $iconMap  = dict "failure" "fas fa-times-circle fa-fw" | merge $iconMap -}}
11{{- $iconMap  = dict "danger" "fas fa-skull-crossbones fa-fw" | merge $iconMap -}}
12{{- $iconMap  = dict "bug" "fas fa-bug fa-fw" | merge $iconMap -}}
13{{- $iconMap  = dict "example" "fas fa-list-ol fa-fw" | merge $iconMap -}}
14{{- $iconMap  = dict "quote" "fas fa-quote-right fa-fw" | merge $iconMap -}}
15{{- $iconDetails := "fas fa-angle-right fa-fw" -}}
16
17{{- if .IsNamedParams -}}
18    {{- $type := .Get "type" | default "note" -}}
19    <div class="details admonition {{ $type }}{{ if .Get `open` | ne false }} open{{ end }}">
20        <div class="details-summary admonition-title">
21            <i class="icon {{ index $iconMap $type | default (index $iconMap "note") }}"></i>{{ .Get "title" | default (T $type) }}<i class="details-icon {{ $iconDetails }}"></i>
22        </div>
23        <div class="details-content">
24            <div class="admonition-content">
25                {{- $inner -}}
26            </div>
27        </div>
28    </div>
29{{- else -}}
30    {{- $type := .Get 0 | default "note" -}}
31    <div class="details admonition {{ $type }}{{ if .Get 2 | ne false }} open{{ end }}">
32        <div class="details-summary admonition-title">
33            <i class="icon {{ index $iconMap $type | default (index $iconMap "note") }}"></i>{{ .Get 1 | default (T $type) }}<i class="details-icon {{ $iconDetails }}"></i>
34        </div>
35        <div class="details-content">
36            <div class="admonition-content">
37                {{- $inner -}}
38            </div>
39        </div>
40    </div>
41{{- end -}}
assets/css/extended/shortcode/admonition.css
  1.admonition {
  2  position: relative;
  3  margin: 1rem 0;
  4  padding: 0 0.75rem;
  5  background-color: rgba(68, 138, 255, 0.1);
  6  border-left: 0.25rem solid #448aff;
  7  border-radius: var(--radius);
  8  overflow: auto;
  9}
 10
 11.admonition .admonition-title {
 12  font-weight: bold;
 13  margin: 0 -0.75rem;
 14  padding: 0.25rem 1.8rem;
 15  border-bottom: 1px solid rgba(68, 138, 255, 0.1);
 16  background-color: rgba(68, 138, 255, 0.25);
 17}
 18
 19.admonition.open .admonition-title {
 20  background-color: rgba(68, 138, 255, 0.1);
 21}
 22
 23.admonition .admonition-content {
 24  padding: 0.5rem 0;
 25}
 26
 27.admonition i.icon {
 28  font-size: 0.85rem;
 29  color: #448aff;
 30  position: absolute;
 31  top: 0.8rem;
 32  left: 0.4rem;
 33}
 34
 35.admonition i.details-icon {
 36  position: absolute;
 37  top: 0.6rem;
 38  right: 0.3rem;
 39}
 40
 41.admonition.note {
 42  border-left-color: #448aff;
 43}
 44
 45.admonition.note i.icon {
 46  color: #448aff;
 47}
 48
 49.admonition.abstract {
 50  border-left-color: #00b0ff;
 51}
 52
 53.admonition.abstract i.icon {
 54  color: #00b0ff;
 55}
 56
 57.admonition.info {
 58  border-left-color: #00b8d4;
 59}
 60
 61.admonition.info i.icon {
 62  color: #00b8d4;
 63}
 64
 65.admonition.tip {
 66  border-left-color: #00bfa5;
 67}
 68
 69.admonition.tip i.icon {
 70  color: #00bfa5;
 71}
 72
 73.admonition.success {
 74  border-left-color: #00c853;
 75}
 76
 77.admonition.success i.icon {
 78  color: #00c853;
 79}
 80
 81.admonition.question {
 82  border-left-color: #64dd17;
 83}
 84
 85.admonition.question i.icon {
 86  color: #64dd17;
 87}
 88
 89.admonition.warning {
 90  border-left-color: #ff9100;
 91}
 92
 93.admonition.warning i.icon {
 94  color: #ff9100;
 95}
 96
 97.admonition.failure {
 98  border-left-color: #ff5252;
 99}
100
101.admonition.failure i.icon {
102  color: #ff5252;
103}
104
105.admonition.danger {
106  border-left-color: #ff1744;
107}
108
109.admonition.danger i.icon {
110  color: #ff1744;
111}
112
113.admonition.bug {
114  border-left-color: #f50057;
115}
116
117.admonition.bug i.icon {
118  color: #f50057;
119}
120
121.admonition.example {
122  border-left-color: #651fff;
123}
124
125.admonition.example i.icon {
126  color: #651fff;
127}
128
129.admonition.quote {
130  border-left-color: #9e9e9e;
131}
132
133.admonition.quote i.icon {
134  color: #9e9e9e;
135}
136
137.admonition.note {
138  background-color: rgba(68, 138, 255, 0.1);
139}
140
141.admonition.note .admonition-title {
142  border-bottom-color: rgba(68, 138, 255, 0.1);
143  background-color: rgba(68, 138, 255, 0.25);
144}
145
146.admonition.note.open .admonition-title {
147  background-color: rgba(68, 138, 255, 0.1);
148}
149
150.admonition.abstract {
151  background-color: rgba(0, 176, 255, 0.1);
152}
153
154.admonition.abstract .admonition-title {
155  border-bottom-color: rgba(0, 176, 255, 0.1);
156  background-color: rgba(0, 176, 255, 0.25);
157}
158
159.admonition.abstract.open .admonition-title {
160  background-color: rgba(0, 176, 255, 0.1);
161}
162
163.admonition.info {
164  background-color: rgba(0, 184, 212, 0.1);
165}
166
167.admonition.info .admonition-title {
168  border-bottom-color: rgba(0, 184, 212, 0.1);
169  background-color: rgba(0, 184, 212, 0.25);
170}
171
172.admonition.info.open .admonition-title {
173  background-color: rgba(0, 184, 212, 0.1);
174}
175
176.admonition.tip {
177  background-color: rgba(0, 191, 165, 0.1);
178}
179
180.admonition.tip .admonition-title {
181  border-bottom-color: rgba(0, 191, 165, 0.1);
182  background-color: rgba(0, 191, 165, 0.25);
183}
184
185.admonition.tip.open .admonition-title {
186  background-color: rgba(0, 191, 165, 0.1);
187}
188
189.admonition.success {
190  background-color: rgba(0, 200, 83, 0.1);
191}
192
193.admonition.success .admonition-title {
194  border-bottom-color: rgba(0, 200, 83, 0.1);
195  background-color: rgba(0, 200, 83, 0.25);
196}
197
198.admonition.success.open .admonition-title {
199  background-color: rgba(0, 200, 83, 0.1);
200}
201
202.admonition.question {
203  background-color: rgba(100, 221, 23, 0.1);
204}
205
206.admonition.question .admonition-title {
207  border-bottom-color: rgba(100, 221, 23, 0.1);
208  background-color: rgba(100, 221, 23, 0.25);
209}
210
211.admonition.question.open .admonition-title {
212  background-color: rgba(100, 221, 23, 0.1);
213}
214
215.admonition.warning {
216  background-color: rgba(255, 145, 0, 0.1);
217}
218
219.admonition.warning .admonition-title {
220  border-bottom-color: rgba(255, 145, 0, 0.1);
221  background-color: rgba(255, 145, 0, 0.25);
222}
223
224.admonition.warning.open .admonition-title {
225  background-color: rgba(255, 145, 0, 0.1);
226}
227
228.admonition.failure {
229  background-color: rgba(255, 82, 82, 0.1);
230}
231
232.admonition.failure .admonition-title {
233  border-bottom-color: rgba(255, 82, 82, 0.1);
234  background-color: rgba(255, 82, 82, 0.25);
235}
236
237.admonition.failure.open .admonition-title {
238  background-color: rgba(255, 82, 82, 0.1);
239}
240
241.admonition.danger {
242  background-color: rgba(255, 23, 68, 0.1);
243}
244
245.admonition.danger .admonition-title {
246  border-bottom-color: rgba(255, 23, 68, 0.1);
247  background-color: rgba(255, 23, 68, 0.25);
248}
249
250.admonition.danger.open .admonition-title {
251  background-color: rgba(255, 23, 68, 0.1);
252}
253
254.admonition.bug {
255  background-color: rgba(245, 0, 87, 0.1);
256}
257
258.admonition.bug .admonition-title {
259  border-bottom-color: rgba(245, 0, 87, 0.1);
260  background-color: rgba(245, 0, 87, 0.25);
261}
262
263.admonition.bug.open .admonition-title {
264  background-color: rgba(245, 0, 87, 0.1);
265}
266
267.admonition.example {
268  background-color: rgba(101, 31, 255, 0.1);
269}
270
271.admonition.example .admonition-title {
272  border-bottom-color: rgba(101, 31, 255, 0.1);
273  background-color: rgba(101, 31, 255, 0.25);
274}
275
276.admonition.example.open .admonition-title {
277  background-color: rgba(101, 31, 255, 0.1);
278}
279
280.admonition.quote {
281  background-color: rgba(60, 60, 60, .1);
282}
283
284.admonition.quote .admonition-title {
285  border-bottom-color: rgba(60, 60, 60, 0.1);
286  background-color: rgba(60, 60, 60, 0.25);
287}
288
289.admonition.quote.open .admonition-title {
290  background-color: rgba(60, 60, 60, 0.1);
291}
292
293.admonition:last-child {
294  margin-bottom: 0.75rem;
295}
[ css ]
 1.details .details-summary:hover {
 2    cursor: pointer;
 3}
 4
 5.details .details-content {
 6    max-height: 0;
 7    overflow-y: hidden;
 8    -webkit-transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) -0.1s;
 9    -moz-transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) -0.1s;
10    -o-transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) -0.1s;
11    transition: max-height 0.8s cubic-bezier(0, 1, 0, 1) -0.1s;
12}
13
14.details.open .details-content {
15    max-height: 1200rem;
16    -webkit-transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s;
17    -moz-transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s;
18    -o-transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s;
19    transition: max-height 0.8s cubic-bezier(0.5, 0, 1, 0) 0s;
20}
21
22.details .details-icon {
23    color: var(--content);
24    -webkit-transition: transform 0.2s ease;
25    -moz-transition: transform 0.2s ease;
26    -o-transition: transform 0.2s ease;
27    transition: transform 0.2s ease;
28}
29
30[class=dark] .details .details-icon {
31    color: var(--content);
32}
33
34.details.open .details-icon {
35    -webkit-transform: rotate(90deg);
36    -moz-transform: rotate(90deg);
37    -ms-transform: rotate(90deg);
38    -o-transform: rotate(90deg);
39    transform: rotate(90deg);
40}

最后,在 footer.htmlextend_footer.html 中引入脚本实现折叠动画

[ html ]
 1<script>
 2let details = document.getElementsByClassName('details')
 3details = details || [];
 4for (let i = 0; i < details.length; i++) {
 5    let element = details[i]
 6    const summary = element.getElementsByClassName('details-summary')[0];
 7    if (summary) {
 8        summary.addEventListener('click', () => {
 9            element.classList.toggle('open');
10        }, false);
11    }
12}
13</script>

效果展示:

This is a tip
一个 技巧 横幅

界面显示优化

配置默认语言为中文

hugo.html 文件中添加配置 defaultContentLanguage: zh 可以汉化大部分主题的内容

日期格式显示优化

hugo.html 文件中的Param节点下添加配置 DateFormat: "2006-01-02" (go的时间配置必须要用这个日期,yyyy-MM-dd不行)

标签页添加词云

参考资料: https://blog.xlap.top/post/tech/wordcloud4hugo/#%E9%A1%B9%E7%9B%AE%E5%9C%B0%E5%9D%80 感谢开源作者和大佬代码,我直接抄了

  1. 下载 wordcloud2.js 拷贝到 static/js/ 目录下,下载地址
  2. 修改 head.html 引入js文件,新增以下代码:
layouts/partials/head.html
1{{- if eq .Section "tags"}}
2{{/* 标签云 */}}
3<link rel="stylesheet" href="/css/word-cloud.css"/>
4<script src="/js/wordcloud2.js"></script>
5{{- end }}
  1. 创建 staic/css/word-cloud.css 文件输入以下代码:
staic/css/word-cloud.css
 1.word-color:nth-child(7n + 1) {
 2    color: rgb(202, 110, 255);
 3  }
 4  .word-color:nth-child(7n + 2) {
 5    color: rgb(83, 110, 255);
 6  }
 7  .word-color:nth-child(7n + 3) {
 8    color: rgb(143, 253, 241);
 9  }
10  .word-color:nth-child(7n + 4) {
11    color: rgb(183, 255, 112);
12  }
13  .word-color:nth-child(7n + 5) {
14    color: rgb(255, 212, 126);
15  }
16  .word-color:nth-child(7n + 6) {
17    color: rgb(248, 140, 131);
18  }
19  .word-color:nth-child(7n + 7) {
20    color: rgb(104, 160, 255);
21  }
22  @keyframes word {
23    0% {
24      opacity: 0.5;
25    }
26    3% {
27      opacity: 1;
28    }
29    9% {
30      opacity: 1;
31    }
32    12% {
33      opacity: 0.5;
34    }
35    100% {
36      opacity: 0.5;
37    }
38  }
39
40  .word-animate {
41    animation-name: word;
42    animation-duration: 20s;
43    animation-iteration-count: infinite;
44    will-change: opacity;
45    opacity: 0.5;
46  }
47
48  .word-animate:nth-child(7n + 1) {
49    animation-delay: 0s;
50  }
51  .word-animate:nth-child(7n + 2) {
52    animation-delay: 3s;
53  }
54  .word-animate:nth-child(7n + 3) {
55    animation-delay: 6s;
56  }
57  .word-animate:nth-child(7n + 4) {
58    animation-delay: 9s;
59  }
60  .word-animate:nth-child(7n + 5) {
61    animation-delay: 12s;
62  }
63  .word-animate:nth-child(7n + 6) {
64    animation-delay: 15s;
65  }
66  .word-animate:nth-child(7n + 7) {
67    animation-delay: 18s;
68  }
  1. 创建 layouts/_default/terms.html 输入以下代码
layouts/_default/terms.html
 1{{- define "main" }}
 2{{- if .Title }}
 3<header class="page-header">
 4    <h1>{{ .Title }}</h1>
 5    {{- if .Description }}
 6    <div class="post-description">
 7        {{ .Description }}
 8    </div>
 9    {{- end }}
10</header>
11{{- end }}
12  
13<ul class="terms-tags">
14    <div id="sourrounding_div" style="width:100%;height:100%;min-height: 500px;">
15        <div id="tag-canvas"></div>
16    </div>
17    <script src="/js/wordcloud2.js"></script>
18    {{- range $key, $value := .Data.Terms.Alphabetical }}
19    {{ if eq "" ($.Scratch.Get "tagsMap") }}
20    {{ $.Scratch.Set "tagsMap" (slice (dict .Name .Count)) }}
21    {{ else }}
22    {{ $.Scratch.Add "tagsMap" (slice (dict .Name .Count)) }}
23    {{ end }}
24    {{- end }}
25    {{ $result := ($.Scratch.Get "tagsMap")}}
26    <span id="tag-temp" style="display:none">{{$result | jsonify }}</span>
27
28    <script>
29        //因为前期每个标签值比较小,帮X一个系数
30        var XISHU = 20;
31        //为了动态宽度
32        var div = document.querySelector("#sourrounding_div");
33        var canvas = document.querySelector("#tag-canvas");
34        canvas.style.width = div.offsetWidth + 'px';
35        canvas.style.height = div.offsetHeight + 'px';
36        var wordFreqData = document.querySelector("#tag-temp").innerHTML;
37        var jsonObj = JSON.parse(wordFreqData);
38        var arr = []
39        jsonObj.forEach(element => {
40            var key = Object.keys(element);
41            var itemArr = [key[0], element[key] * XISHU];
42            arr.push(itemArr);
43        });
44
45        //获取当前是暗色还是浅色
46        var isDark = document.body.className.includes("dark");
47        WordCloud(canvas, {
48            "list": arr,//或者[['各位观众',45],['词云', 21],['来啦!!!',13]],只要格式满足这样都可以
49            "shape": "cardioid", //形状 circle (default), cardioid (心型), diamond, square, triangle-forward, triangle, pentagon, and star.
50            "gridSize": 20, // 密集程度 数字越小越密集
51            "weightFactor": 1, // 字体大小=原始大小*weightFactor
52            "fontWeight": 'normal', //字体粗细
53            "fontFamily": 'Times, serif', // 字体
54            "color": isDark ? 'random-light' : 'random-dark', // 字体颜色 'random-dark' 或者 'random-light'
55            "backgroundColor": 'none', // 背景颜色
56            "classes": "tag-cloud-item word-color", //用于点击事件
57        });
58        canvas.addEventListener('wordcloudstop', function (e) {
59            //动画
60            setTimeout(() => {
61                var els = document.querySelectorAll(".word-color");
62                Array.from(els).forEach((el) => {
63                    console.log('动画', el)
64                    el.classList.add("word-animate")
65                })
66            }, 2000);
67            //点击
68            document.querySelectorAll('.tag-cloud-item').forEach(function (element) {
69                const text = element.innerHTML;
70                element.innerHTML = `<a href="/tags/${text}" style="color: inherit;">${text}</a>`;
71            });
72        });
73    </script>
74</ul>
75{{- end }}{{/* end main */ -}}

参数可以自行修改,可以关注 shapegridSize 等属性

url管理

hugo.yaml 中添加 permalinks.posts: "/:year/:month/:day/:slug/"archetypes/default.md 中添加 slug: 在每一篇博文中,自己管理slug

为文章添加过期提示

  1. 修改 single.html 文件中,在post-meta后面新增一个div块来展示该信息,代码如下:
layouts/_default/single.html
 1    {{- if not (.Param "hideMeta") }}
 2    <div class="post-meta">
 3      {{- partial "post_meta.html" . -}}
 4      {{- partial "translation_list.html" . -}}
 5      {{- partial "edit_post.html" . -}}
 6      {{- partial "post_canonical.html" . -}}
 7    </div>
 8    {{- end }}
 9
10    <!-- 添加提示框逻辑 -->
11    {{- $lastmod := .Lastmod }}
12    {{- $now := now }}
13    {{- $duration := $now.Sub $lastmod }}
14    {{- $daysAgo := $duration.Hours }}
15    {{- if ge $daysAgo (math.Mul 180 24) }}
16    {{- if eq site.Params.defaultTheme `dark` -}}
17    {{- $isDark := ".dark" }}
18    {{- end -}}
19    <div class="post-age-hint">
20      这篇文章最后更新已经超过180天了,内容可能已经过时。
21    </div>
  1. 为该div块定制样式,在 assets/css/extended 目录下新增 custom.css 文件,输入以下代码,颜色可以随自己喜欢调整
assets/css/extended/custom.css
 1:root {
 2    --post-age-hint-bg-color: #d4d4d4ac;
 3    --post-age-hint-color: #26b3fe;
 4}
 5
 6.dark {
 7    --post-age-hint-bg-color: #6d6c6cac;
 8    --post-age-hint-color: #ff7723;
 9}
10
11.post-age-hint {
12    background-color: var(--post-age-hint-bg-color);
13    /* 深灰色背景 */
14    color: var(--post-age-hint-color);
15    /* 浅灰色文字 */
16    padding: 10px;
17    border-left: 3px solid #ff6666;
18    /* 红色边框,醒目但不刺眼 */
19    margin: 20px 0;
20    border-radius: 5px;
21    /* 圆角边框 */
22}
23
24/* 添加一个过渡效果,鼠标悬停时改变边框颜色 */
25.post-age-hint:hover {
26    border-color: #ff9999;
27}

这段代码中,定义了两个变量用于适配不同主题,默认的白色主题,会使用root中的两个颜色,当切换到dark时,会使用.dark中定义的颜色