Back

从 Hexo 搬迁至 Hugo

由于之前突发奇想,看到某些同学的博客确实好看,心血来潮便花了大概几个晚上把博客生成工具从原来的 Hexo 迁移到了 Hugo 上,便有了这次搬迁记录,也算是给其他想迁移小伙伴一个参考。

Hugo VS Hexo

根据一些文章 [1][2]已经给出的对比,Hugo 拥有着比较不错的渲染速度,(这里贴个实测渲染数据),对于我这个目前所有 Posts 比较多,又有比较多图片的用户来说,在渲染速度方面确实也有一定需求。而且最近自己也在学习 Golang ,对于使用 Golang 的工具我还是比较有好感的。我进行了一个小对比,虽然 Hexo 插件用的有些多,但是在满足我个人需求下,使用两个工具对相同文章数量生成静态页面的对比效果如下图所示:

左为 Hexo 生成速度 VS 右为 Hugo 生成速度
左为 Hexo 生成速度 VS 右为 Hugo 生成速度

当然在稍微使用了一下后,也能感受到 Hugo 的缺点,目前 Hugo 并不是特别完美,因为没有 NPM 那么方便地使用拓展,很多都是使用了 Hugo 自己本身支持的内容,没有良好的拓展性,所以导致很多东西都得自己动手,比如一些小插件;但是好在由于 Hugo 目前来说已经比较成熟,不需要自己动手做太多的定制化东西,文档也还尚可。

况且自己目前使用的 Hexo 主题,原作者已经不再继续开发了,并且主题也并不是很符合我的预期。所以综合起自身需求来看,我还是更需要一个比较高效的框架,也正好换一个满意的主题。

Select A Theme

这里先整理了一下自己心目中理想的主题风格:

  • 简洁。
    • 主页 posts 展示不需要配图展示,因为我比较懒,每次写完文章都懒得去找配图的人,而且配图跟文章还要很搭配,这样照起来比较费劲,而且还会增加自己仓库图片数量(因为我文章用图片很多),基本没有必要。
    • 不需要太艳丽的配色,我一开始也很喜欢各种 Material 风格的主题,但是后面撞主题的实在太多了,看着我都审美疲劳了…所以我也开始厌恶那种花花绿绿、五颜六色的主题了,后面使用了 Minos 这款非常简洁的主题,深得我心。不过可惜的是该主题油画并不是很好,使用 pagespeed 对博客测速在即使上了 CDN 的情况下评分十分不理想,况且原作者已经不怎么开发这个主题了,自己也没有很多精力来优化这个主题,尝试了多次优化只能说收效甚微,这也是促使我更换架构的原因之一。
  • 大方。因为我不需要其他七七八八的东西,竟可能展示页面内容就行,所以我觉得内容展示需要尽量宽屏,不要弄的窄窄的,尤其是 PC 端,有些主题我是很不明白 PC 端显示内容宽度只占了整个屏幕的 20-30% ,其他也没有利用,不知道这么设计是为了啥,比较懒去适配响应式设计?
  • 侧边目录。这个功能在 2021 年我觉得是非常必要的,并且我博客有好几篇非常长的文章,没有侧边目录,阅读起来简直受罪,我也不知道在这时代侧边目录竟然还不是现在博客的标配。之前优化 Minos 主题时想给他加个侧边栏来着,但是奈何确实太菜了,也没啥精力,只能做到在文章开头加入目录。
  • 功能性实用。这里的功能性指的是可以支持各种的小插件,比如:嵌入式推文,这样就显得很 Modern 了;颜色不一样的注释样式,这样可以在文章中使用这些不同颜色的注释样式来达到不同的提示效果。

我在 Github 上查询 Hugo Theme 话题,找了一些最近一年内有活动的 Repo 进行筛选,也有一些是直接从 Hugo 官网的主题页面找的:

还有几个同一些列看起来挺棒的主题,但是由于某些原因最终没有选用:

  • https://github.com/dillonzq/LoveIt 比较喜欢的主题之一,但是目前看起来作者不继续维护了,比较可惜。当时也是看到别人主题用这个比较漂亮,也才有想入坑 Hugo 的想法

  • https://github.com/khusika/FeelIt 从 LoveIt 衍生出来的主题之一,Demo Site 看过去我并不喜欢主页的卡片设计,比较喜欢原来 LoveIt 的主页样式。宽屏的样式改的也很好,但是就是主页这个。。因为我的文章缩略可能都比较长而且不喜欢放主页图,就会很丑,PASS

  • https://themes.gohugo.io/themes/ublogger/ 也是从 LoveIt 衍生出来的主题之一,主页同样是卡片设计,跟 FeelIt 一样找了一下配置,但是看起来没办法配置主页的显示样式,PASS

最终在 eureka 跟 stack 中进行最终选择:

  • eureka 整体比较符合我的预期,但是用的人也确实贼多,随便打开几个 hugo 的网站,不少人用的都是 eureka,拿 eureka 的 Demo Stie 在 pagespeed 测试,只有 32 分
  • stack 看起来并没有 eureka 那么好,主要是对于我来说主页设置还是有点太拥挤了,不够落落大方,也没有支持多彩的注释,看起来比较的一般,但是其 Demo Site 相对于 eureka 的 Demo Site 来说,pagespeed 得分可以拿到近乎满分的成绩,优化相当不错。

所以,在综合考虑了两者的优劣对比之后,我最终选择了 stack ,尽管其功能性并不是特别完美,但是我觉得优化是一件比较复杂的事,从各自的 Demo Site 来看,stack 的优化效果要远远由于 eureka 的,尽管 stack 功能性上比 eureka 稍弱一些,但是我可以在其基础上去增加、修改功能,而不需要在对主题进行整体优化了,这是最主要的一点。

Configuration

首先肯定是得先参考 stack 主题文档,跟着文档,把该配置的配置好了,该复制的复制好了,再进一步进行修改其他的操作。

URL Path

对于个人博客的迁移而言,保证文章链接不变是最重要的。

因为对于一般站长自建的站点来说,如果一些文章被其他站点引用了,那么肯定就是静态链接了,如果迁移后改变了原文的 URL 链接地址,会对原来其他站点的导向非常不友好,所以我们基本得要确保旧文章链接最好不要进行改动。

这里主要参考该文章[2]的迁移修改部分,我之前 Hexo 的站点链接为:/:year/:month/:day/:filename/,所以我们也要在 Hugo 这里将生成的页面链接改成这种样子,我们需要在配置文件中增加如下配置:

permalinks:
    posts: /:year/:month/:day/:filename/

我这里选用了 yaml 格式, toml 格式的同学可以根据以上自行修改。

RSS

默认 Hugo 生成的 RSS 链接为 /index.xml ,而我之前用的是 atom.xml,可以通过如下配置修改:

outputFormats:
    RSS:
        mediatype: "application/rss"
        baseName: "atom"

并不需要修改主题文件夹下的 rss.xml ,Hugo 会自动根据我们的配置在站点目录下生成 atom.xml 。并且,在 stack 主题下我们还可以通过在主题配置文件中配置rssFullContent选项来决定是否在 rss 文件中输出全文,像我的文章会比较长就使用了false表示不输出全文。

Tags 链接

​ Hugo 中默认会把链接中的字母变成小写,比如标签 Go ,在之前对应地址 /tags/Go ,换成 Hugo 后则是 /tags/go ,可以通过下面的配置关闭这个转化。

其实我认为这个保留选项并不重要,所以我并没有特别修改这个选项,保持了原样,如果有同学建议,可以参考上述文章[2]

Edit The Theme

接下里就是优化主题时间了,对比原主题,我在其基础上做了一定的修改,主要有如下几个点。(因为我对 Hugo 主题开发并不是很熟,基本就是对着文档改,以及包括对着其他有我需要的特性的主题代码参考来进行修改的,所以代码可能改的比较垃圾)

去除了首页左侧 Sidebar 的网站描述,改成了社交链接。

图标使用的是主题配套的 Tabler Icons ,还是比较好用的,把你想要的 svg 放到 assets/icons 目录下。接着为了保持与主题整体色调一致,需要修改这个 svg 文件中的stroke属性,改成stroke="currentColor"。这样我们就可以在其他地方引用到这个图标了。

接着直接搜site-description,在 themes/stack/layouts/partials/sidebar/left.html 定位到侧边栏的代码,删掉原来的代码,将其修改为以下代码:

<div class="site-description">
  {{ if (.Site.Params.social.github) }}
  <a title="GitHub" href="{{.Site.Params.social.github}}" target="_blank" rel="noopener">
    {{ partial "helper/icon" "github" }}
  </a>
  {{ end }}

  {{ if (.Site.Params.social.twitter) }}
  <a title="Twitter" href="{{.Site.Params.social.twitter}}" target="_blank" rel="noopener">
    {{ partial "helper/icon" "twitter" }}
  </a>
  {{ end }}

  {{ if (.Site.Params.social.rss) }}
  <a title="RSS" href="{{.Site.Params.social.rss}}" target="_blank" rel="noopener">
    {{ partial "helper/icon" "rss" }}
  </a>
  {{ end }}
</div>

稍微解释一下,.Site.Params可以获取到主题配置文件中的params配置选项,后面对应的就是相应的层级关系了。所以我们需要在配置文件中增加如下选项:

params:
	#...
  social:
    github: https://github.com/ZeddYu
    twitter: https://twitter.com/ZeddYu_Lu
    rss: https://blog.zeddyu.info/atom.xml

{{ partial "helper/icon" "rss" }}就可以帮助我们去获取上述添加的 svg 文件作为我们的图标,比如这里对应的就是 assets/icons/rss.svg 文件。

这里我只增加了三个自己常用的社交链接,如果想要增加其他链接,都是类似的操作;这里最优解当然是通过循环读取 social 配置,然后自动配置,但是自己比较懒,也没什么其他要添加的,就静态写了三个自己常用的社交链接。

Summary Of Articles

原主题在展示主页文章的时候,对于文章内容的获取并非是读取摘要,这里的摘要就是读取文章开头至<!--more-->之间的内容,而是读取文章的 description 字段,而对于我来说,我更喜欢使用摘要的形式,并且自己的文章之前也没有写过 description 字段,给每篇文章增加该字段会很麻烦。

所以我修改了原主题主页展示文章内容的形式,改成了显示摘要的形式。通过定位article-subtitle找到对应文件 themes/stack/layouts/partials/article/components/details.html ,但是并不是很好改,因为这个文件被主页页面所使用,也被文章详情内容所使用,也就是说如果我们直接将其简单替换为之摘要内容的话,虽然主页展示了文章摘要,但是文章详情内容也会展示内容摘要,而且同时显示文章全部内容,就会有两段重复的内容了。

所以我们只能分别处理,也就是说增加一个文件,让主页引用,文章详情内容页面不引用就不会出现这种问题了。

最终修改:

删除了 themes/stack/layouts/partials/article/components/details.html 页面中的

-    {{ with .Params.description }}
-    <h3 class="article-subtitle">
-        {{ . }}
-    </h3>
-    {{ end }}

在 themes/stack/layouts/partials/article/components 增加了一个新文件为 article-intro.html ,文件内容与删除上述内容前的 details.html 文件略有不同:

@@ -15,11 +15,11 @@
         </a>
     </h2>

-    {{ with .Params.description }}
-    <h3 class="article-subtitle">
+    {{ with .Summary }}
+    <div class="article-subtitle">
         {{ . | safeHTML}}
-    </h3>
-    {{ end }} -->
+    </div>
+    {{ end }}

     {{ if or (not .Date.IsZero) (.Site.Params.article.readingTime) }}
     <footer class="article-time">
@@ -40,6 +40,9 @@
                 </time>
             </div>
         {{ end }}
+        <div>
+            <a href="{{ .RelPermalink }}">Read More…</a>
+        </div>
     </footer>
     {{ end }}
 </div>

