本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2019年02月01日 统计字数: 3679字 阅读时间: 8分钟阅读 本文链接: https://soulteary.com/2019/02/01/upgrade-hugo-across-versions.html ----- # Hugo 跨版本升级 使用 Hugo 一年半了,终于有了升级的动力。趁着最近事情不多,着手搞定了这个事情,记录下来分享给需要的同学。 ## 背景 在这一年半里,我一直使用着的是老版本:`v0.20.7` ,运行非常稳定,写完文章 `Git Push` 后,GitLab Runner 自动更新预览地址,浏览没问题就可以一键发布了。 但是下面两个问题让我有了升级的想法。 ### 构建速度随着内容增多变慢 去年十月,在[网站架构简化](https://soulteary.com/2018/10/14/site-optimization-record.html)之后,我的完整发布编译时间从 **1分钟** 进入了 **40s** 的阶段,但是随着内容的膨胀、编译时间越来越慢了,可以看到不少发布时间变长。 ![类似这种编译时间超过45s不在少数](https://attachment.soulteary.com/2019/02/01/more-and-more-slow.png) 看着 Hugo 升级 Golang 版本,**重构优化速度变的越来越快,不免心动。** ### 需要维护两套不同版本的站点 一月初将博客主题重构之后,分享给了公司技术团队博客使用,考虑可维护性,新站点使用的是最新版本 `0.53` ,因为技术团队站点功能更简单,所以升级过程中不兼容的部分其实并不多。 但是这个站点,因为自定义了“年月日”格式的归档,以及使用的是老版本的模板查找逻辑,生成页面链接也不完全兼容,所以直接升级是不行的。 可以预期的是,随着使用时间越来越长,这两个站点的差异会越来越大,**为了可维护性,必须将这两个站点使用的 Hugo 版本统一**。 ## 梳理主要问题 1. 官方支持 RSS 文件直接输出,是否还需要自定义站点 RSS 文件? 2. 官方直接提供压缩能力,是否足够替换 Pipeline 中定制的压缩服务? 3. 页面模板查找逻辑、模板语法、站点配置文件变更,现有模板无法直接使用。 4. 分类标签系统扁平化,不再支持树形层级嵌套,链接兼容如何处理? 下面我来逐个击破。 ## Hugo RSS 解决方案 官方支持了 RSS 格式的输出,只要在 `layouts` 根目录创建一个文件即可,`index.rss.xml`,模板可以自定义,参考官方文档 。 另外官方生成文档,默认会输出正确的 XML Version,所以可以检查并删除己配置的文档模板中下面的内容。 ```xml ``` 但是这样会有两个额外的问题,第一个是额外生成了许多不需要生成的“订阅源”,像是下面这样。 ```bash find public/**/*.xml public/tags/标签A/index.xml public/tags/标签A/index.xml public/tags/标签C/index.xml public/topics/code/index.xml public/topics/funny/index.xml public/topics/index.xml public/topics/life/index.xml public/topics/share/index.xml public/topics/website/index.xml ``` 公司的技术团队博客可以保留这个功能,但是我个人一来更新频率没有那么高,二来我希望订阅源唯一可控,所以这些多余的内容我是要干掉的。 第二个问题是官方 RSS 输出内容不支持自定义路径,你的订阅地址就只能是下面这样: ```plain 网站地址/index.xml ``` 使用老版本的 RSS 方案,创建一个 `/feed` ,然后放置自定义的 RSS 模板,你会发现生成内容,仅支持该目录之下的文章… ORZ 如果你有类似的需求,这里更好的方案是“禁用官方RSS生成能力”、“自定义RSS模板”,可以做到按照你的需求在你期望的路径生成你期望数量的 RSS 内容。 首先是禁用官方RSS生成能力,在站点 `config.toml` 配置文件中添加下面的内容: ```Toml disableKinds= ["RSS"] ``` 如果你有定义 `output` 格式,并包含 RSS 定义,也需要删除该内容。 ```diff [outputs] -page = [ "HTML", "RSS" ] +page = [ "HTML" ] ``` 接着分别创建 `layouts/feed/index.html` 和 `content/feed/index.md` 两个文件。 模板文件内容可以参考: ```xml {{ `` | safeHTML }} {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} - {{ end }}{{ .Site.Title }}{{ end }} {{ .Site.BaseURL }}feed/ {{ .Site.Title }}最近更新内容。{{ with .Site.LanguageCode }} {{.}}{{end}}{{ with .Site.Author.email }} {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Author.email }} {{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}{{end}}{{ with .Site.Copyright }} {{.}}{{end}}{{ if not .Date.IsZero }} {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}{{ end }} hourly 1 hugo {{ range first 10 (where .Site.Pages "Type" "post") }} {{ .Title }} {{ .Permalink }} {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }} {{``|safeHTML}} {{``|safeHTML}} {{ .Permalink }} {{``|safeHTML}} {{``|safeHTML}} {{ end }} ``` 页面数据文件,示例文件: ```plain --- title: "Rss Feed" author: "soulteary" date: "2019-01-24" type: feed draft: false isCJKLanguage: true outputs: [ "HTML" ] --- ``` 再次执行 `hugo` 进行站点生成,会发现生成的页面内容已经大幅减少: ```plain 自定义之前: | EN +------------------+------+ Pages | 4419 Paginator pages | 594 Non-page files | 0 Static files | 197 Processed images | 0 Aliases | 1796 Sitemaps | 1 Cleaned | 0 自定义之后: | EN +------------------+------+ Pages | 3285 Paginator pages | 594 Non-page files | 0 Static files | 197 Processed images | 0 Aliases | 1796 Sitemaps | 1 Cleaned | 0 ``` 最后别忘记通过构建脚本,将生成文件进行重命名。 ```bash mv feed/index.html feed/index.xml ``` ## 更好的Hugo页面压缩能力 在使用 Hugo 版本和之前的压缩模式进行对比,发现 Hugo 压缩确实效率高不少,添加压缩参数 `--minify` 执行 Hugo ,生成时间几乎没有变化,还能省下 GitLab Pipeline 调用 Job 过程中的时间损耗,真的是太赞了。 但是压缩结果完全使用了 HTML 宽松模式,所有的 HTML Tag 属性都失去了引号,像是下面这样。 ```html / ``` 个人倾向使用相对严格的模式进行页面结构编写,即使是程序生成的代码也是如此,因为一旦要进行调试,相对严格的标准,可以消除很多歧义,减少不必要的调试时间。 由于老版本不支持 `--minify` 参数,所以我使用 `Node.js` 简单写了一个脚本,用于替换页面内的空白内容,程序处理内容比较多,贴一下主要替换逻辑供参考。 ```js const trimTags = (s) => s.replace(/>\s+<').replace(/>(\s+\n|\r)/g, '>'); const content = trimTags(content); ``` 如果你和我一样,定制了代码高亮,有更复杂的 HTML 结构,那么还需要额外处理一下代码,避免出现 `ReDoS` 问题。 ## 模板配置相关处理 首先贴出我的站点主题的 `layouts` 目录结构(部分),它也代表了网站的整个抽象逻辑。 ```plain layouts ├── 404.html ├── _default │   ├── baseof.html │   ├── list.html │   ├── section.html │   ├── summary.html │   ├── tag.html │   ├── topics.html │   └── topics.terms.html ├── about │   └── single.html ├── about-site │   └── single.html ├── archives │   ├── list.html │   └── single.html ├── booklist │   └── single.html ├── contact │   └── single.html ├── feed │   └── single.html ├── index.html ├── index.redir ├── links │   └── single.html ├── partials │   ├── bloc │   │   ├── content │   │   │   ├── badges.html │   │   │   ├── comments.html │   │   │   ├── content.html │   │   │   ├── navigation.html │   │   │   ├── pagination.html │   │   │   ├── sidebar.html │   │   │   └── summary.html │   ├── loop.html │   ├── modules │   │   ├── footer │   │   │   ├── index.html │   │   │   └── script.html │   │   ├── header │   │   │   ├── custom.html │   │   │   ├── index.html │   │   │   ├── link-style.html │   │   │   ├── meta-robots.html │   │   │   ├── meta.html │   │   │   └── script.html │   │   ├── meta │   │   │   ├── page-home-meta.html │   │   │   ├── page-topic-meta.html │   │   │   ├── post-meta-bottom.html │   │   │   └── post-meta-top.html │   │   ├── sidebar │   │   │   └── index.html │   │   └── site │   │   │   ├── social │   │   │   │   └── rss.html │   │   │   └── social.html │   │   ├── pagination.html │   │   └── topline.html │   ├── pages │   │   ├── about │   │   │   └── index.html │   │   ├── about-site │   │   │   └── index.html │   │   ├── archives │   │   │   ├── all.html │   │   │   ├── index.html │   │   │   ├── month.html │   │   │   └── year.html │   │   ├── booklist │   │   │   └── index.html │   │   ├── contact │   │   │   └── index.html │   │   ├── error-page │   │   │   └── index.html │   │   ├── home │   │   │   ├── index.html │   │   │   ├── list.html │   │   │   └── summary.html │   │   ├── links │   │   │   └── index.html │   │   ├── post │   │   │   └── index.html │   │   ├── subject │   │   │   ├── index.html │   │   │   └── item.html │   │   ├── tag │   │   │   ├── index.html │   │   │   ├── list.html │   │   │   └── summary.html │   │   └── topic │   │   ├── index.html │   │   ├── list.html │   │   └── summary.html │   └── static │   ├── error-track.js.html │   ├── global-config.js.html │   ├── init.js.html │   ├── page-debug.js.html │   └── web-pref.js.html ├── post │   ├── single.html │   ├── single.md ├── robots.txt ├── shortcodes │   └── crayonCode.html ├── sitemap.xml └── subject └── single.html ``` 现在的查找逻辑不是十分合理,为了避免构建时的警告信息,我使用 `layouts/_default` 接管了标签和分类的模板入口,其余的入口页面依旧放在 `layouts` 子目录的各同名目录下,比如 `layouts/post/single.html` 。 所有页面的真实处理逻辑放置在 `layouts/partials/pages` 中,可以最大限度保障页面主题的兼容性,比如这次,我就只是修改了入口页面的位置,而页面处理逻辑没有大动。 额外说明一点,`_default` 目录下的文件,只有 `tag` 和 `topics` 相关三个文件存在内容,其余文件保持为空即可。 新版本 Hugo 针对 config.toml 也有升级策略,直接执行 `hugo` ,配置文件中的问题,它会进行报错提示,并示例你如何更改,比如这样: ```diff @@ -61,30 +61,13 @@ [mediaTypes] [mediaTypes."text/plain"] -suffix = "md" - -[mediaTypes."application/rss"] -suffix = "xml" +suffixes = ["md"] [outputFormats.MD] Path = "/" mediaType = "text/plain" isPlainText = true -[outputFormats.FEED] -mediatype = "application/rss" -baseName = "feed" - [outputs] -page = [ "HTML" ,"MD", "FEED" ] +page = [ "HTML" ,"MD" ] ``` ## 分类和标签扁平化以及其他兼容处理 在 Hugo 升级之前,我使用的是这样的分类结构: ```plain topics: [ "知识点滴/容器化" ] ``` 老版本的 Hugo 会自动生成两级分类目录,并且两个目录都支持索引,像是下面这样。 ```plain /知识点滴 /知识点滴/index.html /知识点滴/page/2.html /知识点滴/容器化 /知识点滴/容器化/index.html /知识点滴/容器化/page/2.html ``` 而新版本会生成唯一的分类,并且使用自己的策略**转义链接地址中的空格和斜杠为连字符**。 ```plain /知识点滴-容器化 /知识点滴-容器化/index.html /知识点滴-容器化/page/2.html ``` 在思考之后,我发现除了做接口需要表明资源从属关系之外,除非写书似乎文章还真的不太需要那么明确的层级隔离,一级目录外加标签就能提供良好的阅读和解决简单检索需求。 所以我进行了简化,去掉了所有的文章分类从属关系。但是我还有一个按照年月日进行日期归档的路径,这里还是希望能够保持从属关系的,该怎么解决呢? 首先归档内容,暂时还是需要自己手动生成并维护的,下面是我使用另外一个脚本在每次文章发布时生成的目录结构(部分)。 ```plain ./content/archives ├── 2018 │   ├── 11 │   │   └── index.md │   ├── 12 │   │   └── index.md │   └── _index.md ├── 2019 │   ├── 01 │   │   └── index.md │   └── _index.md └── _index.md ``` 这里使用了一个取巧的方案,使用 `_index.md` 会枚举目录中所有内容的特性,可以轻松生成下面的结构。 ```plain ./public/archives ├── 2018 │   ├── 12 │   │   └── index.html │   └── index.html ├── 2018.html ├── 2019 │   ├── 01 │   │   └── index.html │   └── index.html ├── 2019.html └── index.html ``` 接下来只需要修正原本模板中引用地址 `/archives/YEAR/` 为 `/archives/YEAR.html` 即可 上文提到过 Hugo 新版本对于链接的一些额外转义处理,除了分类会被影响外,标签也被波及到了。 举个例子,我原本有一个标签叫做 : `Linux/Mac` ,在旧版本的 Hugo 中的输出结果是这样: ```plain /public/tags/linux/mac/index.html ``` 但是在新版本变成了这样: ```plain /public/tags/linux-mac/index.html ``` 因为我禁用了 RSS ,暂时不提供标签的订阅,文章内直接引用标签目前也比较少,访问地址变了就变了,但是模板中如果直接使用老版本的语法,标签地址生成的还是老样子(生成链接策略和渲染逻辑不一致),结果就是含有空格和斜杠的标签页面是无法正常浏览的! 举个例子,老版本语法: ```plain {{ $tagLink | urlize }} ``` 解决方式比较 trick,需要手动在模板中进行转义,并补全 `.html` 后缀: ```plain { replace (replace (lower $tag) "/" "-") " " "-"}}.html" ``` 至此,升级过程中的主要问题就都讲完了,我们接下来聊聊性能提升和其他的话题。 ## 性能和一些其他的事情 这次重构之后,完整发布时间缩短了 `10s` ,构建时间减少了 `3s` ,后续计划将预览和生产的流水线进行分割,应该还能进一步提升效率。 ![完整发布时间35s左右](https://attachment.soulteary.com/2019/02/01/finial.png) 在做这次升级重构之前,我首先考虑了最低成本升级到的完全兼容的可用版本。 > v0.20.7 > @bep bep released this on 3 May 2017 · 1666 commits to master since this release 在试验之后,我发现唯一和我当前版本兼容的是 `v0.21`,也是一个古董版本,没有实质的变化。 > v0.47.1 > Hugo Static Site Generator v0.47.1 darwin/amd64 BuildDate: 2018-08-20T08:16:52Z 稍微做一些重构,能够接触到的最新版本则是上面的 0.47.1 ,也还是低版本的 Golang,并且和主流版本差异还是不小。 我使用 `hugo benchmark` 比较了 `0.20.x` 和 `0.40.x` 发现性能不升反降,期初我也很犹豫是否值得进行升级。 **v0.20.x** ```plain Average time per operation: 3955ms Average memory allocated per operation: 1651040kB Average allocations per operation: 22417100 ``` **v0.40.x** ```plain Average time per operation: 4056ms Average memory allocated per operation: 1650683kB Average allocations per operation: 22396305 ``` 于是我认真浏览了这一年半以来的[版本发布记录](https://github.com/gohugoio/hugo/releases?after=v0.25.1),觉得还是要与时俱进,如果不升级,将一直锁定在低版本的 Golang 运行时,所有的问题也都只能自己定制解决,完全不能使用社区新功能、也不把折腾内容贡献社区。 在使用两个简单站点分别使用 v0.20 和 v0.50 进行测试的时候,我发现提升还是很明显的,于是便着手进行了升级,附加调试完整花费了6个小时。 很可惜在 `v0.50.3` 版本之后,官方废弃了 `hugo benchmark` 这个命令,所以我们不能够和以往一样输出性能报告,不过直接使用站点生成时间来进行对比,也是一样的(站点实际构建时间)。 ```plain Total in 3452 ms Total in 3547 ms Total in 3730 ms ``` 可以看到效果还是不错的,另外我也终于可以使用 `brew install hugo` 安装的 Hugo 更方便的进行本地预览了。 ## 最后 下篇内容,我们继续聊聊 Wiki 系统的搭建。 --EOF