折腾Hugo的Loveit主题

上一篇文章里面刚说好不要又是一篇技术博客,结果,可能我今天就要水一篇了……主要原因是觉得用了LoveIT这个主题之后,觉得还是有点美中不足,所以自己动手修改了一下。修改完了觉得,总得找个地方记录一下,以免,之后自己忘记怎么搞。下面说的修改都以LoveIT为例,其他的主题可以参照修改。

Tip

除非打算提交PR,否则不建议在主题的文件夹内直接进行修改,不然会给日后更新主题带来很多的麻烦。推荐把主题内需要修改的文件复制到站点根目录对应位置内(例如:/themes/LoveIt/layouts/partials/single/footer.html -> /layouts/partials/single/footer.html)之后进行修改。hugo在生成站点的时候,会优先使用根目录下的同名文件覆盖主题文件。

1. 相关文章

以前用WordPress的时候,可以显示相关文章的推荐。我觉得这个功能挺好用的,于是想着我也要增加这个功能。因为hugo是全静态的,所以这个实现起来似乎有些难度。google了一圈发现了这篇文章:How to show related posts in Hugo。看了一下原理,似乎是通过文章的tag来实现的。

那就,照着那篇文章里面的代码,稍微修改一下吧:

需要修改的文件是/layouts/partials/single/footer.html

在合适的位置插入下面代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<div class="related-posts">
	{{- if ( .Params.RelatedPosts | default true ) -}}
		</br>
		<b>{{- printf (T "RelatedPosts") -}}</b>
		{{ range first 5 ( where ( where .Site.Pages.ByDate.Reverse ".Params.tags" "intersect" .Params.tags ) "Permalink" "!=" .Permalink ) }}
			<li class="relatedPost">
				<a href="{{ .RelPermalink }}">{{ .Title | markdownify }}</a><br />
				{{ .Description | markdownify }}
			</li>
		{{ end }}
	{{- end -}}
</div>

上面的代码默认会显示最多5篇相关文章,如果某一篇文章不需要显示相关文章的话,可以在文章的front matter中手动输入"RelatedPosts: false"来禁用相关文章的显示。

PS. 我后来又发现hugo-theme-meme这个主题的config.toml里面有相关文章的部分,可以通过文章的分类,标签和发布日期来设定权重,感觉更加全面一些了。

2. 全文搜索(Algolia)

感觉这个是最难的部分,我搞了好久好久啊……最大的难点就在于,如何生成全文的索引。Algolia怎么注册什么的,网上铺天盖地都有,就不再赘述了。下面假设已经注册好了Algolia,同时也已经有了对应的API Keys。

在config.toml里面需要设置下面这些内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[outputFormats.Algolia]
  baseName = "algolia"
  isPlainText = true
  mediaType = "application/json"
  notAlternative = true

[params.algolia]
  appId = 你的APP ID
  indexName = 你的index名称
  searchOnlyKey = 记得这里只需要Search-Only API Key,不需要Admin API Key

同时,在[outputs]中,home增加Algolia。修改完毕应该长这样:

1
2
[outputs]
  home = ["HTML", "RSS", "Algolia"]

2.1 生成索引

生成文章的索引是实现搜索的第一步,也是最麻烦的一步。

麻烦的点主要在于,Algolia免费的服务限制了每一条记录的大小不能超过10 KB。所以,对于一些比较长的文章来说,如果要实现全文索引,那就势必牵涉到如何分割记录这个问题。

Algolia官方的建议是可以按照每一段来分割记录,并且针对WordPress和Laravel给出了教程。可惜的是,这些都是针对PHP的教程,没法移植到hugo上面。我于是google了一遍,发现一个俄罗斯的哥们写了个Gulp脚本,按照副标题来切割记录。然而……我很多的文章并没有副标题……然后我又发现了这篇文章。这个哥们也跟我有同样的困扰,于是采用了一个机智的方法,利用delimit函数,每1000个空格截断记录。我尝试了一下,可耻地失败了。失败原因:中文他没有空格啊……

不过虽然照抄代码失败了,思路还是可以借鉴的。机智如我想到:💡英文用空格来截断,主要是为了防止一个单词中途被切开,那中文并没有这个问题啊!我的博客,绝大多数的内容都是中文的,所以,直接截取若干个字符,应该就好了吧!至于为数不多的几篇英文博客,就,抓大放小吧……如果有更好的切割长文章的方法,强烈欢迎大家一起讨论。

简单爬了一下hugo的文档,发现了substr这个函数挺有潜力的。不过似乎substr不能用在Page.PlainWords上面,没关系,那就改用Page.Plain吧。

Page.Plain得到的结果里面有一些转义的字符需要修改一下。我就直接贴一下我的模板吧:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{{- $index := slice -}}
{{- range $page := $.Site.RegularPages -}}
  {{- $cleaned := slice -}}
  {{- $cleaned = $page.Plain}}
  {{- $cleaned = replace $cleaned "\r" ""}}
  {{- $cleaned = replace $cleaned "\n" ""}}
  {{- $cleaned = replace $cleaned "\u0026rsquo;" "'"}}
  {{- $cleaned = replace $cleaned "\u0026amp;" "&"}}
  {{- $cleaned = replace $cleaned "\u0026#34;" "\""}}
  {{- $cleaned = replace $cleaned "\u0026#39;" "'"}}
  {{- $cleaned = replace $cleaned "\u0026ndash;" "-"}}
  {{- $cleaned = replace $cleaned "\u0026gt;" ">"}}
  {{- $cleaned = replace $cleaned "\u0026quot;" "\""}}
  {{- $cleaned = replace $cleaned "\u0026ldquo;" "“"}}
  {{- $cleaned = replace $cleaned "\u0026rdquo;" "”"}}
  {{- $chunked := slice -}}
  {{- $chunked = $chunked | append (substr $cleaned 0 500) -}}
  {{- if gt (countwords $cleaned) 500 }}
    {{- $chunked = $chunked | append (substr $cleaned 500 500) -}}
  {{- end -}}
  {{- if gt (countwords $cleaned) 1000 }}
    {{- $chunked = $chunked | append (substr $cleaned 1000 500) -}}
  {{- end -}}
//
// 中间省略,大家根据自己blog的长短来决定写多少行吧。建议直接写一个脚本或者用Excel生成……
//
  {{- range $i, $c := $chunked -}}
    {{- $index = $index | append (dict "objectID" (print $page.File.UniqueID "_" $i) "content" $c "order" $i "title" $page.Title "date" $page.Date "url" $page.Permalink "tags" $page.Params.tags "categories" $page.Params.Categories) -}}
  {{- end -}}
{{- end -}}
{{- $index | jsonify -}}

把模板保存为/layouts/{?_}defaults/list.algolia.json。运行hugo命令之后就会在/public文件夹下生成algolia.json,可以上传到algolia了

2.2 上传索引

上传到algolia相对来说就比较简单了。我用的是atomic-algolia。首先安装npm,然后在站点根目录下面运行:

1
2
npm init
npm install atomic-algolia --save-dev

npm init是用来生成默认的package.json,基本上一路回车就行了。运行运行完这两个命令之后,打开package.json文件,在"scripts"这部分增加一句"algolia": "atomic-algolia"。修改完毕之后应该长这样:

1
2
3
4
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "algolia": "atomic-algolia"
},

完成之后,还需要新建一个.env的文件,告诉atomic-algolia一些信息:

ALGOLIA_APP_ID={{ YOUR_APP_ID }}
ALGOLIA_ADMIN_KEY={{ YOUR_ADMIN_KEY }}
ALGOLIA_INDEX_NAME={{ YOUR_INDEX_NAME }}
ALGOLIA_INDEX_FILE=public/algolia.json

填写并保存之后,可以尝试把索引提交给Algolia了:

1
npm run algolia

如果一切正常,那么应该看到的是类似于下面这张图片(我懒得截图,用了上面那个blog的截图):

success

2.3 修改Algolia的设置

上传完毕索引之后,还需要在Algolia里面设置一下。点击Indices,然后找到Configuration这个tab。主要修改下面几个选项:

  1. Searchable attributes

    这个选项用来告诉Algolia,在哪些索引里面进行搜索。比如,我不需要Algolia在日期和URL里面进行搜索(一般人都不需要吧),所以我的选项是title,tags,content和categories。

  2. Ranking and Sorting

    这个主要是排序,除了Algolia自己默认的之外,我还增加了date。优先显示最近发表的博客。

  3. Duplication and Grouping

    我们之前把一条博客拆分成了好多条索引,因此默认情况下Algolia如果在同一篇博客的不同索引找到了同一个关键词,会把这些索引都显示出来。这就有些没必要了。所以可以在这里进行设置,把Distinct设置为true,然后Attribute for Distinct设置为url,或者title。

  4. Highlighting

    这里最重要的是设置搜索结果中高亮的tag。Algolia默认用了<em></em>来进行高亮,但是LoveIt(或者hugo?)用了这个标签来标记斜体。因此需要修改一下。于是我把标签修改成了<m></m>

  5. 记得点Review and Save Settings来保存修改

2.4 和Netlify结合

每次更新blog之后手动生成和上传索引肯定不是一个方便的解决方案。好在我的博客采用了Netlify来部署,使用Netlify的话使得部署更方便。怎么注册Netlify,如何把Netlify和GitHub整合也不是这篇blog要cover的内容。下面的内容假设你已经可以成功在Netlify上面部署博客站点了。

首先,在settings的Build & deploy里面找到Environment,然后点击Edit variables。点击New variables,把刚才.env里面的4个变量分别填进去就行了。类似这样:

netlify

之后,修改根目录下netlify.toml,把{?[}build]下面的command行修改为:

command = "hugo --gc --minify && npm install atomic-algolia --save-dev && npm run algolia"

这样,下次更新完博客之后,netlify就会自动生成索引,并且提交给Algolia了。

2.5 为主题增加搜索页

这部分吧,主要难点在修改css……熟悉我的都知道我是个学财务和金融做咨询的,体健貌端思想开放,技术宅只是业余爱好。好在做咨询的,对于Google的使用还算略有一些心得。摸爬滚打误打误撞之后,居然也给我改出来了……

之前解决了我们分割记录问题的哥们也给了一个代码,做出了一个电脑上挺好用的即时搜索框。可惜这个框在手机上不太好用,所以我决定还是新建一个搜索页,专门用来搜索吧。

首先,在/content下面新建一个search.md文件,内容如下:

1
2
3
4
5
6
---
title: "搜索结果"
date: 1970-01-01T00:00:01+08:00
type: static
layout: search
---

日期设置为1970年的原因是搜索页面在输入关键词前,会显示最近更新的10篇文章。这样设置可以避免搜索的时候先显示了这篇文章。

然后,在/layouts下新建static文件夹,放入search.html。这个文件比较大,就不贴在这里了点这里下载

接下来是修改css,否则的话这个搜索丑丑的。我直接用了Algolia给的示例主题进行修改。文件太大我也不贴了,点这里下载之后放在/assets/css/_page中。记得修改_index.scss文件来引入_search.scss

最后,需要在导航栏中引入搜索页。这个超级简单,直接在config.toml中增加一项[[menu.main]]:

[[menu.main]]
  url = "/search/"
  name = "搜索"
  weight = 6
  pre = ""

完成之后,搜索部分就搞定啦!

3. PWA

PWA纯粹是,好玩……是我在看hugo-theme-meme的时候发现的。所以,具体过程参考这篇文章就行了。

说几个需要注意和修改的地方:

  1. 因为之前生成了algolia.json,如果内容多的话,这个文件还挺大的。虽然我不知道这个文件会不会被缓存,但是考虑到我其实并没有其他用到json的地方,所以我还是决定在gulpfile.js里面的globPatterns部分删掉了json。

  2. 我增加了Google的PWA Compact,可以自动根据manifest.webmanifest来生成各种东西支持其他浏览器。这么做需要修改/layouts/partials/head/link.html,增加下面这一行:

    1
    
    <script async src="https://cdn.jsdelivr.net/npm/pwacompat@2.0.10/pwacompat.min.js" integrity="sha384-I1iiXcTSM6j2xczpDckV+qhhbqiip6FyD6R5CpuqNaWXvyDUvXN5ZhIiyLQ7uuTh" crossorigin="anonymous"></script>
    

    记得检查一下,link.html里面那个manifest文件的文件名是否和/static里面的文件名一致。具体叫manifest.json还是site.webmanifest还是manifest.webmanifest无所谓,只要文件名一致就行。

  3. 注册Service Worker和提醒的代码我放在了/layouts/partials/footer.html里面,照抄就行。

  4. CSS脚本我原封不动地放在了/assets/css/_core/_pwa.scss里面,然后记得修改/assets/css/style.template.scss来引入增加的scss文件。

老规矩,测试通过之后可以交给Netlify。还是修改netlify.toml文件,增加命令。和Algolia搜索的相关命令整合之后,这个文件现在长这样:

1
2
3
[build]
publish = "public"
command = "hugo --gc --minify && npm install workbox-build gulp gulp-uglify readable-stream uglify-es atomic-algolia --save-dev && ./node_modules/gulp/bin/gulp.js build && npm run algolia"

更新成功之后,可以用Lighthouse来检查一下。如果提示有任何错误,修正即可。

4. Disqus代理

由于众所周知的原因,国内现在访问不了disqus,和一些其他的网站了。所以除了科学上网之外,还需要科学评论……采用的方案是SukkaW/DisqusJS。这是一个超轻量级的代理,可以自动检测访客的Disqus可用性,自动选择加载原生Disqus(评论完整模式)和DisqusJS提供的评论基础模式。

Disqus代理的另外一个选项是szhielelp/disqus-proxy。这个方案的优点是可以匿名评论,可是deal breaker是他用了bootstrap CSS,会和主题的CSS冲突。因此作为一个懒人,我断然决定放弃。

DisqusJS部署起来超级简单,我后端用ZEIT Now,只需要一个now.json文件就行了。不需要下载now的教程参照这里。看上去只需要部署一次,一劳永逸。

前端的话,也不复杂,只需要在config.toml里面增加几个变量,然后动一动/layouts/partials/comment.html这个文件就行了。

config.toml增加的变量:

1
2
3
4
5
6
7
8
[params.comment.disqus]
  enable = true 
  proxy = true
  shortname = 
  api =  
  apikey = 
  admin = 
  adminLabel = 

变量说明:

变量名 说明
enable 是否启用Disqus评论系统(true/false)
proxy 是否启用DisqusJS代理(true/false)
注意:如果没有在上面启用Disqus评论系统,那么即使这里设置为true,DisqusJS也不会启用
shortname 你的Disqus Forum的shortname,你可以在Disqus Admin - Settings - General - Shortname获取你的shortname
api 搭建的后端api地址(https://xxx.now.sh/)
apikey Disqus Application中API Key
admin 你的站点的Disqus Moderator的用户名(也就是你的用户名)。你可以在Disqus - Settings - Account - Username获取你的Username
adminLabel 你想显示在Disqus Moderator Badge中的文字。该配置应和Disqus Admin - Settings - Community - Moderator Badge Text相同

/layouts/partials/comment.html的修改:在<div id="disqus_thread"></div>下面插入这段代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{{- if .Site.Params.comment.disqus.proxy -}}
	<script src="https://cdn.jsdelivr.net/npm/disqusjs@1.2/dist/disqus.js"></script>
	<script>
		var dsqjs = new DisqusJS({
			shortname: {{ .Site.Params.comment.disqus.shortname }},
			siteName: {{ .Site.Params.title }},
			identifier: {{ if .Params.identifier }}{{ trim .Params.identifier "/" }}{{ else }}{{ trim .RelPermalink "/" }}{{end}},
			url: {{ if .Params.identifier }}"{{ trim .Site.BaseURL "/" }}{{ .Params.identifier }}"{{ else }}{{ .Permalink }}{{end}},
			title: {{ .Title }},
			api: {{ .Site.Params.comment.disqus.api }},
			apikey: {{ .Site.Params.comment.disqus.apikey }},
			admin: {{ .Site.Params.comment.disqus.admin }},
			adminLabel: {{ .Site.Params.comment.disqus.adminLabel }}
		});
	</script>
{{- else -}}

这里没有引入DisqusJS默认CSS的原因是,LoveIt会根据系统设置启用夜晚模式,而夜晚模式下DisqusJS的默认颜色就比较刺眼了。解决方式是下载这个文件,保存到/assets/css/_partial/_single/,最后在同目录下的_comment.scss最后增加一句代码@import "_disqusjs.scss";来引入修改之后的scss文件。

需要提一下的是,默认在development环境下LoveIt是不会渲染Disqus的。所以要么直接在production里面测试,要么暂时把{{- if $scratch.Get "production" | and (ne .Site.Params.comment.enable false) | and (ne .Params.comment false) -}}里面的production改成development。部署之前记得改回来就好。

5. 懒人方案

说了那么多,如果上面这些功能都想要的话,我在GitHub上面fork并修改了LoveIt的源码,直接用git submodule add --depth 1 https://github.com/dreamsafari/loveit.git themes/LoveIt,然后修改config.toml即可。

6. 后话

选择LoveIt这个主题,很大一部分的原因是因为丰富的Shortcodes支持。考虑到我目前心水hugo-theme-meme这个主题,所以估计接下来一段时间里面,如果有空的话,会尝试一下能不能把Shortcodes和这些功能移植到meme上面吧……关于这些功能在LoveIt上面的实现,如果有问题的话大家在下面留个言吧。

updatedupdated2020-04-102020-04-10