这里使用了 Hugo 提供的.Summary获取文章摘要,替换掉了原来文章描述的位置,并增加了“阅读更多”的入口。接着我们还需要增加一个 themes/stack/layouts/partials/article/components/header-intro.html 文件,文件内容与 header.html 区别如下:

@@ -31,5 +31,5 @@
         </div>
     {{ end }}

-    {{ partialCached "article/components/details" . .RelPermalink }}
+    {{ partialCached "article/components/article-intro" . .RelPermalink }}
 </header>

这里只需要将我们所更改的 artile-intro 代替之前的 details 引入即可。

ShortCodes

这里主要参考的文章是:自定义 Hugo Shortcodes 简码 当中的 notice 部分,除了自己做了一定的字体、颜色定制调整,其他基本上一摸一样,所以这里不再赘述。

样式效果:

{{< notice notice-warning >}}
Hello World!
{{< /notice >}}

Hello World!

{{< notice notice-info >}}
Hello World!
{{< /notice >}}

Hello World!

{{< notice notice-note >}}
Hello World!
{{< /notice >}}

Hello World!

{{< notice notice-tip >}}
Hello World!
{{< /notice >}}

Hello World!

并且这里简单提个醒,对于 Hugo 来说,既是你是在代码段里面写 ShortCodes ,Hugo 也会直接对其进行渲染,并不会将其视为字符串处理。

所以当时我在渲染以上 Shortcodes 的时候就遇到了这个问题,我们得在不需要渲染的 ShortCodes 代码加注释才能使得 Hugo 认为这是一段字符串,也就是如下这个样子:

carbon.png
carbon.png

CDN For IMG

因为 Stack 主题推荐在文章内插入图片使用 Hugo 的 Page Bundleopen 功能[3],而外层我使用的是 CloudFlare 做 CDN ,尽管使用了一些压缩处理尝试尽可能优化图片加载速度,但是 CloudFlare 对于国内“加速”效果属实难以苟同,好在 jsdelivr 提供了基于 Github 仓库资源的加速服务,并且在国内加速效果还不错,所以我还针对链接到本地的图片资源在生成图片链接时自动加上了 jsdelivr 链接。

思路非常简单,因为生成图片链接是使用的相对路径,所以我们只需要对这些相对路径的图片进行处理即可,加上 jsdelivr 到自己的 Github Page 博客仓库开头即可。例如,我的博客仓库为 https://github.com/ZeddYu/ZeddYu.github.io ,分支为 master 分支,对应的 jsdelivr 加速链接为 https://cdn.jsdelivr.net/gh/ZeddYu/ZeddYu.github.io@master/

找到主题对于本地图片生成处理的地方,为 themes/stack/layouts/_default/_markup/render-image.html 文件,

{{- if $image -}}
<img src="https://cdn.jsdelivr.net/gh/ZeddYu/ZeddYu.github.io@master/{{ $Permalink| relURL }}"
{{- else -}}
<img src="{{ $Permalink }}"
{{- end -}}

将 img 对应的链接手动加上 jsdelivr 的链接,但是需要手动加上判断,这个$image是上文给的判断是否为本地图片的变量;对于外链我们不需要使用 jsdelivr 链接前缀进行加速,所以这里只需要对本地图片 jsdelivr 加速即可。

Github Action

最后配置 .github/workflows/gh-pages.yml 使用 Github Action 自动化部署,这里我的配置为私有的博客源代码仓库,博客静态代码仓库为公开仓库,所以需要使用到 Github 的个人 Access Token ,其余的不再赘述。

name: github pages

on:
  push:
    branches:
      - master  # Set a branch to deploy
  pull_request:

jobs:
  deploy:
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v2
        with:
          submodules: true  # Fetch Hugo themes (true OR recursive)
          fetch-depth: 0    # Fetch all history for .GitInfo and .Lastmod

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: 'latest'
          extended: true

      - name: Build
        run: hugo --minify

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          external_repository: ZeddYu/ZeddYu.github.io	# 发布到哪个 repo
          personal_token: ${{ secrets.HUGO_TOKEN }}	# 发布到其他 repo 需要提供上面生成的 personal access token
          publish_dir: ./public	# 注意这里指的是要发布哪个文件夹的内容,而不是指发布到目的仓库的什么位置,因为 hugo 默认生成静态网页到 public 文件夹,所以这里发布 public 文件夹里的内容
          publish_branch: master	# 发布到哪个 branch

