上一次折腾 Hugo 是去年年初的事情了,时隔接近两年,再次记录下 Hugo 跨版本升级的一些实践细节。
写在前面
为什么接近两年时间里,没有继续折腾 Hugo 呢?
- 我对网站模版功能没有变动诉求。这个模版虽然写于六年前,但是不论性能还是基础功能、以及在资源有限的老设备上运行,都没有什么问题。在三年前移植到 Hugo 模版后,除了为增强 Hugo 日志归档功能,添加了一个功能外,就没有变动了。
- Hugo 跨版本升级,总会出现一些 Break Changes 的问题。在 0.5x 版本和 0.6x 版本中,官网引入了一些导致 Crash 的问题,让我没有升级的兴趣。
那为什么有了这篇文章呢?主要也有两个原因:
- 最近业务上有静态网站诉求,使用 Hugo 0.78 构建了一个站点,目测后续还有更多的需求,所以需要深度使用踩踩坑。相比较零到一的站点,我的网站有上千篇内容,以及各种场景站点的模块,作为试验田非常合适。
- 为了统一软件技术栈,减少维护心智负担,只要新版本的软件在数据和功能处理方面没有问题,我一般会将其升级到新的稳定版本。
主要调整内容
因为之前使用 Hugo 模版功能,将一些场景的功能模块,以及这几个版本中 Hugo 变动了的接口都实现和封装了一遍,比如:页面布局模块、RSS、归档页面、多级文档目录、网站地图…
所以这里升级只需要调整一些简单的数据字段引用,或者调整下配置文件,最多是调整下 Nginx 配置,除了调试编写模版外,过程还是比较轻松的。
修正路由变化的独立页面
在升级到 0.78 之后,Hugo 配置文件中的 uglyurls = true
配置项会使得独立的页面路由发生一些变化,从原来的 /pageName
变为 /pageName.html
。
如果你想让页面 URL 保持和原来一致,可以考虑参考 内容组织管理 文档中的方案,在 Markdown 文档中添加下面的参数来声明页面的地址。
url = /page-url/
部分模版使用 Page 变量获取数据为空
在升级之后,有一部分模版页面使用 where
函数获取站点数据会出现异常。
{{ $Posts := where .Pages "Section" "post" }}
{{ range $Posts }}
<p>{{.Title}}</p>
{{end}}
这时可以尝试使用带有 $.Site
命名空间,或者更换方法为 RegularPages
来获取数据。
{{ $Posts := where $.Site.Pages "Section" "post" }}
{{ range $Posts }}
<p>{{.Title}}</p>
{{end}}
{{ $Posts := where .RegularPages "Section" "post" }}
{{ range $Posts }}
<p>{{.Title}}</p>
{{end}}
标签分类地址兼容
上篇文章中有提过对一些带有特殊符号的标签,做了 URL 兼容,避免它生成多级目录。
官方对于标签/分类的 URL 生成其实一直很摇摆,挨着时间顺序看以下讨论,可以发现官方原本的方向是朝着标准的网站实现去做的:
- https://github.com/gohugoio/hugo/issues/3577
- https://github.com/gohugoio/hugo/issues/4090
- https://github.com/gohugoio/hugo/pull/5519/files
- https://github.com/gohugoio/hugo/pull/5282
但是在有一天,一个老外反馈他使用 Hugo 做地理位置多级联动的站点,官方便对上面的实现方案产生了动摇,最终将 Hugo 的标签生成模式改成了下面这样:
并且单独开了一个帖子,讨论未来如何增强调整标签/分类的地址:
为了不"伤经动骨",单独编译一个定制版出来。这里可以使用 Nginx 处理链接兼容问题,让原来的 /tags/linux-mac.mac
这类只有两级目录的标签/分类转向到类似 /tags/linux/mac.html
的地址。
location = /tags/linux-mac.html {
return 301 https://$host/tags/linux/mac.html;
}
location ~* ^/tags/linux-mac/(page/.*)$ {
return 301 https://$host/tags/linux/mac/$1;
}
使用自定义模版输出 RSS
上篇文章中我有提过我使用自定义的模版替换了官方实现的 RSS 生成,所以在官方修改 RSS 输出实现后,也就幸免,省的折腾了。
调整客户端输出页面为服务端
之前生成日志归档,主要依赖前端脚本和 Hugo 模版,以及脚本生成的列表数据进行页面生成。
比如先使用工具生成类似下面年份或者月份的内容:
---
title: "2020年文章存档"
type: archives
draft: false
isCJKLanguage: true
outputs: [ "HTML"]
---
## [2020年11月](/archives/2020/11/)
- `01` [使用 Nginx 构建前端日志统计服务(打点采集)服务](/2020/11/01/use-nginx-to-build-a-front-end-log-statistics-service-service.html)
## [2020年10月](/archives/2020/10/)
- `31` [阿里云 IP 地理位置库(淘宝IP库)实践(后篇)](/2020/10/31/dockerize-aliyun-geoip-part-2.html)
- `30` [阿里云 IP 地理位置库(淘宝IP库)实践(前篇)](/2020/10/30/dockerize-aliyun-geoip-part-1.html)
- `04` [容器化 FRP 使用方案](/2020/10/04/frp-in-docker.html)
...
Hugo 模版调整为方便 JavaScript 使用的模式就行,将上面的内容,直接输出在页面中:
<h2 class="post-title"><a href="{{ .scope.Permalink }}" rel="bookmark">{{ .scope.Title }}</a></h2>
<div class="post-content">
<script type="text/plain" id="archives-data">{{ .scope.RawContent }}</script>
<div class="row all-archive-container" data-year-filter="{{ .year }}"></div>
<div class="row month-archive-content"></div>
<div class="row">
<div class="col-md-12">
<a class="footer-opt-link" href="/archives.html"><i class="fa fa-angle-left"></i>返回博客存档首页</a>
</div>
</div>
</div>
最后使用类似下面的模式拼合 HTML 片段,然后呈现在页面上:
...
function makeYearTemplate(years, isCurrentYear, hasFiltered) {
var tpl = [];
if (isCurrentYear) {
tpl.push('<div class="col-md-12 archive-year-item archive-year-current clearfix">');
} else {
tpl.push('<div class="col-md-12"><div class="row clearfix archive-year-item">');
}
for (var i = 0, j = years.length; i < j; i++) {
if (!isCurrentYear) tpl.push('<div class="col-xs-12 col-sm-3 col-md-3 col-lg-3">');
var data = getYearData(years[i]);
tpl.push(' <a href="/archives/' + data.id + '.html" class="archive-link-container">');
tpl.push(' <div class="image-container">');
tpl.push(' <img src="/asset/image/years/' + data.id + '.svg" alt="' + data.name + '年文章存档">');
tpl.push(' <div class="image-caption">');
tpl.push(' <div class="text">' + data.id + ' 年文章存档</div><div class="bg"></div>');
tpl.push(' </div>');
tpl.push(' </div>');
if (!hasFiltered) {
tpl.push(' <div class="text-container">');
tpl.push(' <h3>' + data.name + '</h3><p>' + data.desc + '<i class="fa fa-link"></i></p>');
tpl.push(' </div>');
}
tpl.push(' </a>');
if (!isCurrentYear) tpl.push('</div>');
}
if (isCurrentYear) {
tpl.push('</div>');
} else {
tpl.push('</div></div>');
}
return tpl.join('');
}
...
function main() {
var container = document.querySelector('.all-archive-container');
var dataContainer = document.getElementById('archives-data');
var filterYear = container.getAttribute('data-year-filter');
filterYear = filterYear ? parseInt(filterYear) : 0;
var filterMonth = container.getAttribute('data-month-filter');
filterMonth = filterMonth ? parseInt(filterMonth) : 0;
if (container) {
showMainPage(container, filterYear);
if (!dataContainer) return;
if (filterYear) {
var data = getSource(dataContainer);
return archiveList(data, filterYear);
}
}
}
这样做的好处是 Hugo 什么活都不用干,逻辑极其简单,其实就几个不到二十行的 HTML 模版,完全依赖外部脚本和浏览器中的客户端 JavaScript 生成,浏览器访问的时候,页面传输量也小。
但是劣势也很明显,内容无法被搜索引擎检索,页面第一次打开后会抖动一次,尤其是和其他直接服务端渲染的页面对比时,另外这个页面必须依赖客户端 JavaScript 执行正常。
而这套来自六年前的模版主题有一个隐藏的特点是“JavaScript Less Support”,即使客户端不支持运行 JavaScript,其实也不影响页面内容的浏览,所以这次调整的时候的想法之一就是把内容归档交付给 Hugo 来渲染,让这个“特点”完整起来。
按照上面的思路来做,需要编写一个 Hugo 模版,像是下面这样:
{{ $allowYearList := ($.Site.RegularPages.GroupByDate "2006" "desc") }}
{{ $archiveData := $.Site.Data.archive.years }}
{{ $archiveListData := $.Site.Data.archive.years.list }}
<div class="post-content">
<div class="row all-archive-container">
{{ $currentYear := first 1 $allowYearList }}
{{ range $currentYear }}
{{ $yearName := .Key }}
{{ range $archiveListData }}
{{ $dataId := string .id }}
{{ if eq $dataId $yearName }}
<div class="col-md-12 archive-year-item archive-year-current clearfix">
<a href="/archives/{{$dataId}}.html" class="archive-link-container">
<div class="image-container">
<img src="/asset/image/years/{{$dataId}}.svg" alt="{{.name}}年文章存档" />
<div class="image-caption">
<div class="text">{{$dataId}} 年文章存档</div><div class="bg"></div>
</div>
</div>
<div class="text-container">
<h3>{{.name}}</h3>
<p>{{.content}}<i class="fa fa-link"></i></p>
</div>
</a>
</div>
{{end}}
{{end}}
{{ end }}
{{ $YearCount := len $allowYearList }}
{{ $YearExcludeCount := 1}}
{{ $YearGroupLimit := 4}}
{{ $groupCount := math.Ceil (div (sub (float $YearCount) $YearExcludeCount) $YearGroupLimit ) }}
{{ range $gid := seq $groupCount }}
<div class="col-md-12" data-group-id="{{$groupCount}}">
<div class="row clearfix archive-year-item">
{{ range $yid, $sequence := seq $YearCount }}
{{ if gt $yid 0 }}
{{ $groupId := add (div (sub $yid $YearExcludeCount) $YearGroupLimit ) 1 }}
{{if eq $groupId $gid}}
{{ $yearName := $yid }}
{{ $yearData := index $archiveListData $yid }}
<div class="col-xs-12 col-sm-3 col-md-3 col-lg-3">
<a href="/archives/{{$yearData.id}}.html" class="archive-link-container">
<div class="image-container">
<img src="/asset/image/years/{{$yearData.id}}.svg" alt="二零一九年文章存档"/>
<div class="image-caption">
<div class="text">{{$yearData.id}}年文章存档</div>
<div class="bg"></div>
</div>
</div>
<div class="text-container">
<h3>{{$yearData.name}}</h3>
<p>{{$yearData.content}}<i class="fa fa-link"></i></p>
</div>
</a>
</div>
{{ end }}
{{ end }}
{{ end }}
</div>
</div>
{{ end }}
</div>
</div>
按照这样编写的模版,可以自动筛选适合展示的,以年份或者月份归档的文章,生成对应的页面,不光是简化了客户端 JavaScript 的实现,原本用来分析文章时间生成归档文件的逻辑也可以省略掉了。
最后
原本以为从 Hugo 0.20 升级到 0.50 ,性能提升已经接近瓶颈,没想到在 0.70 的版本里,Hugo 构建速度能够更进一步,更加期待接下来 Go 运行时和编译器的更重磅的性能优化了。
接下来我会试验使用 Hugo 创建几十万页面,并进行内容的快速更新迭代,或许结果会颠覆之前对于静态网站生成器的看法也说不定。
–EOF