本篇记录我把Hexo博客主题更换为Landscape并自定义的过程。
更换主题
要选择一个充分发挥博客作用的主题,必须要显示足够多的内容(标签、分类、归档等),布局要清晰有条理,并符合读者浏览的惯性和预期。
以这个标准来看,Hexo的默认主题Landscape就非常好。同时也非常简洁,有利于把读者的注意力集中到真正有意义的内容上。
由于是默认主题,Hexo内置依赖项已经包含Landscape。但为了方便自定义,我把Landscape主题clone进/themes目录下。
clone进来后,新主题会变成本地库的内嵌子库,远程主题库可以跟踪其变化。
为了方便后续修改,我删除了/themes/landscape目录下的.git及相关文件,并清除本地git缓存,让本地库统一管理博客目录下的所有文件。
关于新主题的初步设置,我参考了并推荐这个tutorial系列。
自定义主题
Landscape算老主题了,写这篇博文的当下,该主题的/source目录最近更新已是2年前。
我在用目前的最新版v1.1.0,但有些老代码需要修改,又或者根据需要添加新功能。
显示文章更新日期
文章更新日期是很重要的信息,而Lanscape只显示发布日期。要显示更新日期,需要动2个文件:
修改
themes/landscape/layout/_partial/article.ejs参照已有的发布日期代码:
1
<%- partial('post/date', {class_name: 'article-date' , date_format: null}) %>
我们可以写一个更新日期的代码,并放在你想要显示的位置:
1
<%- partial('post/update-date', {class_name: 'article-date' , date_format: null}) %>
post/update-date指的是引用当前文件article.ejs所在的/_partial目录下的post/update-date.ejs文件。添加
themes/landscape/layout/_partial/post/update-date.ejs同样地,参照
themes/landscape/layout/_partial/post/date.ejs:1
2
3
4
5<span class="<%= class_name %>">
<time class="dt-published" datetime="<%= date_xml(post.date) %>" itemprop="datePublished">
<%= date(post.date, date_format) %>
</time>
</span>我们把以下代码写入
update-date.ejs文件:1
2
3
4
5
6
7<% if (post.published && post.updated && post.updated.isAfter(post.date, 'day')) { %>
<span class="<%= class_name %>">
<time class="dt-updated" datetime="<%= date_xml(post.updated) %>" itemprop="dateModified">
更新于 <%= date(post.updated, date_format) %>
</time>
</span>
<% } %>表示已发布文章的
updated有值、并且以日期粒度比date晚时,才显示更新日期。一般来说,
post.updated的值就算没写在文章的front-matter里也可以自动拿到。
但这样一来,就算你只是加个空格,它都会判断你更新了。所以建议自己手动在front-matter设置updated:1
2
3
4
5---
title: "abc"
date: 2022-04-26
updated: 2022-04-26
---
修改新旧文章导航链接顺序
按照一贯的逻辑,文章底部的新旧文章导航链接的显示应该类似:
← 旧帖 新帖 →
但是你可以在Landscape的官方demo里看到,这个顺序是反过来的:
← newer older →
当我发现这个bug时,be like这是在…?
它不是error报错,但我惊讶于它被设计得如此不符合用户习惯,以至于我可以称之为bug 
我猜是为了在小屏显示时,让older自动跑到newer下面。
这样确实比较合理,但我们大多是用电脑看博客,所以有点因小失大了。
而在themes/landscape/layout/_partial/post/nav.ejs文件的源码中,post.next的值是旧帖,post.prev的值是新帖。
这怎么不算震撼人心呢 
所以要改正这个顺序,就要修改2组数据:prev/next和older/newer。
prev/next控制文章标题的顺序
为了不在改代码时变得左右不分,我先定义了可读性更高的变量

1
2
3
4<%
const previousPost = post.next; // 旧
const nextPost = post.prev; // 新
%>之后就是置换上述
nav.ejs文件里对应的值:post.prev→previousPostpost.next→nextPostolder/newer控制文章标题上方说明文字的顺序和内容
为了提高代码可读性,先把上述
nav.ejs文件中的”older”和”newer”互换。改文字顺序:
来到themes/landscape/source/css/_partial/article.styl文件,同样把”older”和”newer”互换。改文字内容:
在themes/landscape/languages/你的语言.yml里修改newer和older的值,我分别改成了Newer和Older。
这样就改好宽屏下的导航链接顺序了。
但是这样一来,到了窄屏就会变成older在上newer在下,而用户习惯看到新文章在上。
这时只需要在上述article.styl文件中添加以下代码:
1 | #article-nav |
这表示#article-nav在窄屏时变成flex容器,并利用子项的order属性来调整older/newer的位置顺序。
添加Waline评论系统
Landscape支持Valine和Disqus评论系统。但Valine已经不能注册了,而Disqus则有广告。
我希望博客评论区支持匿名评论,并且干净简洁无广告,所以我决定导入Waline评论系统。
这是主题自定义里比较花时间的一环,但是我的博客值得 
首先按照Waline官方指南,把评论系统部署到Vercel上。
接着把部署好的评论系统集成到Landscape主题,这需要改动4个文件:
在
themes/landscape/layout/_partial/article.ejs添加留言计数器和评论区域:1
2
3
4
5
6
7// 在footer元素内添加留言计数器
<% if (post.comments && theme.waline.enable && theme.waline.serverURL) { %>
<a href="<%- url_for(post.path) %>#waline" class="article-comment-link">
<span class="post-comments-count waline-comment-count fa fa-comment" data-path="<%= url_for(post.path) %>" itemprop="commentCount"></span>
<%= __('comment') %>
</a>
<% } %>1
2
3
4
5
6// 添加评论区域
<% if (!index && post.comments && theme.waline.enable && theme.waline.serverURL){ %>
<section id="comments">
<div id="waline"></div>
</section>
<% } %>在
themes/landscape/layout/_partial/head.ejs的head元素内引入Waline的css:1
2
3<% if (theme.waline.enable && theme.waline.serverURL){ %>
<%- css('https://unpkg.com/@waline/client@3.13.0/dist/waline.css') %>
<% } %>在
themes/landscape/_config.yml中配置参数:1
2
3
4# Waline comment system
waline:
enable: true
serverURL: https://example.domain.com在
themes/landscape/layout/_partial/after-footer.ejs初始化评论区和计数器:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<% if (theme.waline.enable && theme.waline.serverURL){ %>
<script type="module">
import { init, commentCount } from 'https://unpkg.com/@waline/client@v3/dist/waline.js';
// 等DOM完全加载后再执行,确保所有留言计数器都存在页面中
document.addEventListener('DOMContentLoaded', function () {
// 文章页(含#waline元素):init + commentCount
if (document.getElementById('waline')) {
// 初始化评论区
init({
el: '#waline',
serverURL: theme.waline.serverURL,
});
}
// 主页:commentCount
// 初始化计数器
commentCount({
serverURL: theme.waline.serverURL,
selector: '.waline-comment-count'
});
});
</script>
<% } %>
这样就算集成好了。
但我还想修改评论区的样式,因为Waline评论区的主题色是绿色,而博客的主题色是蓝色。
为了方便博客样式的统一管理,我把Waline的CSS文件下载到themes/landscape/source/external/目录下。
用在线CSS Beautifier格式化css文件,改好样式后再用VScode的Minify插件压缩,最后更新上述head.ejs文件的CSS路径即可。
导入站内表情包
仅用文字就能表达微妙情绪是作家的本事。而我文笔拙劣,没有表情包简直寸步难行 
这里说的站内表情包是写博客时用的,如果想在Waline评论区设置自定义表情包,参考这个官方文档。
导入站内表情包后,可直接用:表情包名:的格式输出到博文。要达到这个目标,需要走5步:
添加表情包图片文件
这里假设添加到
source/images/stickers目录。之所以用本地表情包,是因为网络图片有可能被撤走,也不好浪费人家的带宽。
但可以把表情包目录部署到其它可访问的地址,这里不作讨论。注册
hexo filter,让:表情包名字:自动转换为该表情包的img元素新建
themes/landscape/scripts/sticker.js文件,参考以下代码: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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47const fs = require('fs');
const path = require('path');
// 支持的文件格式
const exts = ['gif', 'png', 'jpg', 'webp'];
let stickerMap = null;
function loadStickers(hexo) {
if (stickerMap) return stickerMap;
// 表情包目录
const dir = path.join(hexo.source_dir, 'images/stickers');
stickerMap = new Map();
if (!fs.existsSync(dir)) return stickerMap;
// 读取 dir 文件夹里的所有文件名,返回数组
const files = fs.readdirSync(dir);
for (const file of files) {
const ext = path.extname(file).slice(1).toLowerCase(); // 获取文件后缀
const name = path.basename(file, path.extname(file)); // 去掉后缀的文件名
// 判断文件格式是否支持,是则塞入map
if (exts.includes(ext)) {
stickerMap.set(name, file);
}
}
return stickerMap;
}
hexo.extend.filter.register('before_post_render', function (data) {
const stickers = loadStickers(hexo);
// match = ":cat:" 整个匹配到的字符串
// name = "cat" 正则表达式括号里的内容
data.content = data.content.replace(/:([a-zA-Z0-9_-]+):/g, (match, name) => {
const file = stickers.get(name); // 获取 文件名.后缀
if (!file) return match; // 找不到则显示原文字内容
return `<img src="/images/stickers/${file}" class="sticker" alt="${name}" loading="lazy">`;
});
return data;
});这段代码先扫描表情包目录,找到符合指定格式的文件,生成一个以
文件名: 文件名.后缀为键值对的map,然后在文章中找:表情包名:并在map里找到它的后缀,返回相应的img元素。设置表情包样式
这时已经可以用
:表情包名:的格式输出表情包了,但需要调整一下样式。新建
themes/landscape/source/css/_partial/sticker.styl(注意上一步返回的img元素有.sticker类):1
2
3img.sticker
display: inline-block // 和文字同一行
vertical-align: text-bottom // 垂直方向上的对齐位置要想确保表情包不会过大,也可以添加
max-height等属性。在
themes/landscape/source/css/style.styl导入新样式,让其生效:1
@import "_partial/sticker"
防止表情包进入
fancybox画廊Landscape会扫描文章里的
img元素,并用一个a元素将其包裹,让它可以被点击进入fancybox画廊里浏览。但表情包不需要这个功能(到底是要放进去欣赏什么
在
themes/landscape/source/js/script.js进行以下修改:1
2
3
4
5
6
7// 修改前
$('.article-entry').each(function(i){
$(this).find('img').each(function(){
// 修改后
$('.article-entry').each(function(i){
$(this).find('img').not('.sticker').each(function(){这样就能避免
fancybox处理带.sticker类的img元素。更新
source/robots.txt,禁止搜索引擎抓取表情包表情包只是起修饰作用,不需要展示在搜索结果里。在
robots.txt文件中添加:1
Disallow: /images/stickers/
注册带色文字hexo tag
总有些时候需要用不同颜色来突出一段文字,或是赋予它特殊含义。
比起每次都手写内联css样式的html,我选择注册hexo tag,以便使用 {% tagname 颜色 文字内容 %} 的格式输出带色文字。
新建themes/landscape/scripts/colortxt.js文件,注册colortxt标签,参考以下代码:
1 | hexo.extend.tag.register('colortxt', function(args){ |
这段代码支持使用{% colortxt 颜色 文字内容 %}或{% colortxt 文字内容 %}输出带色文字,没指定颜色则默认红色。
可用的颜色参数有:red, blue, green, yellow, orange, black, purple, pink, brown, gray
要注意,假设你想输出默认红字的blue sky,若写成{% colortxt blue sky %},实际会输出sky。
因为blue是colors数组里的颜色参数,它以为你想让”sky”变蓝。
所以当文字内容的开头是颜色参数,但实际想输出默认红字时,需要用英文引号"包起文字内容":{% colortxt "blue sky" %} → blue sky
行内代码自动换行
Landscape居然行内代码不会自动换行?!!
这还是我在写这篇博文时发现的bug!!
谁懂我看到行内代码在窄屏下像剑一样笔直刺穿border时的感受 
你贵为一个知名开源项目的默认主题
你居然…居然…你…

回归正题,在themes/landscape/source/css/_partial/highlight.styl文件里加一行代码即可:
1 | .article-entry |
写作工具
如果你一路跟下来并完成上面的tutorial,恭喜你!你做到了!!我也做到了!!!
至于工具,就在于让写博客变得简单方便。
我开始用Typora,这篇博文就是我用它来写的。
比起自己从头到尾吭哧吭哧写markdown,直接设置格式的感觉很不错 