33

Hugo 跨版本升级

 5 years ago
source link: https://soulteary.com/2019/02/01/upgrade-hugo-across-versions.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

使用 Hugo 一年半了,终于有了升级的动力。趁着最近事情不多,着手搞定了这个事情,记录下来分享给需要的同学。

背景

在这一年半里,我一直使用着的是老版本: v0.20.7 ,运行非常稳定,写完文章 Git Push 后,GitLab Runner 自动更新预览地址,浏览没问题就可以一键发布了。

但是下面两个问题让我有了升级的想法。

构建速度随着内容增多变慢

去年十月,在网站架构简化之后,我的完整发布编译时间从 1分钟 进入了 40s 的阶段,但是随着内容的膨胀、编译时间越来越慢了,可以看到不少发布时间变长。

BjYR73V.jpg!web

看着 Hugo 升级 Golang 版本, 重构优化速度变的越来越快,不免心动。

需要维护两套不同版本的站点

一月初将博客主题重构之后,分享给了公司技术团队博客使用,考虑可维护性,新站点使用的是最新版本 0.53 ,因为技术团队站点功能更简单,所以升级过程中不兼容的部分其实并不多。

但是这个站点,因为自定义了“年月日”格式的归档,以及使用的是老版本的模板查找逻辑,生成页面链接也不完全兼容,所以直接升级是不行的。

可以预期的是,随着使用时间越来越长,这两个站点的差异会越来越大, 为了可维护性,必须将这两个站点使用的 Hugo 版本统一

梳理主要问题

  1. 官方支持 RSS 文件直接输出,是否还需要自定义站点 RSS 文件?
  2. 官方直接提供压缩能力,是否足够替换 Pipeline 中定制的压缩服务?
  3. 页面模板查找逻辑、模板语法、站点配置文件变更,现有模板无法直接使用。
  4. 分类标签系统扁平化,不再支持树形层级嵌套,链接兼容如何处理?

下面我来逐个击破。

Hugo RSS 解决方案

官方支持了 RSS 格式的输出,只要在 layouts 根目录创建一个文件即可, index.rss.xml ,模板可以自定义,参考官方文档 。

另外官方生成文档,默认会输出正确的 XML Version,所以可以检查并删除己配置的文档模板中下面的内容。

<?xml version="1.0" encoding="utf-8" standalone="yes" ?>

但是这样会有两个额外的问题,第一个是额外生成了许多不需要生成的“订阅源”,像是下面这样。

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 输出内容不支持自定义路径,你的订阅地址就只能是下面这样:

网站地址/index.xml

使用老版本的 RSS 方案,创建一个 /feed ,然后放置自定义的 RSS 模板,你会发现生成内容,仅支持该目录之下的文章… ORZ

如果你有类似的需求,这里更好的方案是“禁用官方RSS生成能力”、“自定义RSS模板”,可以做到按照你的需求在你期望的路径生成你期望数量的 RSS 内容。

首先是禁用官方RSS生成能力,在站点 config.toml 配置文件中添加下面的内容:

disableKinds= ["RSS"]

如果你有定义 output 格式,并包含 RSS 定义,也需要删除该内容。

 [outputs]
-page = [ "HTML", "RSS" ]
+page = [ "HTML" ]

接着分别创建 layouts/feed/index.htmlcontent/feed/index.md 两个文件。

模板文件内容可以参考:

{{ `<?xml version="1.0" encoding="utf-8" standalone="yes" ?>` | safeHTML }}
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/">
    <channel>
        <title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} - {{ end }}{{ .Site.Title }}{{ end }}</title>
        <link>{{ .Site.BaseURL }}feed/</link>
        <description>{{ .Site.Title }}最近更新内容。</description>{{ with .Site.LanguageCode }}
        <language>{{.}}</language>{{end}}{{ with .Site.Author.email }}
        <managingEditor>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</managingEditor>{{end}}{{ with .Site.Author.email }}
        <webMaster>{{.}}{{ with $.Site.Author.name }} ({{.}}){{end}}</webMaster>{{end}}{{ with .Site.Copyright }}
        <copyright>{{.}}</copyright>{{end}}{{ if not .Date.IsZero }}
        <lastBuildDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</lastBuildDate>{{ end }}
        <sy:updatePeriod>hourly</sy:updatePeriod>
        <sy:updateFrequency>1</sy:updateFrequency>
        <generator>hugo</generator>
        <atom:link href="{{ .Site.BaseURL }}feed/" rel="self" type="application/rss+xml"/>
        {{ range first 10 (where .Site.Pages "Type" "post") }}
        <item>
            <title>{{ .Title }}</title>
            <link>{{ .Permalink }}</link>
            <pubDate>{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 -0700" | safeHTML }}</pubDate>
            <dc:creator>{{`<![CDATA[`|safeHTML}}苏洋(soulteary){{`]]>`|safeHTML}}</dc:creator>
            <author>{{`<![CDATA[`|safeHTML}}苏洋(soulteary){{`]]>`|safeHTML}}</author>
            <guid>{{ .Permalink }}</guid>
            <description>{{`<![CDATA[`|safeHTML}}{{ .Description | safeHTML }}{{`]]>`|safeHTML}}</description>
            <content:encoded>{{`<![CDATA[`|safeHTML}}{{ .Description | safeHTML }}{{`]]>`|safeHTML}}</content:encoded>
        </item>{{ end }}
    </channel>
</rss>

页面数据文件,示例文件:

---
title: "Rss Feed"
author: "soulteary"
date: "2019-01-24"
type: feed
draft: false
isCJKLanguage: true
outputs: [ "HTML" ]
---

再次执行 hugo 进行站点生成,会发现生成的页面内容已经大幅减少:

自定义之前:
                   |  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  

最后别忘记通过构建脚本,将生成文件进行重命名。

mv feed/index.html feed/index.xml

更好的Hugo页面压缩能力

在使用 Hugo 版本和之前的压缩模式进行对比,发现 Hugo 压缩确实效率高不少,添加压缩参数 --minify 执行 Hugo ,生成时间几乎没有变化,还能省下 GitLab Pipeline 调用 Job 过程中的时间损耗,真的是太赞了。

但是压缩结果完全使用了 HTML 宽松模式,所有的 HTML Tag 属性都失去了引号,像是下面这样。

<span class=crayon-o>/</span>

个人倾向使用相对严格的模式进行页面结构编写,即使是程序生成的代码也是如此,因为一旦要进行调试,相对严格的标准,可以消除很多歧义,减少不必要的调试时间。

由于老版本不支持 --minify 参数,所以我使用 Node.js 简单写了一个脚本,用于替换页面内的空白内容,程序处理内容比较多,贴一下主要替换逻辑供参考。

const trimTags = (s) => s.replace(/>\s+</gm, '><').replace(/>(\s+\n|\r)/g, '>');
const content = trimTags(content);

如果你和我一样,定制了代码高亮,有更复杂的 HTML 结构,那么还需要额外处理一下代码,避免出现 ReDoS 问题。

模板配置相关处理

首先贴出我的站点主题的 layouts 目录结构(部分),它也代表了网站的整个抽象逻辑。

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 目录下的文件,只有 tagtopics 相关三个文件存在内容,其余文件保持为空即可。

新版本 Hugo 针对 config.toml 也有升级策略,直接执行 hugo ,配置文件中的问题,它会进行报错提示,并示例你如何更改,比如这样:

@@ -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 升级之前,我使用的是这样的分类结构:

topics: [ "知识点滴/容器化" ]

老版本的 Hugo 会自动生成两级分类目录,并且两个目录都支持索引,像是下面这样。

/知识点滴
/知识点滴/index.html
/知识点滴/page/2.html
/知识点滴/容器化
/知识点滴/容器化/index.html
/知识点滴/容器化/page/2.html

而新版本会生成唯一的分类,并且使用自己的策略 转义链接地址中的空格和斜杠为连字符

/知识点滴-容器化
/知识点滴-容器化/index.html
/知识点滴-容器化/page/2.html

在思考之后,我发现除了做接口需要表明资源从属关系之外,除非写书似乎文章还真的不太需要那么明确的层级隔离,一级目录外加标签就能提供良好的阅读和解决简单检索需求。

所以我进行了简化,去掉了所有的文章分类从属关系。但是我还有一个按照年月日进行日期归档的路径,这里还是希望能够保持从属关系的,该怎么解决呢?

首先归档内容,暂时还是需要自己手动生成并维护的,下面是我使用另外一个脚本在每次文章发布时生成的目录结构(部分)。

./content/archives
├── 2018
│   ├── 11
│   │   └── index.md
│   ├── 12
│   │   └── index.md
│   └── _index.md
├── 2019
│   ├── 01
│   │   └── index.md
│   └── _index.md
└── _index.md

这里使用了一个取巧的方案,使用 _index.md 会枚举目录中所有内容的特性,可以轻松生成下面的结构。

./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 中的输出结果是这样:

/public/tags/linux/mac/index.html

但是在新版本变成了这样:

/public/tags/linux-mac/index.html

因为我禁用了 RSS ,暂时不提供标签的订阅,文章内直接引用标签目前也比较少,访问地址变了就变了,但是模板中如果直接使用老版本的语法,标签地址生成的还是老样子(生成链接策略和渲染逻辑不一致),结果就是含有空格和斜杠的标签页面是无法正常浏览的!

举个例子,老版本语法:

{{ $tagLink | urlize }}

解决方式比较 trick,需要手动在模板中进行转义,并补全 .html 后缀:

{ replace (replace (lower $tag) "/" "-") " " "-"}}.html"

至此,升级过程中的主要问题就都讲完了,我们接下来聊聊性能提升和其他的话题。

性能和一些其他的事情

这次重构之后,完整发布时间缩短了 10s ,构建时间减少了 3s ,后续计划将预览和生产的流水线进行分割,应该还能进一步提升效率。

In6NZvz.jpg!web

在做这次升级重构之前,我首先考虑了最低成本升级到的完全兼容的可用版本。

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.x0.40.x 发现性能不升反降,期初我也很犹豫是否值得进行升级。

v0.20.x

Average time per operation: 3955ms
Average memory allocated per operation: 1651040kB
Average allocations per operation: 22417100

v0.40.x

Average time per operation: 4056ms
Average memory allocated per operation: 1650683kB
Average allocations per operation: 22396305

于是我认真浏览了这一年半以来的 版本发布记录 ,觉得还是要与时俱进,如果不升级,将一直锁定在低版本的 Golang 运行时,所有的问题也都只能自己定制解决,完全不能使用社区新功能、也不把折腾内容贡献社区。

在使用两个简单站点分别使用 v0.20 和 v0.50 进行测试的时候,我发现提升还是很明显的,于是便着手进行了升级,附加调试完整花费了6个小时。

很可惜在 v0.50.3 版本之后,官方废弃了 hugo benchmark 这个命令,所以我们不能够和以往一样输出性能报告,不过直接使用站点生成时间来进行对比,也是一样的(站点实际构建时间)。

Total in 3452 ms
Total in 3547 ms
Total in 3730 ms

可以看到效果还是不错的,另外我也终于可以使用 brew install hugo 安装的 Hugo 更方便的进行本地预览了。

最后

下篇内容,我们继续聊聊 Wiki 系统的搭建。

—EOF


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK