博客主题迁移笔记

  • ~34.07K 字
  • 次阅读
  • 条评论
  1. 1. STEP 0. 了解新的主题配置结构
  2. 2. STEP 1. 迁移背景图、封面图等
  3. 3. STEP 2. 下雪效果
  4. 4. STEP 3. 添加小波浪
  5. 5. STEP 4. 小动画
  6. 6. STEP 5. 个性化 style
  7. 7. STEP 6. 气泡通知
  8. 8. STEP 7. 评论!!
  9. 9. STEP 8. 页脚的统计信息
  10. 10. STEP 9. 友链页面
  11. 11. STEP 10. 其他小修小改

本文记录一下博客将 Kratos Rebirth 迁移到 v3 的过程。

这次迁移最大的改动就是所有自定义内容与 theme 文件彻底分离,以前写一些东西时对 Kratos 做了很多侵入式修改,现在统统插件化。
这个文章就带你看看我是如何一一实现之前的自定义内容的。

首先自然是把新版主题 clone 下来,搭建一个测试环境……这个过程就不讲了,反正得到一个纯原版的 Kratos。

STEP 0. 了解新的主题配置结构

其实 Hexo 很早就这么做了,以前就是 theme/{主题名} 下有一个 _config.yml 文件,所有主题配置都在里面。
而新的主题配置结构改成了,theme/{主题名} 下有一个 _config.yml 文件,Hexo 目录还有一个 _config.{theme 名}.yml 文件。

二者关系是:
_config.yml 提供所有能配置的选项,并给出一个默认值,而 _config.{theme 名}.yml 只包含需要修改的配置,并覆盖 _config.yml 里的配置。

这样的好处是,当 _config.yml 内容有点多的时候,用户往往只会修改其中一部分配置,这部分配置分离到 _config.{theme 名}.yml 中可以简化修改配置的难度。

当然,如果你愿意你当然可以全改在_config.yml

STEP 1. 迁移背景图、封面图等

以前我是把这些文件直接塞主题目录下的 source/images 文件夹内,现在就放在 Hexo 目录的 source/images 内。对,就是那个有_posts 的 source 文件夹。
这样放与之前是等效的,可以避免到处修改 url。

随机封面图在以前是主题内置功能,现在被改成插件形式,Kratos 的插件其实就是外置 js/css。在主题配置中,Kratos 提供了三个直接插入 js/css 的位置:

1
2
3
4
5
6
7
8
9
10
additional_injections:
head: |
<link rel="stylesheet" href="/custom/css/common.css"/>
# ...
footer: |
# ...
after_footer: |
<script src="/custom/js/snow.js"></script>
<script src="https://unpkg.com/aplayer@1.10.1/dist/APlayer.min.js"></script>
# ...

于是我们可以在 Hexo 目录的 source 文件夹内创建一个 custom 文件夹,里面放上自定义的 js/css,就可以通过类似 <link rel="stylesheet" href="/custom/css/common.css"/> 载入我们的文件。

参考随机文章封面图,写出下面的代码:

random-post-cover.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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
(() => {
// 请设置为您在配置中配置的默认封面路径
const defaultCoverSrc = "/images/default.webp";
// 请设置为您的随机文件基础路径({no} 替代编号,从 1 开始)
const randomImageSrcTemplate = "/images/thumb/thumb_B{no}.webp";
// 请设置为您的随机图片文件数量
const randomImageCount = 9;

// 初始化暂存目录,使用减员随机来减少连续重复可能导致的糟糕体验
const usedImages = new Array(randomImageCount);

// 初始化随机函数
const generateNewCoverID = () => {
let remailFailCounts = 2; // 设置最大随机失败介入次数
let imageNo;
while (remailFailCounts > 0) {
// 随机挑选一个
imageNo = Math.floor(Math.random() * randomImageCount);
if (!usedImages[imageNo]) {
// 有效
break;
} else {
// 无效,重试
remailFailCounts--;
}
}

if (remailFailCounts <= 0) {
// 随机失败了,寻找最近的未使用元素
imageNo = -1;
for (let i = 0; i < randomImageCount; i++) {
if (!usedImages[i]) {
// 找到了
imageNo = i;
break;
}
}
if (imageNo === -1) {
// 全都被使用过了,清空重置
for (let i = 0; i < randomImageCount; i++) {
usedImages[i] = false;
}
// 随机挑选一个
imageNo = Math.floor(Math.random() * randomImageCount);
}
}

// 标记为已经使用过的
usedImages[imageNo] = true;

// 返回 +1 来从 1 开始
return imageNo + 1;
};

// 替换所有使用默认图片的元素
const randAll = () => {
const allDefaultImageEls = document.querySelectorAll(
`img.kratos-entry-thumb-img[src='${defaultCoverSrc}']`
);
for (const el of allDefaultImageEls) {
el.setAttribute("src", randomImageSrcTemplate.replace("{no}", generateNewCoverID().toString()));
}
};

// 调用一次
randAll();
// 在 PJAX 之后再调用一次
window.addEventListener("pjax:complete", randAll);
})();

然后添加到主题配置中:

1
2
3
additional_injections:
after_footer: |
<script src="/custom/js/random-post-cover.js"></script>

STEP 2. 下雪效果

同理,我们可以参考下雪配置下雪效果,但是还是有一点问题,这个没有提供下雪切换按钮,但是不要紧,我们可以参考 v2 修改修改代码:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
(() => {
// 设置雪花参数
const snowConf = {
flakeCount: 100,
minDist: 150,
color: "255, 255, 255",
size: 2,
speed: 0.5,
opacity: 0.3,
stepsize: 0.5,
};

// 记录下雪状态
let isSnowing = true;

const requestAnimationFrame =
window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60);
};
window.requestAnimationFrame = requestAnimationFrame;
const canvas = document.getElementById("snow");
const ctx = canvas.getContext("2d");
const flakeCount = snowConf.flakeCount;
let mX = -100,
mY = -100;
let flakes = [];
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const snow = () => {
if (!isSnowing) {
return; // 结束
}

// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);

const minDist = snowConf.minDist;
for (let i = 0; i < flakeCount; i++) {
let flake = flakes[i];
const x = mX,
y = mY;
const x2 = flake.x,
y2 = flake.y;
const dist = Math.sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2));
if (dist < minDist) {
const force = minDist / (dist * dist);
const xcomp = (x - x2) / dist;
const ycomp = (y - y2) / dist;
const deltaV = force / 2;
flake.velX -= deltaV * xcomp;
flake.velY -= deltaV * ycomp;
} else {
flake.velX *= 0.98;
if (flake.velY < flake.speed && flake.speed - flake.velY > 0.01) {
flake.velY += (flake.speed - flake.velY) * 0.01;
}
flake.velX += Math.cos((flake.step += 0.05)) * flake.stepSize;
}
ctx.fillStyle = "rgba(" + snowConf.color + ", " + flake.opacity + ")";
flake.y += flake.velY;
flake.x += flake.velX;
if (flake.y >= canvas.height || flake.y <= 0) {
reset(flake);
}
if (flake.x >= canvas.width || flake.x <= 0) {
reset(flake);
}
ctx.beginPath();
ctx.arc(flake.x, flake.y, flake.size, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(snow);
};
const reset = (flake) => {
flake.x = Math.floor(Math.random() * canvas.width);
flake.y = 0;
flake.size = Math.random() * 3 + 2;
flake.speed = Math.random() * 1 + 0.5;
flake.velY = flake.speed;
flake.velX = 0;
flake.opacity = Math.random() * 0.5 + 0.3;
};
// 初始化函数
const init = () => {
// 判断当前是否应该下雪
const isSnowDisabled = localStorage.getItem("kr-disable-snow") !== null;
if (isSnowDisabled || window.kr?.notMobile === false) {
// 用户禁用了或者是移动端,就不下雪了
isSnowing = false;
canvas.classList.add("disabled");
return;
}

startSnow();
};
const startSnow = () => {
// 生成初始雪花
for (let i = 0; i < flakeCount; i++) {
const x = Math.floor(Math.random() * canvas.width);
const y = Math.floor(Math.random() * canvas.height);
const size = Math.random() * 3 + snowConf.size;
const speed = Math.random() * 1 + snowConf.speed;
const opacity = Math.random() * 0.5 + snowConf.opacity;
flakes.push({
speed: speed,
velX: 0,
velY: speed,
x: x,
y: y,
size: size,
stepSize: (Math.random() / 30) * snowConf.stepsize,
step: 0,
angle: 180,
opacity: opacity,
});
}
// 开始下雪
snow();
};
// 雪花避让鼠标
document.addEventListener("mousemove", (e) => {
(mX = e.clientX), (mY = e.clientY);
});
// 窗口大小调整
window.addEventListener("resize", () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
});

// 切换雪花按钮
const darkSwicth = document.getElementById("theme-toggle");
darkSwicth.insertAdjacentHTML(
"afterend",
`<div class="box theme-box" id="snow-switch"><span class="fa fa-snowflake-o"></span></div>`
);
document.getElementById("snow-switch")?.addEventListener("click", () => {
if (isSnowing) {
setTimeout(() => {
isSnowing = false;
}, 600);
localStorage.setItem("kr-disable-snow", true);
canvas.classList.add("disabled");
} else {
isSnowing = true;
localStorage.removeItem("kr-disable-snow");
canvas.classList.remove("disabled");
startSnow();
}
});

// 初始化
init();
})();

以上添加了插入切换按钮的代码。

此外还有些鼠标点击特效、aplayer 等由于都没有额外修改,直接按文档来,就不讲了。

STEP 3. 添加小波浪

在 banner 和页面主体间有一个小波浪,同样的先找个位置插入 html:

wave.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
(() => {
const kratos_start = document.querySelector("#kratos-page #kratos-blog-post");
kratos_start.insertAdjacentHTML(
"beforebegin",
`<div class="preview-overlay">
<svg id="gentle-wave-svg" class="preview-waves hide-wave" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
<defs>
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z"></path>
</defs>
<g class="preview-parallax">
<use xlink:href="#gentle-wave" x="48" y="0" fill="var(--gentle-wave1)"></use>
<use xlink:href="#gentle-wave" x="48" y="3" fill="var(--gentle-wave2)"></use>
<use xlink:href="#gentle-wave" x="48" y="5" fill="var(--gentle-wave3)"></use>
<use xlink:href="#gentle-wave" x="48" y="7" fill="var(--gentle-wave)"></use>
</g>
</svg>
</div>`
);

const darkmodeCss = document.getElementById("dark-waves-css");
const waveSVG = document.getElementById("gentle-wave-svg");

const refreshColor = () => {
if (document.documentElement.getAttribute("data-theme") === "dark") {
darkmodeCss.setAttribute("media", "all");
} else {
darkmodeCss.setAttribute("media", "(prefers-color-scheme: dark)");
}
waveSVG.classList.remove("hide-wave");
waveSVG.classList.add("wave-colors");
};

const initColor = () => {
document.removeEventListener("load", initColor);
document.querySelector("#theme-toggle").addEventListener("click", refreshColor);
refreshColor();
};

// 适配夜间模式
if (document.readyState === "complete") {
setTimeout(initColor);
} else {
document.addEventListener("DOMContentLoaded", initColor);
window.addEventListener("load", initColor);
}
})();

然后两个 css 文件:

light-waves.css
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
.wave-colors {
--gentle-wave1: rgba(245, 245, 245, 0.7);
--gentle-wave2: rgba(245, 245, 245, 0.5);
--gentle-wave3: rgba(245, 245, 245, 0.3);
--gentle-wave: rgb(245, 245, 245);
}

.hide-wave {
--gentle-wave1: #ffffff00;
--gentle-wave2: #ffffff00;
--gentle-wave3: #ffffff00;
--gentle-wave: #ffffff00;
}

.preview-overlay {
width: 100%;
position: relative;
left: 0;
right: 0;
z-index: 0;
}

.preview-overlay .preview-waves {
position: relative;
width: 100%;
height: 1vh;
min-height: 101px;
max-height: 150px;
}

.preview-overlay .preview-waves use {
transition: fill 0.3s ease-in-out;
}

@media (max-width: 768px) {
.preview-overlay {
display: none;
}
}

.preview-overlay .preview-parallax > use {
animation: move-forever 25s cubic-bezier(0.55, 0.5, 0.45, 0.5) infinite;
}

.preview-overlay .preview-parallax > use:nth-child(1) {
animation-delay: -2s;
animation-duration: 7s;
}

.preview-overlay .preview-parallax > use:nth-child(2) {
animation-delay: -3s;
animation-duration: 10s;
}

.preview-overlay .preview-parallax > use:nth-child(3) {
animation-delay: -4s;
animation-duration: 13s;
}

.preview-overlay .preview-parallax > use:nth-child(4) {
animation-delay: -5s;
animation-duration: 20s;
}

@-moz-keyframes move-forever {
0% {
transform: translate3d(-90px, 0, 0);
}

100% {
transform: translate3d(85px, 0, 0);
}
}

@-webkit-keyframes move-forever {
0% {
transform: translate3d(-90px, 0, 0);
}

100% {
transform: translate3d(85px, 0, 0);
}
}

@-o-keyframes move-forever {
0% {
transform: translate3d(-90px, 0, 0);
}

100% {
transform: translate3d(85px, 0, 0);
}
}

@keyframes move-forever {
0% {
transform: translate3d(-90px, 0, 0);
}

100% {
transform: translate3d(85px, 0, 0);
}
}

#banner {
overflow: hidden;
width: 110%;
height: 110%;
right: 5%;
}

.kratos-cover.kratos-hero-2 {
height: 300px;
}

.kratos-start.kratos-hero-2 {
height: 300px;
}

.kratos-cover.kratos-cover-2 {
height: 400px;
}
dark-waves.css
1
2
3
4
5
6
.wave-colors {
--gentle-wave1: rgba(24, 28, 39, 0.7);
--gentle-wave2: rgba(24, 28, 39, 0.5);
--gentle-wave3: rgba(24, 28, 39, 0.3);
--gentle-wave: rgb(24, 28, 39);
}

最后引入代码:

1
2
3
4
5
6
additional_injections:
head: |
<link rel="stylesheet" id="light-waves-css" href="/custom/css/light-waves.css" media="all"/>
<link rel="stylesheet" id="dark-waves-css" href="/custom/css/dark-waves.css" media="(prefers-color-scheme: dark)"/>
after_footer: |
<script src="/custom/js/waves.js"></script>

注意两个 css 相对顺序不能反。

STEP 4. 小动画

文章卡片有一个出现动画,这个要依靠 animate.css 实现:

1
2
3
additional_injections:
head: |
<link rel="stylesheet" id="animate-css" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"/>

然后再加一个 animated.js,在特定元素上加上 animate__xxx,并监听 pjax 事件,使得 pjax 刷新时,动画正常出现:

animated.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(() => {
const sideWidget = document.querySelectorAll(".sticky-area .widget");
sideWidget.forEach((element) => {
element.classList.add("animate__animated", "animate__fadeIn");
});
const animateArticle = () => {
const articleCard = document.querySelectorAll("article");
articleCard.forEach((element) => {
if (element.classList.contains("kratos-hentry"))
element.classList.add("animate__animated", "animate__zoomIn");
else element.classList.add("animate__animated", "animate__fadeIn");
});
};
window.addEventListener("pjax:complete", animateArticle);
animateArticle();
})();

然后把 animated.js 引入主题配置即可。

pjax:complete 是 kratos 提供的一个事件,所有 pjax 事件可参考文档

STEP 5. 个性化 style

我爱大圆角,我怕它完了?

我魔改过一部分主题的样式,v3 当然不能丢啦:

common.css
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/* ====================== border radius / blur ====================== */

#kratos-widget-area .widget.widget-kratos-about .photo-background {
border-radius: 5px 5px 0 0;
}

.kratos-entry-border-new .kratos-post-meta-new {
border-radius: 0 0 5px 5px;
}

.post-comments {
box-shadow: -3px 3px 4px 0px rgba(0, 0, 0, 0.15);
}

@media (min-width: 768px) {
#kratos-widget-area .widget,
.kratos-hentry,
.navigation div {
border-radius: 5px;
backdrop-filter: blur(6px);
}

#kratos-desktop-topnav {
backdrop-filter: blur(6px);
}
}

/* ====================== shadow ====================== */

#kratos-widget-area .widget {
box-shadow: -3px 3px 4px 0px rgba(0, 0, 0, 0.15);
}

.post-navigation .nav-previous,
.post-navigation .nav-next {
box-shadow: -3px 3px 4px 0px rgba(0, 0, 0, 0.15);
}

.kr-tool .box {
box-shadow: -3px 3px 4px 0px rgba(0, 0, 0, 0.15);
}

.kratos-hentry {
box-shadow: -3px 3px 4px 0px rgba(0, 0, 0, 0.15);
}

/* ====================== fit thumb ====================== */

.kratos-entry-thumb img {
object-fit: contain;
}

/* ====================== article header style ====================== */

article .kratos-page-inner .kratos-page-content h1,
article .kratos-page-inner .kratos-page-content h2,
article .kratos-page-inner .kratos-page-content h3,
article .kratos-page-inner .kratos-page-content h4,
article .kratos-page-inner .kratos-page-content h5,
article .kratos-page-inner .kratos-page-content h6 {
transition: all 0.3s ease-in-out;
}

article .kratos-page-inner .kratos-page-content h1:hover,
article .kratos-page-inner .kratos-page-content h2:hover,
article .kratos-page-inner .kratos-page-content h3:hover,
article .kratos-page-inner .kratos-page-content h4:hover,
article .kratos-page-inner .kratos-page-content h5:hover,
article .kratos-page-inner .kratos-page-content h6:hover {
color: #51aded; /* 给文章的h标签添加悬浮变色 */
}

/* ====================== sharing button color ====================== */

/* 分享按钮居然是绿色的,跟我网站色调不搭,得改!! */
#post-actions .share {
border-color: #51aded;
color: #51aded;
}

#post-actions .share:hover {
background-color: #51aded;
}

#kr-share-modal .kr-modal-header {
color: #51aded;
}

#kr-share-modal .kr-modal-header,
#kr-share-modal .kr-share-qr {
border-color: #51aded;
}

/* ====================== footer style ====================== */

#footer {
padding-bottom: 48px; /* 这个是给 aplayer 用的 */
background-image: linear-gradient(#24384b, #23282d);
}

然后加入到配置中即可。

STEP 6. 气泡通知

两年多前,我写过一个气泡通知提示效果。嘿,看看我当年写的什么寄吧。得好好大改一番:

bubble-notice.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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
(() => {
var bubbleNum = 0;
const typeMap = {
normal: "#cfe2ff",
problem: "#fff3cd",
err: "#f8d7da",
ok: "#d1e7dd",
};
const page = document.getElementById("kratos-wrapper");
page.insertAdjacentHTML("afterbegin", `<div id="bubble-noticer-list"></div>`);
const bubbleList = document.getElementById("bubble-noticer-list");

const refreshPosition = () => {
const bubbles = bubbleList.childNodes;
let stillCount = 0;
bubbles.forEach((bubble) => {
if (!bubble.classList.contains("bubbleNotice-hide")) {
bubble.style.top = `${60 + stillCount * 40}px`;
++stillCount;
}
});
};
const removeOne = () => {
const bubbles = bubbleList.childNodes;
let stillCount = 0;
let foundRemoved = false;
bubbles.forEach((bubble) => {
const hasRemoved = bubble.classList.contains("bubbleNotice-hide");
if (!foundRemoved && !hasRemoved) {
bubble.classList.add("bubbleNotice-hide");
bubble.style.top = "-40px";
setTimeout(() => {
bubbleList.removeChild(bubble);
refreshPosition();
}, 700);
foundRemoved = true;
} else if (!hasRemoved) {
bubble.style.top = `${60 + stillCount * 40}px`;
++stillCount;
}
});
};

const addBubble = (text, type, time) => {
const bg = typeMap[type];

bubbleList.insertAdjacentHTML(
"beforeend",
`<div class="bubbleNotice ${type}" style="background-color: ${bg}; top: ${
60 + bubbleList.childElementCount * 40
}px;">${text}</div>`
);
setTimeout(removeOne, time);
return bubbleNum;
};
window.addEventListener("pjax:before", () => {
addBubble("请求页面中", "normal", 3000);
});
window.addEventListener("pjax:complete", () => {
addBubble("请求成功", "ok", 3000);
});
window.addEventListener("pjax:error", () => {
addBubble("请求失败,即将刷新", "err", 3000);
});
})();
bubble-notice.css
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
47
48
49
50
51
@keyframes slideInRight {
from {
right: -230px;
}

to {
right: 0;
}
}

.bubbleNotice {
color: #666;
font-weight: bold;
position: fixed;
right: 0;
padding: 10px;
width: 150px;
min-height: 15px;
line-height: 15px;
font-size: 15px;
z-index: 1;
opacity: 0.8;
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
animation: ease-in-out 0.5s slideInRight;
transition: top 0.75s ease-in-out;
word-break: break-all;
}

.bubbleNotice::before {
font: 100%/1 FontAwesome;
margin-right: 10px;
}

.bubbleNotice.normal::before {
color: #084298;
content: "\f021";
}
.bubbleNotice.problem::before {
color: #664d03;
content: "\f071";
}

.bubbleNotice.err::before {
color: #842029;
content: "\f12a";
}
.ok::before {
color: #0f5132;
content: "\f00c";
}

STEP 7. 评论!!

本站使用的 waline,参考周边文档迁移还是比较简单的,可以说没有修改,其中这是 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
import { init, commentCount } from "https://unpkg.com/@waline/client@v3.1.3/dist/waline.js";
import { pageviewCount } from "https://unpkg.com/@waline/client@v3.1.3/dist/pageview.js";

(() => {
const serverURL = "https://blog-waline.ver.moe/";

const loadComments = async () => {
const container = document.getElementById("w-comments");
if (!!container) {
// 是文章或页面,完整加载 Waline
const path = container.getAttribute("data-path");
init({
el: container,
path,
dark: 'html[data-theme="dark"]',
serverURL,
pageview: true,
comment: true,
});
} else {
// 是首页,只展示页面访问和评论数量,不渲染评论区
pageviewCount({ serverURL, update: false });
commentCount({ serverURL });
}

if (localStorage.getItem("firstVisit") === null) {
// 站点的访问统计,仅生效一次
localStorage.setItem("firstVisit", false);
pageviewCount({ serverURL, path: "/" });
}
};

window.loadComments = loadComments;
window.addEventListener("pjax:success", () => {
window.loadComments = loadComments;
});
})();

主要修改是 css,要添加切换夜间模式的过度动画:

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
#w-comments .wl-reaction-list {
gap: 24px;
}

#w-comments .wl-reaction-img {
width: 72px;
height: 72px;
}

#w-comments .wl-reaction-votes {
font-size: 1em;
}

#w-comments,
#w-comments .wl-meta span {
transition: background-color 0.3s ease-in-out;
}

#w-comments .wl-panel,
#w-comments .wl-header {
transition-duration: 0.3s;
transition-timing-function: ease-in-out;
transition-delay: 0s;
transition-property: background-color border;
}

剩下的一样。

STEP 8. 页脚的统计信息

咱们页脚有 3 行统计信息:

1
2
3
站点访问量: 114, 站点访客数: 514, 页面访问量: 1919
您是本站的第 810 位访客
IP地址: 127.0.0.1, CDN节点: Youkai Mountain, Gensokyo - (2UN), IP归属地: JP

首先在页脚的配置添加:

1
2
3
4
5
6
7
8
footer:
components:
additional:
- - |
站点访问量: <span id="qexo-site-pv">Loading...</span>, 站点访客数: <span id="qexo-site-uv">Loading...</span>, 页面访问量: <span id="qexo-page-pv">Loading...</span>
- - 您是本站的第 <span data-path="/" class="waline-pageview-count"></span> 位访客
- - |
ip地址: <span id="ip">Loading...</span>, cdn节点: <span id="cdn">Loading...</span>, ip归属地: <span id="loc">Loading...</span>

当然,萌 icp 也是写在这里的哦:

1
2
3
4
footer:
components:
additional:
- - <a href="https://icp.gov.moe/?keyword=20226222" target="_blank">萌ICP备20226222号</a>

第一行是 qexo 的统计信息,找个地方写这个 js:

1
2
3
4
loadStatistic("https://blog-qexo.ver.moe");
window.addEventListener("pjax:complete", () => {
loadStatistic("https://blog-qexo.ver.moe");
});

当然啦,loadStatistic 是 qexo 库的函数,所以我们要引入:

1
2
3
additional_injections:
after_footer: |
<script src="https://cdn.jsdelivr.net/npm/qexo-static@2.3.1/hexo/statistic/statistic.js"></script>

第二行是 waline 的功能,只需要 footer 那一处就可以了

第三行是 cloudflare 的 cdn-cgi/trace 的功能,跟 qexo 的代码我一起放到了common.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
(() => {
loadStatistic("https://blog-qexo.ver.moe");
window.addEventListener("pjax:complete", () => {
loadStatistic("https://blog-qexo.ver.moe");
});
const getCDNinfo = () => {
fetch("https://blog.ver.moe/cdn-cgi/trace")
.then((response) => response.text())
.then((data) => {
const areas =
"Antananarivo, Madagascar - (TNR);Cape Town, South Africa - (CPT);Casablanca, Morocco - (CMN);Dar Es Salaam, Tanzania - (DAR);Djibouti City, Djibouti - (JIB);Durban, South Africa - (DUR);Johannesburg, South Africa - (JNB);Kigali, Rwanda - (KGL);Lagos, Nigeria - (LOS);Luanda, Angola - (LAD);Maputo, MZ - (MPM);Mombasa, Kenya - (MBA);Port Louis, Mauritius - (MRU);Réunion, France - (RUN);Bangalore, India - (BLR);Bangkok, Thailand - (BKK);Bandar Seri Begawan, Brunei - (BWN);Cebu, Philippines - (CEB);Chengdu, China - (CTU);Chennai, India - (MAA);Chittagong, Bangladesh - (CGP);Chongqing, China - (CKG);Colombo, Sri Lanka - (CMB);Dhaka, Bangladesh - (DAC);Dongguan, China - (SZX);Foshan, China - (FUO);Fuzhou, China - (FOC);Guangzhou, China - (CAN);Hangzhou, China - (HGH);Hanoi, Vietnam - (HAN);Hengyang, China - (HNY);Ho Chi Minh City, Vietnam - (SGN);Hong Kong - (HKG);Hyderabad, India - (HYD);Islamabad, Pakistan - (ISB);Jakarta, Indonesia - (CGK);Jinan, China - (TNA);Karachi, Pakistan - (KHI);Kathmandu, Nepal - (KTM);Kolkata, India - (CCU);Kuala Lumpur, Malaysia - (KUL);Lahore, Pakistan - (LHE);Langfang, China - (NAY);Luoyang, China - (LYA);Macau - (MFM);Malé, Maldives - (MLE);Manila, Philippines - (MNL);Mumbai, India - (BOM);Nagpur, India - (NAG);Nanning, China - (NNG);New Delhi, India - (DEL);Osaka, Japan - (KIX);Phnom Penh, Cambodia - (PNH);Qingdao, China - (TAO);Seoul, South Korea - (ICN);Shanghai, China - (SHA);Shenyang, China - (SHE);Shijiazhuang, China - (SJW);Singapore, Singapore - (SIN);Suzhou, China - (SZV);Taipei - (TPE);Thimphu, Bhutan - (PBH);Tianjin, China - (TSN);Tokyo, Japan - (NRT);Ulaanbaatar, Mongolia - (ULN);Vientiane, Laos - (VTE);Wuhan, China - (WUH);Wuxi, China - (WUX);Xi'an, China - (XIY);Yerevan, Armenia - (EVN);Zhengzhou, China - (CGO);Zuzhou, China - (CSX);Amsterdam, Netherlands - (AMS);Athens, Greece - (ATH);Barcelona, Spain - (BCN);Belgrade, Serbia - (BEG);Berlin, Germany - (TXL);Brussels, Belgium - (BRU);Bucharest, Romania - (OTP);Budapest, Hungary - (BUD);Chișinău, Moldova - (KIV);Copenhagen, Denmark - (CPH);Cork, Ireland - (ORK);Dublin, Ireland - (DUB);Düsseldorf, Germany - (DUS);Edinburgh, United Kingdom - (EDI);Frankfurt, Germany - (FRA);Geneva, Switzerland - (GVA);Gothenburg, Sweden - (GOT);Hamburg, Germany - (HAM);Helsinki, Finland - (HEL);Istanbul, Turkey - (IST);Kyiv, Ukraine - (KBP);Lisbon, Portugal - (LIS);London, United Kingdom - (LHR);Luxembourg City, Luxembourg - (LUX);Madrid, Spain - (MAD);Manchester, United Kingdom - (MAN);Marseille, France - (MRS);Milan, Italy - (MXP);Moscow, Russia - (DME);Munich, Germany - (MUC);Nicosia, Cyprus - (LCA);Oslo, Norway - (OSL);Paris, France - (CDG);Prague, Czech Republic - (PRG);Reykjavík, Iceland - (KEF);Riga, Latvia - (RIX);Rome, Italy - (FCO);Saint Petersburg, Russia - (LED);Sofia, Bulgaria - (SOF);Stockholm, Sweden - (ARN);Tallinn, Estonia - (TLL);Thessaloniki, Greece - (SKG);Vienna, Austria - (VIE);Vilnius, Lithuania - (VNO);Warsaw, Poland - (WAW);Zagreb, Croatia - (ZAG);Zürich, Switzerland - (ZRH);Arica, Chile - (ARI);Asunción, Paraguay - (ASU);Bogotá, Colombia - (BOG);Buenos Aires, Argentina - (EZE);Curitiba, Brazil - (CWB);Fortaleza, Brazil - (FOR);Guatemala City, Guatemala - (GUA);Lima, Peru - (LIM);Medellín, Colombia - (MDE);Panama City, Panama - (PTY);Porto Alegre, Brazil - (POA);Quito, Ecuador - (UIO);Rio de Janeiro, Brazil - (GIG);São Paulo, Brazil - (GRU);Santiago, Chile - (SCL);Willemstad, Curaçao - (CUR);St. George's, Grenada - (GND);Amman, Jordan - (AMM);Baghdad, Iraq - (BGW);Baku, Azerbaijan - (GYD);Beirut, Lebanon - (BEY);Doha, Qatar - (DOH);Dubai, United Arab Emirates - (DXB);Kuwait City, Kuwait - (KWI);Manama, Bahrain - (BAH);Muscat, Oman - (MCT);Ramallah - (ZDM);Riyadh, Saudi Arabia - (RUH);Tel Aviv, Israel - (TLV);Ashburn, VA, United States - (IAD);Atlanta, GA, United States - (ATL);Boston, MA, United States - (BOS);Buffalo, NY, United States - (BUF);Calgary, AB, Canada - (YYC);Charlotte, NC, United States - (CLT);Chicago, IL, United States - (ORD);Columbus, OH, United States - (CMH);Dallas, TX, United States - (DFW);Denver, CO, United States - (DEN);Detroit, MI, United States - (DTW);Honolulu, HI, United States - (HNL);Houston, TX, United States - (IAH);Indianapolis, IN, United States - (IND);Jacksonville, FL, United States - (JAX);Kansas City, MO, United States - (MCI);Las Vegas, NV, United States - (LAS);Los Angeles, CA, United States - (LAX);McAllen, TX, United States - (MFE);Memphis, TN, United States - (MEM);Mexico City, Mexico - (MEX);Miami, FL, United States - (MIA);Minneapolis, MN, United States - (MSP);Montgomery, AL, United States - (MGM);Montréal, QC, Canada - (YUL);Nashville, TN, United States - (BNA);Newark, NJ, United States - (EWR);Norfolk, VA, United States - (ORF);Omaha, NE, United States - (OMA);Philadelphia, United States - (PHL);Phoenix, AZ, United States - (PHX);Pittsburgh, PA, United States - (PIT);Port-Au-Prince, Haiti - (PAP);Portland, OR, United States - (PDX);Queretaro, MX, Mexico - (QRO);Richmond, Virginia - (RIC);Sacramento, CA, United States - (SMF);Salt Lake City, UT, United States - (SLC);San Diego, CA, United States - (SAN);San Jose, CA, United States - (SJC);Saskatoon, SK, Canada - (YXE);Seattle, WA, United States - (SEA);St. Louis, MO, United States - (STL);Tampa, FL, United States - (TPA);Toronto, ON, Canada - (YYZ);Vancouver, BC, Canada - (YVR);Tallahassee, FL, United States - (TLH);Winnipeg, MB, Canada - (YWG);Adelaide, SA, Australia - (ADL);Auckland, New Zealand - (AKL);Brisbane, QLD, Australia - (BNE);Melbourne, VIC, Australia - (MEL);Noumea, New caledonia - (NOU);Perth, WA, Australia - (PER);Sydney, NSW, Australia - (SYD)".split(
";"
);
const area = data.split("colo=")[1].split("\n")[0];
for (let i = 0; i < areas.length; i++) {
if (areas[i].indexOf(area) !== -1) {
const cdnElement = document.getElementById("cdn");
const ipElement = document.getElementById("ip");
const locElement = document.getElementById("loc");

if (cdnElement) {
cdnElement.innerHTML = areas[i];
}
if (ipElement) {
ipElement.innerHTML = data.split("ip=")[1].split("\n")[0];
}
if (locElement) {
locElement.innerHTML = data.split("loc=")[1].split("\n")[0];
}
break;
}
}
})
.catch((error) => {
console.error("Error fetching CDN info:", error);
});
};

document.addEventListener("DOMContentLoaded", () => {
getCDNinfo();
});
})();

STEP 9. 友链页面

以前友链页面是自动生成的,友链数据也直接放在主题配置中,现在也被分离了。

先在 Hexo 目录下的source文件夹内创建一个friends文件夹,和一个_data文件夹。
然后friends文件夹内创建一个 index.md:

index.md
1
2
3
4
5
---
title: 好伙伴们
---

{% linklist friendList random %}

_data文件夹内创建一个linklist.yml文件存放友链数据:

1
2
3
4
5
friendList:
- title: "null"
description: "喵~"
image: "/images/user.svg"
link: "https://example.com"

这样就行了,看看效果吧

STEP 10. 其他小修小改

common.js内补一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const replace = () => {
const meta = document.querySelector(".kratos-page-meta.text-center");
if (!meta) return;
const modifiedTime = document
.querySelector("footer.kratos-entry-footer .pull-date time")
.getAttribute("datetime");
meta.children.item(1).remove();
meta.firstElementChild.insertAdjacentHTML(
"afterend",
`<li><time datetime="${modifiedTime}" itemprop="dateModified"><i class="fa fa-clock-o" aria-hidden="true"></i> ${
modifiedTime.split("T")[0]
}</time></li>`
);
};
replace();
window.addEventListener("pjax:complete", () => {
replace();
});

把文章页修改日期搬到标题下面的元信息内,同时去除了我不需要的 author 信息。

animate.js 还有一点问题,由于是用 js 添加的,所以会先看到加载完的页面,再看到动画,所以要把动画加载前的隐藏一下,先添加一个 hide-all.css:

1
2
3
4
.sticky-area .widget,
article {
visibility: hidden;
}

引入配置时添加一个 id="hide-all-css":

1
2
3
additional_injections:
head: |
<link rel="stylesheet" id="hide-all-css" href="/custom/css/hide-all.css"/>

再修改之前的 animated.js:

animated.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(() => {
const hideAllCss = document.getElementById("hide-all-css");
hideAllCss.remove(); // 添加动画时,把 hide-all.css 移除掉
const sideWidget = document.querySelectorAll(".sticky-area .widget");
sideWidget.forEach((element) => {
element.classList.add("animate__animated", "animate__fadeIn");
});
const animateArticle = () => {
const articleCard = document.querySelectorAll("article");
articleCard.forEach((element) => {
if (element.classList.contains("kratos-hentry"))
element.classList.add("animate__animated", "animate__zoomIn");
else element.classList.add("animate__animated", "animate__fadeIn");
});
};
window.addEventListener("pjax:complete", animateArticle);
animateArticle();
})();
分享这一刻
让朋友们也来瞅瞅!