上一次折腾 Hugo 是去年年初的事情了,时隔接近两年,再次记录下 Hugo 跨版本升级的一些实践细节。

写在前面

为什么接近两年时间里,没有继续折腾 Hugo 呢?

  1. 我对网站模版功能没有变动诉求。这个模版虽然写于六年前,但是不论性能还是基础功能、以及在资源有限的老设备上运行,都没有什么问题。在三年前移植到 Hugo 模版后,除了为增强 Hugo 日志归档功能,添加了一个功能外,就没有变动了。
  2. Hugo 跨版本升级,总会出现一些 Break Changes 的问题。在 0.5x 版本和 0.6x 版本中,官网引入了一些导致 Crash 的问题,让我没有升级的兴趣。

那为什么有了这篇文章呢?主要也有两个原因:

  1. 最近业务上有静态网站诉求,使用 Hugo 0.78 构建了一个站点,目测后续还有更多的需求,所以需要深度使用踩踩坑。相比较零到一的站点,我的网站有上千篇内容,以及各种场景站点的模块,作为试验田非常合适。
  2. 为了统一软件技术栈,减少维护心智负担,只要新版本的软件在数据和功能处理方面没有问题,我一般会将其升级到新的稳定版本。

主要调整内容

因为之前使用 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 生成其实一直很摇摆,挨着时间顺序看以下讨论,可以发现官方原本的方向是朝着标准的网站实现去做的:

但是在有一天,一个老外反馈他使用 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