Hugo主题开发记录
前两年都在用WordPress搭建博客,中途因为换主机供应商搬了一次家,试了好几个插件才顺利完成打包下载和上传。搬完家又发现新主机的图片上传速度很慢,因为每次写月记的时候会放很多图,觉得不太方便,不过这家供应商胜在主机和域名都免费,所以老博客就在这边一直挂着了(虽然以后大概不会再更新了,但毕竟当初修改了好多东西还引入了PJAX,关了有点舍不得TT)。后来尝试用NotionNext搭了一个站点,但不太会改主题,兜兜转转最后还是决定用Hugo来搭建静态博客。
之前在WordPress用的主题是Ashe Blog,在Hugo这边没找到类似风格的主题,所以打算参照Ashe的大致样式来定制一个Hugo主题。上班的时候总是最想装修博客的时候()由于是从Hugo的默认模版开始手搓主题,需要改的地方有很多,目测是一项耗时很久的工作,决定在每天上班摸鱼的时候能改一点算一点。
另近一年来感觉到自己越来越难以专注地完成(指达到能commit的程度)一项事务,虽然以前大概也有点这样,不过上班之后表现得更明显了。不知道是思维方式变了,还是可支配的时间很少而想做的事太多,总是一件事做到一半就去做另一件事,因此越来越需要to-do list来提醒自己需要完成哪些事项。
站点配置
购买域名+Vercel部署
Vercel自定义域名配置流程(之前为了NotionNext配过一次,具体流程有点忘了,所以再总结一下……
- 购买域名,并在vercel为项目添加域名
- 在cloudflare为域名添加vercel提供的CNAME和A记录(并删除原有记录)
- 前往域名供应商管理后台,将nameserver改成cloudflare提供的地址(不过这样vercel会提示不建议使用反向代理,用起来感觉没太大影响
设置Hugo版本
应该是站点搭建期初期该完成的设置工作,但我直到开始写Hugo短代码才发现了这个问题……(淡淡
在使用短代码时,本地构建的站点可以正常渲染页面,但部署到Vercel会出现以下错误信息:
1[13:08:03.469] Running "vercel build"
2[13:08:04.458] Vercel CLI 42.2.0
3[13:08:05.581] Building sites … Total in 4 ms
4[13:08:05.581] Error: Error building site: "/vercel/path0/content/posts/daily/site-build.md:28:27": failed to extract shortcode: template for shortcode "spoiler" not found
5[13:08:05.586] Error: Command "hugo --gc" exited with 255
6[13:08:05.806]
7[13:08:08.671] Exiting build container
到处找原因,看到项目设置中的预设框架是Other
(当初导入项目的时候没有选Hugo
就用了默认的……
将项目框架更改为Hugo
之后,日志中多了一行信息。这里Hugo的版本比我本地测试环境用的落后很多,猜测短代码无法运作可能是因为Vercel使用的Hugo版本不对。
1[14:10:12.345] Running "vercel build"
2[14:10:12.794] Vercel CLI 42.2.0
3[14:10:13.503] Installing Hugo version 0.58.2
4[14:10:14.139] Building sites … Total in 2 ms
5[14:10:14.140] Error: Error building site: "/vercel/path0/content/posts/daily/site-build.md:28:27": failed to extract shortcode: template for shortcode "spoiler" not found
在项目设置界面中添加新的环境变量HUGO_VERSION
,指定Hugo版本,与本地开发环境保持一致。
重新部署并查看build log,可以看到Hugo版本更改成功,短代码的问题也解决了。
[14:15:24.492] Running "vercel build"
[14:15:24.949] Vercel CLI 42.2.0
[14:15:25.528] Installing Hugo version 0.138.0
[14:15:26.601] Start building sites …
图床配置
选用Cloudflare R2作为图床,有10GB的免费容量(不过也需要先绑定一种付款方式后才可以使用),操作步骤如下:
- 创建存储桶
- 在储存桶设置页面中添加自定义域。我的博客域名是
alanone.top
,选择了media.alanone.top
这个子域名来连接存储桶。 - 为储存桶创建API,保存该API对应的访问密钥ID(Access Key ID)和机密访问密钥(Secret Access Key)。Cloudflare还会提供一个“为S3客户端使用管辖权地特定的终结点”链接。
使用PicGo作为图片上传客户端:
- 原生的PicGo不支持Amazon S3图床,因此需要先安装S3插件。
- 在S3图床设置页面填写Cloudflare R2 API的密钥ID和访问密钥,储存桶名和上传文件路径。在“自定义节点(endpoint)”中填写Cloudflare提供的S3终结点。
- 最后在储存桶中设置“自定义输出URL模版“,例如
https://media.alanone.top/{path}
,就可以开始使用了。关于上传文件路径和自定义输出URL中的{path}
、{fileName}
等占位符使用方法,在S3插件的文档中有详细说明。
基础功能
章节目录TOC
用{{ .TableOfContents }}
生成章节目录,配合js代码实现对当前浏览内容对应的标题增加高亮显示效果。
<script>
document.addEventListener("DOMContentLoaded", function () {
const tocLinks = document.querySelectorAll(".toc a");
const headings = Array.from(tocLinks)
.map(link => document.getElementById(link.getAttribute("href").substring(1)))
.filter(Boolean);
const activateLink = (id) => {
tocLinks.forEach(link => {
if (link.getAttribute("href") === `#${id}`) {
link.classList.add("active");
} else {
link.classList.remove("active");
}
});
};
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
activateLink(entry.target.id);
}
});
}, {
rootMargin: "0px 0px -60% 0px", // 提前激活(偏移视窗)
threshold: 0.1
});
headings.forEach(h => observer.observe(h));
});
</script>
增加About页面
在content
目录下增加about.md
,存放本文内容,在frontmatter中填写type = 'about'
。创建layouts/about/single.html
,用来定义about页面的样式。目前只写了简单的控制宽度+居中显示。
{{ define "main" }}
<div class="post-content" style="width: 60%; margin: 0 auto;">
<div class="article">
{{ .Content }}
</div>
</div>
{{ end }}
还试了一下只在frontmatter中写layout = 'about'
,然后配合layouts/about.html
定义样式。不清楚为什么这样会fallback到别的模版,如果放到layouts/_default/about.html
就可以了……现在还不太了解hugo寻找模版时的优先级,之后有空的话再看一下文档吧。
站内搜索
从Hugo文档中列出了一些搜索工具,尝试使用其中的开源组件Pagefind。
运行命令:npx pagefind --site public
,对站点public
目录下的内容建立索引文件。完成后会出现以下提示。
Note: Pagefind doesn't support stemming for the language zh-cn.
Search will still work, but will not match across root words.
Pagefind的文档对此做了说明,目前无法在搜索框内对输入内容进行分词,因此检索结果的精确率和召回率会受到影响。
每次文章内容发生变化后,都需要运行一次Pagefind命令来创建新的索引文件。修改Vercel项目的构建命令可以自动完成这一步骤。最简单的方式是在Vercel项目的Settings -> Build and Deployment
菜单中修改Build Command
:在hugo
构建站点之后,接着运行Pagefind的索引命令。
hugo --gc && npx pagefind --site public
接下来为站点创建一个搜索页面,类似上文中about页面的添加方式,在frontmatter中添加layout = 'search'
。创建layouts/_default/search.html
,引入Pagefind提供的默认UI PagefindUI
。
{{ define "main" }}
<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
<script src="/pagefind/pagefind-ui.js"></script>
<div id="search" class="search-container"></div>
<script>
window.addEventListener('DOMContentLoaded', (event) => {
new PagefindUI({
element: "#search",
showSubResults: true
});
});
</script>
{{ end }}
另外,我希望Pagefind只对每一篇文章的标题和正文内容进行检索,从而避免检索结果出现重复,e.g. 文章视图的标题与列表视图的标题。修改文章页面的渲染模版_default/single.html
,为需要Pagefind检索的元素加上data-pagefind-body
。(试验了一下发现如果加在更外层的div
元素上会检索不到)
{{ define "main" }}
<div class="content" style="gap: 2rem;">
<div class="post-content">
<h1 data-pagefind-body>{{ .Title }}</h1>
<div class="article" data-pagefind-body>
{{ .Content }}
</div>
</div>
<aside class="toc">
{{ .TableOfContents }}
</aside>
</div>
{{ end }}
第一次使用的时候,发现返回每条检索结果都只有十几个字符,查阅文档了解到Pagefind的UI有许多可配置的参数,修改其中的excerptLength
就可以控制显示的文本长度。基本的效果已经达成了,接下来就是根据个人喜好继续调整样式。
图片缩放
想为文章中插入的图片用了medium-zoom,然后
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/medium-zoom.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/style.min.css" rel="stylesheet">
<script>
const images = Array.from(document.querySelectorAll(".post-content img"));
images.forEach(img => {
mediumZoom(img, {
margin: 0, /* The space outside the zoomed image */
scrollOffset: 40, /* The number of pixels to scroll to close the zoom */
container: null, /* The viewport to render the zoom in */
template: null, /* The template element to display on zoom */
background: 'rgba(0, 0, 0, 0.8)'
});
});
</script>
后来又找到了zooming
参阅文档
样式修改
字体设置
在尝试了很多字体之后,最终选择了寒蝉端黑宋。
代码块样式设置
Hugo中内置了一套Chroma配色方案,官方文档中也有配色方案展示。
目前比较喜欢hrdark这个配色方案。参考Hugo文档,配色方案的设置有两种方法:
-
在配置文件中设置预设的方案(
noClasses
默认为true
)[markup] [markup.highlight] noClasses = true style = 'hrdark'
除了
style
和noClasses
之外,这里还可以设置其他参数来统一控制是否显示代码行号等行为。如果想在更细粒度上自定义单个代码块的渲染效果,可以直接在Markdown的代码块上附带这些参数,具体参考Hugo文档中的语法高亮。常用的有:显示行号
{linenos=inline}
,高亮显示指定行{hl_lines=["16-18",26]}
。 -
在配置文件中设置
noClasses = false
(该属性用于控制是否使用内联式css),然后引入css文件。使用hugo命令生成css文件:
hugo gen chromastyles --style=hrdark > syntax.css
将css文件移动到
static/css
目录下,并引入:<link rel="stylesheet" href="/css/syntax.css">
感觉hrdark配色方案中括号等字符的颜色太深了,所以在原有配色方案的基础上调整了一下。hugo命令生成的css文件自带注释,找到目标类别修改即可,例如括号这类字符属于.chroma .p
,高亮代码行属于.chroma .hl
。此外,需要注意的是在修改完syntax.css
后进行本地测试时,可能需要强制刷新浏览器页面,避免发生因存在缓存而导致设置不生效的情况。
/* LiteralNumberOct */ .chroma .mo { color:#a6be9d }
/* Operator */ .chroma .o { color:#f2747e }
/* OperatorWord */ .chroma .ow { color:#f2747e }
/* Punctuation */ .chroma .p { color: #6c6b70 }
不过不知道为什么代码有了高亮,但代码块的背景颜色没有被设置成预期的样子,也许是在哪里被覆盖了,所以暂时先手动写一下样式:
.highlight pre {
background-color: #F7F9FA;
border: 2px solid;
border-color: #ecf1f7;
border-radius: 6px;
padding: 1em;
overflow-x: auto;
}
防剧透短代码
增加防剧透效果:inline效果测试
顺便测试一下为单个段落添加遮罩的效果。这是一个段落,这是一个段落,这是一个段落,这是一个段落,这是一个段落,这是一个段落,这是一个段落,这是一个段落,这是一个段落,这是一个段落,这是一个段落,这是一个段落。
一开始我在shortcodes目录下的spoiler.html
中只写了以下代码:
<span class="spoiler">{{ .Inner | markdownify }}</span>
这样的写法在inline使用短代码的情况下正常,但如果在markdown里对一个单独的段落运用短代码,这部分内容生成html后依然只是一个span,没有自动加上<p>
,因此这一段的文本样式会和文章其他段落不同(在没有特意调整成相同样式的情况下)。
尝试了几种方法试图完成span到p的转换却均失败之后,发现不如直接新创建一个spoiler-block.html
短代码,然后直接在这里加上<p>
标签,<p><span class="spoiler">{{ .Inner | markdownify }}</span></p>
。感觉应该有更好的方法,总之先暂时这样达到了预期效果。
链接样式
目前只改了每篇文章中的链接,加了鼠标悬浮时从左到右出现下划线的动态效果,像这样。文章列表页面的标题链接样式还没想好。
a {
color: #6190c0;
text-decoration: none;
transition: color 0.3s ease;
position: relative;
}
a::before {
content: '';
position: absolute;
bottom: 0;
left: 0;
height: 2px;
width: 100%;
background: #58a1dd;
transform: scaleX(0); // 简单的过渡动画
transform-origin: left;
transition: transform 0.2s ease-in-out;
}
a:hover {
color: #58a1dd;
}
a:hover::before {
transform: scaleX(1);
}
默认情况下点击文章中的链接时,会直接在当前标签页中跳转到新页面,感觉这样在访问外部链接时不太方便。
使用Hugo的render-link
hook,可以在渲染Markdown时自动为链接加上target="_blank"
,从而达到在新标签页中打开链接的效果。
{{- $u := urls.Parse .Destination -}}
<a href="{{ .Destination | safeURL }}"
{{- with .Title }} title="{{ . }}"{{ end -}}
{{- if $u.IsAbs }} target="_blank" rel="noopener noreferrer external"{{ end -}}
>
{{- with .Text }}{{ . }}{{ end -}}
</a>
再给链接后面加一个小箭头(这里写6-8行内容的时候换行了,要注意使用-
,如果不加的话渲染出的链接标题会与周围的文本之间有空格。
{{- $u := urls.Parse .Destination -}}
<a href="{{ .Destination | safeURL }}"
{{- with .Title }} title="{{ . }}"{{ end -}}
{{- if $u.IsAbs }} target="_blank" rel="noopener noreferrer external"{{ end -}}
>
{{- with .Text -}}
{{ . }}{{ if $u.IsAbs }}<span aria-hidden="true" class="external-link-indicator">↗</span>{{ end }}
{{- end -}}
</a>
.external-link-indicator { //小箭头的css
font-size: 0.75em;
margin-left: 0.25em;
vertical-align: super;
}
以下是待办事项
随便写一下以免自己忘了
主页
想加一个新的主页,跳转之后再显示文章列表,嗯需要思考一下怎么设计
分页
做分页组件,或者用列表展示一部分,剩下的点击链接跳转
归档页面
- 放在侧边栏?单独页面?
- 获取年份列表 ok
- 获取当年的文章列表 + url跳转 ok
- 组织方式:在
content
目录下创建archive
, - 关于config中的
permalinks
参数:例如archive = "/archive/:year/"
,将从markdown文件的front matter中读取date属性中的年份,然后形成url,于是当前page的url与实际文件名无关。 - 自动生成脚本
- 组织方式:在
- 样式设计
- 每年列表展示
- 所有年份列表展示
- 组件样式
文章meta data
- tag展示(研究一下taxonomy,还没搞懂是干嘛的)
- 上一篇、下一篇
- 字数计算
其他
- 友链
- 添加sns icon
- iframe嵌套 链接notion页面
- 碎碎念随记模块
- 展柜
- 深色模式配色方案
- 移动端适配:这个以后再说吧
内容迁移
- NotionNext
写作tips
自定义章节id:{#year2025_2}