Workbox For Dark Theme

虽然本地测试的时候看起来十分顺畅,但是在使用部署到远程后,可能由于 CloudFlare 对国内的减速作用,以及主题对于黑暗模式的实现使用的是 javascript 覆盖实现,而应用黑暗模式 js 文件加载速度较慢的情况下,会导致在黑暗模式在切换页面的时候,会在加载其他的时候会恢复到白色的背景,导致“闪屏”效应。这对用户肯定是非常不友好的体验,所以经过一些尝试,虽然无法完美解决掉这个问题,但是在一定程度上有效减缓了这个问题的发生。

这里也问了一下前端大师 @rex ,他一开始给我的方案是让我使用 PWA(Progressive Web Apps,渐进式Web 应用),尝试使用 Web App Manifest ,可以在 static 文件夹下增加 manifest.json 来预加载背景颜色。

{
    "name": "",
    "display": "standalone",
    "background_color": "#303030",
    "theme_color": "#303030"
}

主题黑暗模式的背景颜色为#303030 ,虽然理论上可能会影响到日间模式的切换,但是实际上既没有影响到日间模式,对“闪屏”效应也没有实质性的。

跟 @rex 再次讨论了一下,目前最优的夜间模式的实现方式应该是在服务器端存储用户设置的模式信息,当用户请求的时候,根据 Cookie 或者其他信息返回不同的样式,这样就能完美避开“闪屏”这个问题了,并且对于自适应模式还可以通过媒体查询来实现;其次实现方式才有可能使用 js 覆盖的形式,通过 localstorage 或者本地浏览器的存储介质等获取用户存储的主题设置,再对页面进行颜色覆盖,但是这个方案比较依靠网络速度,因为如果网络不佳,对于 js 文件加载缓慢的话就会导致先出现默认主题颜色,再出现用户设置的主题颜色,就会产生“闪屏”现象。

所以通过分析,我们目前大概知道“闪屏”最好的解决方案是根据用户设置返回不同的样式文件,但是对于静态页面 Github Pages ,并没有什么办法直接识别用户,即使是部署了 CloudFlare ,目前 CloudFlare workers 也没有什么办法能够实现类似效果。在不改变静态页面的架构下,只能通过尝试解决网络加载速度来减缓这个问题。

于是我想到了 Service Worker ,如果我们能通过 SW 将静态文件缓存到用户本地,就可能可以尽量避免网络问题了。于是在经过一番探索,发现了 Workbox 这个比较完善的解决方案。Workbox 是 Google Chrome 团队推出的一套 PWA 的解决方案,这套解决方案当中包含了核心库和构建工具,我们可以利用 Workbox 实现 Service Worker 的快速开发。

Workbox 说到底还是 SW ,其注册使用与 SW 并没有什么不同,所以我们肯定得在主题注册 SW 文件。这里我选择在 themes/stack/layouts/partials/footer/components/script.html 文件中注册 sw 文件

<script>
    if ('serviceWorker' in navigator) {
        window.addEventListener('load', function() {
            navigator.serviceWorker.register('/sw.js');
        });
    }
</script>

根目录下的 sw.js 文件就需要在 static 目录下放置文件

importScripts("https://cdn.jsdelivr.net/npm/workbox-cdn/workbox/workbox-sw.js");

if (workbox) {
	console.log(`Yay! Workbox is loaded 🎉`);

	workbox.routing.registerRoute(
		/\.(?:js|css)$/,
		new workbox.strategies.StaleWhileRevalidate({
			cacheName: "static-resources",
		})
	);

	workbox.routing.registerRoute(
		"https://cdn.jsdelivr.net/gh/ZeddYu/ZeddYu.github.io@master//ts/main.js",
		new workbox.strategies.StaleWhileRevalidate({
			cacheName: "static-resources",
		})
	);

	workbox.routing.registerRoute(
		/\.(?:png|jpg|jpeg|svg|gif)$/,
		new workbox.strategies.CacheFirst({
			cacheName: "image-cache",
			plugins: [
				new workbox.expiration.ExpirationPlugin({
					maxEntries: 20,
					maxAgeSeconds: 7 * 24 * 60 * 60,
				}),
			],
		})
	);

	workbox.routing.registerRoute(
		/^https:\/\/fonts\.googleapis\.com/,
		new workbox.strategies.StaleWhileRevalidate({
			cacheName: "google-fonts-stylesheets",
		})
	);

	workbox.routing.registerRoute(
		/^https:\/\/fonts\.gstatic\.com/,
		new workbox.strategies.CacheFirst({
			cacheName: "google-fonts-webfonts",
			plugins: [
				new workbox.cacheableResponse.CacheableResponsePlugin({
					statuses: [0, 200],
				}),
				new workbox.expiration.ExpirationPlugin({
					maxAgeSeconds: 60 * 60 * 24 * 365,
					maxEntries: 30,
				}),
			],
		})
	);
} else {
	console.log(`Boo! Workbox didn't load 😬`);
}

以上对图片等一些静态资源都做了缓存,当然最主要的还是缓存操作覆盖主题的 main.js 文件,为了加快这个文件获取,我还特地给该文件使用了 jsdelivr 进行加速。这里值得注意的就是,Service Worker 默认只缓存本站链接的静态文件,如果需要加载其他站点的文件,就需要通过registerRoute来注册该文件。

再经过一番折腾后,在使用了 jsdelivr CDN 加速 js 文件、Service Worker 缓存静态文件双加持下,基本可以做到无痛切换页面了。(尽管看起来还是可能会有 25ms 的瞬间白屏…但是应该可以说是很大程度上缓减了这个问题,当然得等以后看看还有没有什么新的技术可以来改善这个问题了。

Snipaste_2021-10-02_14-10-17.png
Snipaste_2021-10-02_14-10-17.png

Issues

以下是在迁移过程中遇到的一些报错错误

Failed to render pages

Failed to render pages: render of "page" failed: execute of template failed: template: posts/single.html:277:98: executing "no-content" at <partial "function/content.html">: error calling partial: "/Users/zedd/Desktop/demo/quickstart/themes/uBlogger/layouts/partials/function/content.html:12:15": execute of template failed: template: partials/function/content.html:12:15: executing "partials/function/content.html" at <partial "function/checkbox.html" $content>: error calling partial: partial that returns a value needs a non-zero argument.
Total in 19 ms

引起这个报错的可能原因就是文章当中有一些不规范的 Markdown 语法,例如我找出来的就是复制一些链接时候有一些不规范的语法:

[https://baidu.com](<[https://baidu.com](https://baidu.com)>)

诸如此类,只要把这些引起错误的语法修正成规范的链接语法即可。

rss error on line PCDATA invalid Char value X

在生成 RSS 文件的时候可能引起如下报错:

This page contains the following errors:
error on line 455 at column 40: PCDATA invalid Char value 8

这个报错可能原因就是生成的 RSS 文件当中包含有不可见字符,比如以上例子就是存在 \x08 字符,可以根据报错定位一下位置,然后去 markdown 文件当中进行删除

References

  1. 浅谈我为什么从 HEXO 迁移到 HUGO
  2. 博客系统迁移:Hexo 到 Hugo
  3. Hugo Stack 主题文档
Licensed under CC BY-NC-SA 4.0

I am looking for some guys who have a strong interest in CTFs to build a team focused on international CTFs that are on the ctftime.org, if anyone is interested in this idea you can take a look at here: Advertisements


想了解更多有意思的国际赛 CTF 中 Web 知识技巧,欢迎加入我的 知识星球 ; 另外我正在召集一群小伙伴组建一支专注国际 CTF 的队伍,如果有感兴趣的小伙伴也可在 International CTF Team 查看详情


comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy