起因

最近在用霞鹜文楷(LXGW WenKai),配合上PaperMod这种强调文字的主题,在观感上很不错,于是决定转到Hugo。Hugo的文档和资料不太容易找,中文支持也少,中间遇到不少麻烦,记录下解决过程,给遇到同样问题的人一些参考。

安装

  • GO:官网下载,一键安装。
  • Hugo:GitHub下载,添加环境变量。
1> hugo version
2hugo v0.114.0-9df2ec7988e5a217a14901cc76c0b7e76b2e9f02+extended windows/amd64 BuildDate=2023-06-19T17:01:43Z VendorInfo=gohugoio

主题 PaperMod

执行hugo new site <blog>创建博客目录,目录结构如下:

 1 archetypes/    # 内容模板文件
 2 assets/        # 静态资源
 3 content/       # 博客内容
 4 data/
 5 layouts/       # 网站生成模版
 6 public/        # 项目导出文件
 7 resources/
 8 static/        # 静态文件
 9 themes/        # 主题
10 hugo.toml      # 配置文件

默认不带主题,可以到Hugo官网主题淘一淘。本文使用PaperMod,将代码直接下载下来,放到themes/下。当然也可以git clone,但是themes/.git/影响博客源码的提交,以及对应的源码部署方式,也可能导致文末出现的CSS加载失败问题。

使用 hugo server 启动本地调试服务,访问http://localhost:1313/查看页面。使用hugo生成网页文件至public目录。

配置 hugo.toml(必看)

站点目录的配置优先级高,会覆盖主题中的配置。所有在主题文件夹中的改动,都可以先将它复制到站点目录对应文件夹下,再作其他修改。这样主题更新的时候,自定义改动就不会丢失。

配置文件支持.toml.yaml等。Hugo推荐.toml,但是PaperMod推荐.yaml。网上.toml的比较少,本文使用这种。配置项的具体含义,请查阅Hugo的官方文档PaperMod的配置wiki

以下为/hugo.toml配置参考:

  1# baseURL = 'http://localhost:1313/'
  2baseURL = 'https://blog.lordash.de'
  3languageCode = 'zh-cn'
  4title = '似水'
  5theme = 'PaperMod'
  6
  7cleanDestinationDir = true
  8enableEmoji = true              # 允许使用Emoji表情
  9enableInlineShortcodes = true   # 允许内联短码
 10enableRobotsTXT = true          # 允许爬虫抓取到搜索引擎
 11hasCJKLanguage = true           # 自动检测是否包含中文日文韩文
 12pagination.pagerSize = 15       # 每页文章数量
 13
 14# 语言设置
 15defaultContentLanguage = "zh"
 16
 17# 单语言,必须在此处,以下设置之前
 18[languages]
 19[languages.zh]
 20    languageName = "中文"
 21
 22[markup.goldmark.renderer]
 23    unsafe = true               # html标签
 24[markup.highlight]
 25    codeFences = true           # 代码框
 26    guessSyntax = true          # 猜测代码类型
 27    lineNos = true              # 显示行号
 28    lineNumbersInTable = false  # table分隔行号与代码
 29    noClasses = true            # 代码块style而非class
 30    style = "monokai"           # 配色方案
 31
 32# 菜单设置
 33[[menu.main]]
 34    name = "搜索"
 35    pre = "<i class='fa fa-search'></i>"
 36    weight = 960
 37    identifier = "search"
 38    url = "/search"
 39[[menu.main]]
 40    name = "文章"
 41    pre = "<i class='fa fa-list'></i>"
 42    weight = 970
 43    identifier = "posts"
 44    url = "/posts"
 45[[menu.main]]
 46    name = "专栏"
 47    pre = "<i class='fa fa-book'></i>"
 48    weight = 980
 49    identifier = "series"
 50    url = "/series"
 51[[menu.main]]
 52    name = "友链"
 53    pre = "<i class='fa fa-link'></i>"
 54    weight = 990
 55    identifier = "links"
 56    url = "/links"
 57[[menu.main]]
 58    name = "关于"
 59    pre = "<i class='fa fa-info-circle'></i>"
 60    weight = 1000
 61    identifier = "about"
 62    url = "/about"
 63
 64[[permalinks]]
 65    post = "/post/:section/:slug/"  # 链接格式
 66
 67# 搜索功能
 68[outputs]
 69    home = ["HTML", "RSS", "JSON"]
 70
 71# 主题设置
 72[params]
 73    env = "production"
 74    description = "Life is like a boat"
 75    author = "Lordash"
 76    keywords = ["中文博客","ACM竞赛题解","计算机代码编程","免费技术分享学习"]
 77    DateFormat = "2006-01-02"
 78    ShowCodeCopyButtons = true  # 代码复制按钮
 79    ShowFullTextinRSS = true    # RSS展示全文
 80    defaultTheme = "dark"       # 默认主题颜色
 81    hideSummary = false         # 隐藏摘要
 82    showtoc = true              # 显示目录
 83    tocopen = true              # 目录默认展开
 84    ShowPostNavLinks = true     # 显示上一篇/下一篇
 85    ShowBreadCrumbs = true      # 文章顶部面包屑导航
 86    # ShowRssButtonInSectionTermList = true
 87    comments = true             # 展示评论
 88    hideFooter = false          # 隐藏页脚信息
 89    # ShowAllPagesInArchive = true #
 90
 91# 左上角标签
 92[params.label]
 93    text = "Lordash's blog"
 94    # icon = "images/favicon-32x32-.png"
 95    # iconHeight = 36
 96
 97[params.assets]
 98    favicon = "images/favicon-16x16-.png"        # 浏览器标签图标
 99    favicon16x16 = "images/favicon-16x16-.png"   # 浏览器标签图标
100    favicon32x32 = "images/favicon-32x32-.png"   # 浏览器标签图标
101    disableHLJS = true  # 不使用highlight.js
102
103[params.profileMode]
104    enabled = true  # 个人主页模式
105    title = "似水"
106    subtitle = "Life is like a boat"
107    imageUrl = "https://s2.loli.net/2022/06/01/D6SzQ9Uc1dTFbKf.png"
108    imageWidth = "125"
109    imageHeight = "172"
110
111# 主页按钮
112[[params.profileMode.buttons]]
113    name = "技术"
114    url = "/post/tech"
115[[params.profileMode.buttons]]
116    name = "生活"
117    url = "/post/life"
118
119# 社交图标
120[[params.socialIcons]]
121    name = "github"
122    url = "https://github.com/GH1656409967"
123[[params.socialIcons]]
124    name = "QQ"
125    url = "http://wpa.qq.com/msgrd?v=3&uin=1656409967&site=qq&menu=yes"
126[[params.socialIcons]]
127    name = "neteasecloudmusic"
128    url = "https://music.163.com/#/user/home?id=270121274"
129[[params.socialIcons]]
130    name = "douban"
131    url = "https://www.douban.com/people/Lordash/"
132[[params.socialIcons]]
133    name = "email"
134    url = "mailto:1656409967@qq.com"
135[[params.socialIcons]]
136    name = "RSS"
137    url = "posts/index.xml"
138
139# fuse.js模糊搜索
140[params.fuseOpts]
141  isCaseSensitive = false   # 不区分大小写
142  shouldSort = true         # 搜索结果排序
143  location = 0
144  distance = 1000
145  threshold = 0.4
146  minMatchCharLength = 0
147  keys = ["title", "permalink", "summary", "content"]
148
149# 分类等级
150[taxonomies]
151    category = "categories"
152    tag = "tags"
153    series = "series"
154
155# 评论
156[params.twikoo]
157    version = "1.6.16"
158
159# 访客统计
160[params.busuanzi]
161    enable = true

默认中文 i18n

关键在于以下配置,且必须在其他配置上层,同理可以添加多语言。

1# 语言设置
2defaultContentLanguage = "zh"
3
4# 单语言,必须在此处,以下设置之前
5[languages]
6[languages.zh]
7    languageName = "中文"

自定义字体

正文采用霞鹜文楷(LXGW WenKai),代码采用Ubuntu Mono derivative Powerline。在/layouts/partials/extend_head.html中引入

1<link rel="stylesheet" href="https://cdn.staticfile.org/lxgw-wenkai-screen-webfont/1.6.0/lxgwwenkaiscreen.css" media="print" onload="this.media='all'">

同时,在/assets/css/extended/blank.css中配置

1body {
2    font-family: "LXGW WenKai Screen", sans-serif !important;
3}
4
5.post-content pre, code {
6    font-family: 'Ubuntu Mono derivative Powerline', sans-serif;
7    max-height: 40rem;
8}

菜单栏

菜单设置,根据权重weight排序。(折叠菜单和汉堡菜单,暂时没有好的适配方式,同时个人感觉也与主题不合,搁置。有了解的小伙伴烦请告知我一下)。

 1# 菜单设置
 2[[menu.main]]
 3    name = "搜索"
 4    pre = "<i class='fa fa-search'></i>"
 5    weight = 960
 6    identifier = "search"
 7    url = "/search"
 8[[menu.main]]
 9    name = "文章"
10    pre = "<i class='fa fa-list'></i>"
11    weight = 970
12    identifier = "posts"
13    url = "/posts"
14[[menu.main]]
15    name = "专栏"
16    pre = "<i class='fa fa-book'></i>"
17    weight = 980
18    identifier = "series"
19    url = "/series"
20[[menu.main]]
21    name = "友链"
22    pre = "<i class='fa fa-link'></i>"
23    weight = 990
24    identifier = "links"
25    url = "/links"
26[[menu.main]]
27    name = "关于"
28    pre = "<i class='fa fa-info-circle'></i>"
29    weight = 1000
30    identifier = "about"
31    url = "/about"

图标 Font Awesome

/layouts/partials/extend_head.html中引入

1<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">

搜索功能

执行hugo new search.md创建搜索页面,修改front-matter标题

1---
2title: "搜索"
3---

同时必须配置以下内容,参考文档

1# 搜索功能
2[outputs]
3    home = ["HTML", "RSS", "JSON"]

侧边目录

参考Hugo博客目录放在侧边 | PaperMod主题。修改/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            {{- if (.Param "UseHugoToc") }}
 13            {{- .TableOfContents -}}
 14            {{- else }}
 15            {{- $largest := 6 -}}
 16            {{- range $headers -}}
 17            {{- $headerLevel := index (findRE "[1-6]" . 1) 0 -}}
 18            {{- $headerLevel := len (seq $headerLevel) -}}
 19            {{- if lt $headerLevel $largest -}}
 20            {{- $largest = $headerLevel -}}
 21            {{- end -}}
 22            {{- end -}}
 23
 24            {{- $firstHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers 0) 1) 0)) -}}
 25
 26            {{- $.Scratch.Set "bareul" slice -}}
 27            <ul>
 28                {{- range seq (sub $firstHeaderLevel $largest) -}}
 29                <ul>
 30                    {{- $.Scratch.Add "bareul" (sub (add $largest .) 1) -}}
 31                    {{- end -}}
 32                    {{- range $i, $header := $headers -}}
 33                    {{- $headerLevel := index (findRE "[1-6]" . 1) 0 -}}
 34                    {{- $headerLevel := len (seq $headerLevel) -}}
 35
 36                    {{/* get id="xyz" */}}
 37                    {{- $id := index (findRE "(id=\"(.*?)\")" $header 9) 0 }}
 38
 39                    {{- /* strip id="" to leave xyz, no way to get regex capturing groups in hugo */ -}}
 40                    {{- $cleanedID := replace (replace $id "id=\"" "") "\"" "" }}
 41                    {{- $header := replaceRE "<h[1-6].*?>((.|\n])+?)</h[1-6]>" "$1" $header -}}
 42
 43                    {{- if ne $i 0 -}}
 44                    {{- $prevHeaderLevel := index (findRE "[1-6]" (index $headers (sub $i 1)) 1) 0 -}}
 45                    {{- $prevHeaderLevel := len (seq $prevHeaderLevel) -}}
 46                    {{- if gt $headerLevel $prevHeaderLevel -}}
 47                    {{- range seq $prevHeaderLevel (sub $headerLevel 1) -}}
 48                    <ul>
 49                        {{/* the first should not be recorded */}}
 50                        {{- if ne $prevHeaderLevel . -}}
 51                        {{- $.Scratch.Add "bareul" . -}}
 52                        {{- end -}}
 53                        {{- end -}}
 54                        {{- else -}}
 55                        </li>
 56                        {{- if lt $headerLevel $prevHeaderLevel -}}
 57                        {{- range seq (sub $prevHeaderLevel 1) -1 $headerLevel -}}
 58                        {{- if in ($.Scratch.Get "bareul") . -}}
 59                    </ul>
 60                    {{/* manually do pop item */}}
 61                    {{- $tmp := $.Scratch.Get "bareul" -}}
 62                    {{- $.Scratch.Delete "bareul" -}}
 63                    {{- $.Scratch.Set "bareul" slice}}
 64                    {{- range seq (sub (len $tmp) 1) -}}
 65                    {{- $.Scratch.Add "bareul" (index $tmp (sub . 1)) -}}
 66                    {{- end -}}
 67                    {{- else -}}
 68                </ul>
 69                </li>
 70                {{- end -}}
 71                {{- end -}}
 72                {{- end -}}
 73                {{- end }}
 74                <li>
 75                    <a href="#{{- $cleanedID -}}" aria-label="{{- $header | plainify -}}">{{- $header | safeHTML -}}</a>
 76                    {{- else }}
 77                <li>
 78                    <a href="#{{- $cleanedID -}}" aria-label="{{- $header | plainify -}}">{{- $header | safeHTML -}}</a>
 79                    {{- end -}}
 80                    {{- end -}}
 81                    <!-- {{- $firstHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers 0) 1) 0)) -}} -->
 82                    {{- $firstHeaderLevel := $largest }}
 83                    {{- $lastHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers (sub (len $headers) 1)) 1) 0)) }}
 84                </li>
 85                {{- range seq (sub $lastHeaderLevel $firstHeaderLevel) -}}
 86                {{- if in ($.Scratch.Get "bareul") (add . $firstHeaderLevel) }}
 87            </ul>
 88            {{- else }}
 89            </ul>
 90            </li>
 91            {{- end -}}
 92            {{- end }}
 93            </ul>
 94            {{- end }}
 95        </div>
 96    </details>
 97</div>
 98</aside>
 99<script>
100    let activeElement;
101    let elements;
102    window.addEventListener('DOMContentLoaded', function (event) {
103        checkTocPosition();
104
105        elements = document.querySelectorAll('h1[id],h2[id],h3[id],h4[id],h5[id],h6[id]');
106         // Make the first header active
107         activeElement = elements[0];
108         const id = encodeURI(activeElement.getAttribute('id')).toLowerCase();
109         document.querySelector(`.inner ul li a[href="#${id}"]`).classList.add('active');
110     }, false);
111
112    window.addEventListener('resize', function(event) {
113        checkTocPosition();
114    }, false);
115
116    window.addEventListener('scroll', () => {
117        // Check if there is an object in the top half of the screen or keep the last item active
118        activeElement = Array.from(elements).find((element) => {
119            if ((getOffsetTop(element) - window.pageYOffset) > 0 &&
120                (getOffsetTop(element) - window.pageYOffset) < window.innerHeight/2) {
121                return element;
122            }
123        }) || activeElement
124
125        elements.forEach(element => {
126             const id = encodeURI(element.getAttribute('id')).toLowerCase();
127             if (element === activeElement){
128                 document.querySelector(`.inner ul li a[href="#${id}"]`).classList.add('active');
129             } else {
130                 document.querySelector(`.inner ul li a[href="#${id}"]`).classList.remove('active');
131             }
132         })
133     }, false);
134
135    const main = parseInt(getComputedStyle(document.body).getPropertyValue('--article-width'), 10);
136    const toc = parseInt(getComputedStyle(document.body).getPropertyValue('--toc-width'), 10);
137    const gap = parseInt(getComputedStyle(document.body).getPropertyValue('--gap'), 10);
138
139    function checkTocPosition() {
140        const width = document.body.scrollWidth;
141
142        if (width - main - (toc * 2) - (gap * 4) > 0) {
143            document.getElementById("toc-container").classList.add("wide");
144        } else {
145            document.getElementById("toc-container").classList.remove("wide");
146        }
147    }
148
149    function getOffsetTop(element) {
150        if (!element.getClientRects().length) {
151            return 0;
152        }
153        let rect = element.getBoundingClientRect();
154        let win = element.ownerDocument.defaultView;
155        return rect.top + win.pageYOffset;
156    }
157</script>
158{{- end }}

/assets/css/extended/blank.css中配置

 1:root {
 2    --nav-width: 1380px;
 3    --article-width: 720px;
 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    /*滚动条*/
69    background: var(--border);
70    border: 7px solid var(--theme);
71    border-radius: var(--radius);
72}
73
74.toc li ul {
75    margin-inline-start: calc(var(--gap) * 0.5);
76    list-style-type: none;
77}
78
79.toc li {
80    list-style: none;
81    font-size: 0.95rem;
82    padding-bottom: 5px;
83}
84
85.toc li a:hover {
86    color: var(--secondary);
87}

Shortcode

参考来写一些好玩的 Hugo 短代码吧

缩写

新建/layouts/shortcodes/abbr.html,内容如下:

1<abbr title="{{ .Get "title" }}">{{ .Get "text" }}</abbr>

使用方法(去掉'#'):

1{#{< abbr title="达拉崩巴斑得贝迪卜多比鲁翁" text="达拉崩巴" >}}
达拉崩巴

折叠

新建/layouts/shortcodes/detail.html,内容如下:

1<details>
2    <summary>{{ (.Get 0) | markdownify }}</summary>
3    {{ .Inner | markdownify }}
4</details>

使用方法(去掉'#'):

1{#{< detail "点击展开" >}}
2    hello world!
3{#{< /detail >}}
点击展开 hello world!

音乐

新建/layouts/shortcodes/music.html,内容如下:

 1<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/aplayer/dist/APlayer.min.css">
 2<style type="text/css">.dark .aplayer .aplayer-body{background-color:#212121}.dark .aplayer .aplayer-info{border-top-color:#212121}.dark .aplayer.aplayer-withlist .aplayer-info{border-bottom-color:#5c5c5c}.dark .aplayer.aplayer-fixed .aplayer-list{border-color:#5c5c5c}.dark .aplayer .aplayer-info .aplayer-music .aplayer-author,.dark .aplayer .aplayer-info .aplayer-music .aplayer-title{color:#fff}.dark .aplayer .aplayer-info .aplayer-controller .aplayer-time{color:#eee}.dark .aplayer .aplayer-info .aplayer-controller .aplayer-time .aplayer-icon path{fill:#eee}.dark .aplayer .aplayer-list{background-color:#212121}.dark .aplayer .aplayer-list::-webkit-scrollbar-thumb{background-color:#999}.dark .aplayer .aplayer-list::-webkit-scrollbar-thumb:hover{background-color:#bbb}.dark .aplayer .aplayer-list li{color:#fff;border-top-color:#666}.dark .aplayer .aplayer-list li:hover{background:#4e4e4e}.dark .aplayer .aplayer-list li.aplayer-list-light{background:#6c6c6c}.dark .aplayer .aplayer-list li .aplayer-list-author,.dark .aplayer .aplayer-list li .aplayer-list-index{color:#ddd}.dark .aplayer .aplayer-lrc{text-shadow:-1px -1px 0 #666}.dark .aplayer .aplayer-lrc:before{background:-moz-linear-gradient(top,#212121 0,rgba(33,33,33,0) 100%);background:-webkit-linear-gradient(top,#212121,rgba(33,33,33,0));background:linear-gradient(180deg,#212121,rgba(33,33,33,0))}.dark .aplayer .aplayer-lrc:after{background:-moz-linear-gradient(top,rgba(33,33,33,0) 0,rgba(33,33,33,.8) 100%);background:-webkit-linear-gradient(top,rgba(33,33,33,0),rgba(33,33,33,.8));background:linear-gradient(180deg,rgba(33,33,33,0),rgba(33,33,33,.8))}.dark .aplayer .aplayer-lrc p{color:#fff}.dark .aplayer .aplayer-miniswitcher{background:#484848}.dark .aplayer .aplayer-miniswitcher .aplayer-icon path{fill:#eee}</style>
 3<script src="https://cdn.jsdelivr.net/npm/aplayer/dist/APlayer.min.js"></script>
 4<script src="https://cdn.jsdelivr.net/npm/meting@2.0.1/dist/Meting.min.js"></script>
 5
 6{{ if .IsNamedParams }}
 7    <meting-js
 8      id="{{ .Get "id" }}"
 9      server="{{ .Get "server" }}"
10      type="{{ .Get "type" }}"
11      fixed="{{ if .Get "fixed" }}{{ .Get "fixed" }}{{ else }}false{{ end }}"
12      mini="{{ if .Get "mini" }}{{ .Get "mini" }}{{ else }}false{{ end }}"
13      autoplay="{{ if .Get "autoplay" }}{{ .Get "autoplay" }}{{ else }}false{{ end }}"
14      loop="{{ if .Get "loop" }}{{ .Get "loop" }}{{ else }}none{{ end }}"
15      theme="{{ if .Get "autoplay" }}{{ .Get "autoplay" }}{{ else }}#255579{{ end }}"
16      volume="{{ if .Get "volume" }}{{ .Get "volume" }}{{ else }}0.6{{ end }}"
17      prelosd="{{ if .Get "prelosd" }}{{ .Get "prelosd" }}{{ else }}auto{{ end }}"
18      mutex="{{ if .Get "mutex" }}{{ .Get "mutex" }}{{ else }}true{{ end }}"
19      list-folded="{{ if .Get "list-folded" }}{{ .Get "list-folded" }}{{ else }}true{{ end }}">
20    </meting-js>
21{{ end }}

使用方法(去掉'#'):

1{#{< music id="560183743" type="song" server="netease" >}}

Bilibili

新建/layouts/shortcodes/bilibili.html,内容如下:

 1{{ $vid := (.Get 0) }}
 2{{ $videopage := default 1 (.Get 1) }}
 3{{ $basicQuery := querify "page" $videopage "high_quality" 1 "danmaku" 1 "as_wide" 1}}
 4{{ $videoQuery := "" }}
 5
 6{{ if strings.HasPrefix (lower $vid) "av" }}
 7    {{ $videoQuery = querify "aid" (strings.TrimPrefix "av" (lower $vid)) }}
 8{{ else if strings.HasPrefix (lower $vid) "bv" }}
 9    {{ $videoQuery = querify "bvid" $vid }}
10{{ else }}
11    <p>Bilibili 视频av号或BV号错误!</p>
12    <p>当前视频av或BV号:{{ $vid }}, 视频分P:{{ $videopage }}</p>
13{{ end }}
14
15<style type="text/css">
16    .video-wrapper {
17        position: relative;
18        overflow: hidden;
19        margin: auto;
20        padding-bottom: 66%;
21        width: 100%;
22        height: 0;
23        text-align: center
24    }
25
26    .video-wrapper iframe {
27        position: absolute;
28        top: 0;
29        left: 0;
30        width: 100%;
31        height: 100%
32    }
33</style>
34
35<div class="video-wrapper">
36    <iframe src="https://player.bilibili.com/player.html?{{ $basicQuery | safeURL }}&{{ $videoQuery | safeURL }}"
37        scrolling="no" frameborder="no" framespacing="0" allowfullscreen="true">
38    </iframe>
39</div>

使用方法(去掉'#'

1{#{< bilibili BV1pX4y1R7d6 >}}

代码块

代码高亮

Hugo使用Chroma,PaperMod使用highlight.js,本文采用Hugo自带方案。

 1[markup.highlight]
 2    codeFences = true           # 代码框
 3    guessSyntax = true          # 猜测代码类型
 4    lineNos = true              # 显示行号
 5    lineNumbersInTable = false  # table分隔行号与代码
 6    noClasses = true            # 代码块style而非class
 7    style = "monokai"           # 配色方案
 8
 9[params.assets]
10    disableHLJS = true  # 不使用highlight.js

代码复制

注意lineNumbersInTable设置为true时,长代码块的行号部分会出现多余的滚动条,并且不同步;设置为false时,点击代码块的复制按钮又会连行号一起复制。

本文处理后一种情况,关键在于通过F12找出行号和代码部分的区别,然后修改复制按钮的执行过程。

可以发现在配置中开启noClasses = true之后(经过网友else提醒,还须关闭pygmentsUseClasses: false),代码块中的<span>标签都使用内嵌的style样式,但是代码部分的顶层会有一个不包含样式的<span>

所以在/layouts/partials/footer.html中,找到对于复制按钮点击事件的监听,修改if里面的内容如下:

 1    copybutton.addEventListener('click', (cb) => {
 2        if ('clipboard' in navigator) {
 3            // 不包含样式的span的内容拼接起来,就是代码块的内容
 4            let x = codeblock.getElementsByTagName("span");
 5            let noLineNumContent = "";
 6            for (i = 0; i < x.length; i++) {
 7                if (!x[i].style.display && !x[i].style.color)
 8                    noLineNumContent += x[i].textContent;
 9            }
10            navigator.clipboard.writeText(noLineNumContent);
11            copyingDone();
12            return;
13        }
14    ...

代码块折叠

目前方案比较简陋,抄一个和复制相同样式的展开按钮,用于控制代码块的最大高度,实现一种“不完全展开”的折叠。

layouts\partials\footer.html中添加代码块的折叠按钮:

 1<script>
 2    document.querySelectorAll('pre > code').forEach((codeblock) => {
 3        const container = codeblock.parentNode.parentNode;
 4
 5        const unfoldbtn = document.createElement('button');
 6        unfoldbtn.classList.add('unfoldbtn');
 7        unfoldbtn.innerHTML = '展开';
 8
 9        unfoldbtn.addEventListener('click', (cb) => {
10            if (container.firstChild.firstChild.classList.contains('unfold')) {
11                container.firstChild.firstChild.classList.remove('unfold');
12                unfoldbtn.innerHTML = '展开';
13            } else {
14                container.firstChild.firstChild.classList.add('unfold');
15                unfoldbtn.innerHTML = '折叠';
16            }
17        });
18
19        if (container.classList.contains("highlight")) {
20            container.appendChild(unfoldbtn);
21        }
22    });
23</script>

/assets/css/extended/blank.css中配置:

 1.highlight:hover {
 2    .unfoldbtn {
 3        display: block;
 4    }
 5}
 6
 7.unfoldbtn {
 8    display: none;
 9    position: absolute;
10    top: 4px;
11    right: 18px;
12    color: rgba(255, 255, 255, .8);
13    background: rgba(78, 78, 78, .8);
14    border-radius: var(--radius);
15    padding: 0 5px;
16    font-size: 14px;
17    user-select: none;
18}
19
20code {
21    max-height: 200px;  /* 折叠后最大高度 */
22}
23
24.unfold {
25    max-height: none;
26}
27
28.copy-code {
29    right: 58px;
30}

数学公式 KaTeX

PaperMod未整合,但文档中提到了做法。

新建/layouts/partials/math.html,复制粘贴KaTeX提供的自动渲染模板

 1<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css" integrity="sha384-GvrOXuhMATgEsSwCs4smul74iXGOixntILdUW9XmUC6+HX0sLNAK3q71HotJqlAn" crossorigin="anonymous">
 2<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.js" integrity="sha384-cpW21h6RZv/phavutF+AuVYrr+dA8xD9zs6FwLpaCct6O9ctzYFfFr4dgmgccOTx" crossorigin="anonymous"></script>
 3<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.8/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              {left: '\\(', right: '\\)', display: false},
13              {left: '\\[', right: '\\]', display: true}
14          ],
15          // • rendering keys, e.g.:
16          throwOnError : false
17        });
18    });
19</script>

然后在/layouts/partials/extend_head.html中添加:

1<!-- KaTeX -->
2{{ if or .Params.math .Site.Params.math }}
3{{ partial "math.html" . }}
4{{ end }}

在需要开启LaTeX渲染的文章front-matter中添加:

1math: true

评论系统 Twikoo

本文使用Twikoo作为评论系统,后台的部署参考Twikoo文档,或按照视频教程一步步完成即可。

前端部分参考Hugo博客添加Twikoo评论。新建/layouts/partials/comments.html,添加以下内容:

 1<div>
 2    <div class="pagination__title">
 3        <span class="pagination__title-h" style="font-size: 20px;">💬评论</span>
 4        <hr />
 5    </div>
 6    <div id="tcomment"></div>
 7    <script src="https://cdn.staticfile.org/twikoo/{{ .Site.Params.twikoo.version }}/twikoo.all.min.js"></script>
 8    <script>
 9        twikoo.init({
10            envId: "",  // 这里填写自己的envId
11            el: "#tcomment",
12            lang: 'zh-CN',
13            region: 'ap-shanghai',
14            path: window.TWIKOO_MAGIC_PATH||window.location.pathname,
15        });
16    </script>
17</div>

添加配置

1# 评论
2[params.twikoo]
3    version = "1.6.16"  # 这个版本号要自己手动修改,和twikoo的版本号要对得上

访客统计 busuanzi

访客统计采用不蒜子,参考Hugo添加不蒜子Busuanzi站点访问量与阅读量统计,在/layouts/partials/extend_head.html中引入:

1<!-- busuanzi -->
2{{- if .Site.Params.busuanzi.enable -}}
3    <script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script>
4    <meta name="referrer" content="no-referrer-when-downgrade">
5{{- end -}}

站点底部显示总访问量与访客数,修改/layouts/partials/footer.html,在footer标签中添加:

1<!-- busuanzi -->
2{{ if .Site.Params.busuanzi.enable -}}
3<span id="busuanzi_container_site_pv">
4    <i class="fa fa-eye"></i><span id="busuanzi_value_site_pv"></span>
5</span>
6<span id="busuanzi_container_site_uv">
7    <i class="fa fa-user"></i><span id="busuanzi_value_site_uv"></span>
8</span>
9{{- end -}}

每篇文章阅读量,在PaperMod主题中,修改/layouts/partials/post_meta.html,在末尾添加:

1<!-- busuanzi -->
2{{ if .Site.Params.busuanzi.enable -}}
3    &nbsp;·&nbsp;
4    <span id="busuanzi_container_page_pv">本文阅读量<span id="busuanzi_value_page_pv"></span>次</span>
5{{- end }}

最后在站点配置中添加:

1# 访问统计
2[params.busuanzi]
3    enable = true

部署

以Vercel为例,部署方式有至少两种。一种是上传博客源码,部署时选择Hugo,在线生成网页文件并部署;另一种则是本地生成网页文件,部署时选择Other,也就是GitHub Pages的方式。本文采用后者,具体过程不再赘述。

CSS加载失败

使用 hugo server 启动本地调试服务,访问http://localhost:1313/时看起来很正常,推送到GitHub上,使用GitHub Pages或Vercel部署的页面却加载不出CSS。

正常页面 CSS加载失败

按下F12,切换到控制台,可以看到提示如下。大意是CSS文件的SHA-256校验失败,所以无法加载。

1Failed to find a valid digest in the 'integrity' attribute for resource 'https://blog-d1lytax8a-lordash.vercel.app/assets/css/stylesheet.94301bb9792e5b60c04e4187a47605d05c85a2062102b81ada42fe7d0cd0aec1.css' with computed SHA-256 integrity 'DtzRH2bXNjGH5kpyxdinsAaB3zwGHAorYyxEe0JoY9I='. The resource has been blocked.

失败原因

简单搜索下,有说可能的原因是Cloudflare的速度/优化/Auto Minify功能改动文件导致;也有直接修改/layouts/partials/head.html生成过程,去掉integrity的。

以上两种情况,在折腾 Hugo & PaperMod 主题找到了好的方法:

  • Cloudflare 关闭的方法:速度 - 优化 - Auto Minify。
  • 在 Hugo 中关闭的方法:
    1[params.assets]
    2    disableFingerprinting = true
    

本文未做以上修改。在对本地及GitHub上的CSS文件进行SHA-256校验时,发现提交GitHub后的文件就已经不一致了,可以猜测是Git提交时有改动,此时,很容易就联想到行尾序列(行结束符)的问题。

假设你在Windows上使用Git上传代码,Git会在你提交时自动的把行结束符CRLF转化成LF,而在拉取代码时把LF转化成CRLF。查看Git配置:

1git config --global -l

关闭自动转换行尾序列功能

1git config --global core.autocrlf false

重新提交网页文件(最好是先删除),再次部署即可。