个性化 Hugo 主题「MemE」
2024-11-21 09:25:53 # Blog # Hugo

个性化原则

由于博客主题是通过 Git 子模块的方式引入的,为了避免后面主题的更新不会影响到个性化内容,所以不建议改动「themes」下的内容

我们在博客的根目录下,通过相同文件路径以及文件名的方式覆盖主题文件即可生效

自定义字体

文章主体,使用 「霞鹜文楷」

修改「config.toml」

1
2
3
4
5
6
# 文章标题、文章副标题、列表标题、列表的年份和月份标题、相关文章标题、文章上下篇标题、表格的表头、定义列表中的术语
fontFamilyTitle = "'LXGW WenKai Screen', sans-serif"
# 分节标题、目录标题
fontFamilyHeadings = "'LXGW WenKai Screen', sans-serif"
# 主体
fontFamilyBody = "'LXGW WenKai Screen', sans-serif"

字体链接使用 @一蓑烟雨 分享的 staticfile CDN

1
2
# 网络字体链接
fontsLink = "https://cdn.staticfile.org/lxgw-wenkai-screen-webfont/1.6.0/lxgwwenkaiscreen.css"

文章代码部分,使用 JetBrains Mono

代码的字体直接下载放到项目「static/fonts」下

然后项目下新建「assets/scss/custom」文件夹

新增文件「_custom.scc」

1
2
// 字体设定
@import "font";

新增文件「_font.scss」

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
@font-face {
font-family: 'JetBrains Mono';
font-display: swap;
src: url('#{$baseRelURL}/fonts/JetBrainsMono-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'JetBrains Mono';
font-display: swap;
src: url('#{$baseRelURL}/fonts/JetBrainsMono-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'JetBrains Mono';
font-display: swap;
src: url('#{$baseRelURL}/fonts/JetBrainsMono-Italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
}

:root {
--text-wdth: 90;
--text-opsz: 40;
--text-YTLC: 460;
}

body {
font-variation-settings:
'wdth' var(--text-wdth),
'opsz' var(--text-opsz),
'YTLC' var(--text-YTLC);
}

.post-title {
font-variation-settings:
'wght' 550,
'opsz' 60,
'YOPQ' 90;
}

.list-item-time {
font-feature-settings: 'tnum';
}

//自适应屏幕宽度

@media (max-width: 500px) {
body {
font-size: $fontSize * 0.875;
}
.drop-cap {
font-size: $fontSize * 0.875 * 3;
margin-right: $lineHeight * $fontSize * 0.875 - $fontSize * 0.875;
margin-top: $fontSize * 0.875 / $lineHeight;
line-height: $lineHeight * $fontSize * 0.875;
}
}

修改配置文件「config.toml」

1
2
# 代码、上标、文章元信息、文章更新徽章、文章的 Git 版本信息、极简页脚、不蒜子站点浏览计数
fontFamilyCode = "'JetBrains Mono', 'LXGW WenKai Screen', monospace"

引入 Twikoo 评论系统

Twikoo 一个简洁、安全、免费的静态网站评论系统

通过 Zeabur 部署 Twikoo

使用 GitHub 账号登录 Zeabur[^1]

创建一个新的 Project,名字随意

点击「Add new service」,然后选择「Deploy Other Services」

image.png

部署一个 Mongo 服务

到 GitHub 中将 Twikoo-zeabur fork 到自己的账号下

然后再到 Zeabur 中点击「Add new service」,然后选择「Deploy Your Source Code」将「Twikoo-zeabur」部署到 Zeabur

部署完成后,绑定域名

直接访问绑定的域名

这样就说明部署成功了

MemE 中添加 Twikoo

到博客根目录下

新建「layouts/partials/components/comments.html」文件

在原地址文件的基础上增加

1
2
3
{{ if .Site.Params.enableTwikoo }}
<div id="tcomment"></div>
{{ end }}

完整如下

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
{{ if and (.Params.comments | default .Site.Params.enableComments) (eq hugo.Environment "production") }}
{{ if or (in .Site.Params.mainSections .Section) .Params.comments }}

{{ if not .Site.Params.autoLoadComments }}
<div class="load-comments">
<div id="load-comments">{{ i18n "loadComments" }}</div>
</div>
{{ end }}

{{ if .Site.Params.enableTwikoo }}
<div id="tcomment"></div>
{{ end }}

{{ if .Site.Params.enableDisqus }}
<div id="disqus_thread"></div>
{{ end }}

{{ if .Site.Params.enableValine }}
<div id="vcomments"></div>
{{ end }}

{{ if .Site.Params.enableUtterances }}
<div id="utterances"></div>
{{ end }}

{{ if .Site.Params.enableGitalk }}
<div id="gitalk-container"></div>
{{ end }}
{{ end }}
{{ end }}

新建「layouts/partials/pages/third-party/script.html」文件

在原地址文件的基础上增加

1
2
3
{{ if .Site.Params.enableTwikoo }}
{{ partial "third-party/twikoo.html" . }}
{{ end }}

完整如下

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
{{ if .Params.katex | default .Site.Params.enableKaTeX }}
{{ partial "third-party/katex.html" . }}
{{ end }}

{{ if .Params.mathjax | default .Site.Params.enableMathJax }}
{{ partial "third-party/mathjax.html" . }}
{{ end }}

{{ if .Params.mermaid | default .Site.Params.enableMermaid }}
{{ partial "third-party/mermaid.html" . }}
{{ end }}

{{ if and (.Params.comments | default .Site.Params.enableComments) (eq hugo.Environment "production") }}
{{ if or (in .Site.Params.mainSections .Section) .Params.comments }}

{{ if .Site.Params.enableTwikoo }}
{{ partial "third-party/twikoo.html" . }}
{{ end }}

{{ if .Site.Params.enableDisqus }}
{{ partial "third-party/disqus.html" . }}
{{ end }}

{{ if .Site.Params.enableValine }}
{{ partial "third-party/valine.html" . }}
{{ end }}

{{ if .Site.Params.enableUtterances }}
{{ partial "third-party/utterances.html" . }}
{{ end }}

{{ if .Site.Params.enableGitalk }}
{{ partial "third-party/gitalk.html" . }}
{{ end }}

{{ end }}
{{ end }}

{{ if .Site.Params.enableMediumZoom }}
{{ partial "third-party/medium-zoom.html" . }}
{{ end }}

{{ if .Site.Params.enableInstantPage }}
{{ partial "third-party/instant-page.html" . }}
{{ end }}

{{ partial "third-party/busuanzi.html" . }}

{{ partial "custom/script.html" . }}

新建「layouts/partials/pages/third-party/twikoo.html」文件

这是个新增文件,引入 Twikoo

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
<script>
function loadComments() {
if (typeof Twikoo === 'undefined') {
const getScript = (options) => {
const script = document.createElement('script');
script.defer = true;
script.crossOrigin = 'anonymous';
Object.keys(options).forEach((key) => {
script[key] = options[key];
});
document.body.appendChild(script);
};
getScript({
src: 'https://cdn.jsdelivr.net/npm/[email protected]/dist/twikoo.all.min.js',
onload: () => {
newTwikoo();
}
});
} else {
newTwikoo();
}
}
function newTwikoo() {
twikoo.init({
el: '#tcomment',
envId: '{{ .Site.Params.twikooEnvId }}'
});
}
</script>

最后,到「comfig.toml」中添加对应配置

1
2
3
## Twikoo
enableTwikoo = true
twikooEnvId = ""

注意:开发环境下启动 Hugo 时,不会展示评论,需要在生产环境下启动:hugo --environment production server

引入 Memos

同样的,通过 Zeabur 部署 Memos[^2]

在 Zeabur 中绑定完域名后,第一次进入需要注册一个账号,这个账号便是管理员

主页滚动近期 Memos

MemE 的主页有「文章摘要」和「诗意人生」两种风格,这两种分别对应「layouts/partials/pages/home-posts.html」和「layouts/partials/pages/home-poetry.html」两个文件

我这里使用的是「文章摘要」的风格

复制「layouts/partials/pages/home-posts.html」到博客根目录

在「main」标签下的第一行增加

1
2
3
<div id="memos" class="main-inner">
{{ partial "pages/memos.html" . }}
</div>

同级目录下,新建文件「memos.html」

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
<!--引入相对时间 Lately 插件-->
<!-- <script src="https://tokinx.github.io/lately/lately.min.js"></script> -->

<!--JS 处理 Memos API-->
<script>
(() => {
window.Lately = new function () {
this.lang = {
second: "秒",
minute: "分钟",
hour: "小时",
day: "天",
month: "个月",
year: "年",
ago: "前",
error: "NaN"
};
const format = (date) => {
date = new Date(date);
const floor = (num, _lang) => Math.floor(num) + _lang,
obj = new function () {
this.second = (Date.now() - date.getTime()) / 1000;
this.minute = this.second / 60;
this.hour = this.minute / 60;
this.day = this.hour / 24;
this.month = this.day / 30;
this.year = this.month / 12
},
key = Object.keys(obj).reverse().find(_ => obj[_] >= 1);
return (key ? floor(obj[key], this.lang[key]) : this.lang.error) + this.lang.ago;
},
_val = (date) => {
// date = new Date(date && (typeof date === 'number' ? date : date.replace(/-/g, '/').replace('T', ' ')));
// return isNaN(date.getTime()) ? false : date.getTime();
if (date == 'undefined') {
return false;
}
if ((date + '').indexOf('T') > 0) {
return new Date(date).getTime();
}
if ((date + '').length < 13) {
return false;
}
return date;
};
return {
init: ({ target = "time", lang } = {}) => {
if (lang) this.lang = lang;
for (let el of document.querySelectorAll(target)) {
const date = _val(el.dateTime) || _val(el.title) || _val(el.innerHTML) || 0;
if (!date) return;
el.title = new Date(+date).toLocaleString("zh-CN");
const i = el.innerHTML.indexOf(';');
if (i > 0) {
el.innerHTML = el.innerHTML.substring(0, i+1) + format(+date);
} else {
el.innerHTML = format(+date);
}
}
},
format
}
}
})();

let jsonUrl = "" + Date.parse(new Date());

fetch(jsonUrl).then((res) => res.json()).then((resdata) => {
var result = "",
resultAll = "",
data = resdata.data;
for (var i = 0; i < data.length; i++) {
var talkTime = data[i].createdTs * 1000;
// var talkTime = timeConver(data[i].createdTs);
var talkContent = data[i].content;
var newtalkContent = talkContent.replace(/```([\s\S]*?)```[\s]*/g, " <code>$1</code> ") //全局匹配代码块
.replace(/`([\s\S ]*?)`[\s]*/g, " <code>$1</code> ") //全局匹配内联代码块
.replace(/\!\[[\s\S]*?\]\([\s\S]*?\)/g, "🌅") //全局匹配图片
.replace(/\[[\s\S]*?\]\([\s\S]*?\)/g, "🔗") //全局匹配连接
.replace(/\bhttps?:\/\/(?!\S+(?:jpe?g|png|bmp|gif|webp|jfif|gif))\S+/g, "🔗"); //全局匹配纯文本连接
result += `<li class="item"><span class="memosTime" title=${talkTime}></span>: <a href="https://venom-memos.zeabur.app" target="_blank">${newtalkContent}</a></li>`;
}
var talkDom = document.querySelector("#memos");
var talkBefore = `<img title="Memos" style="width: 1.7em; float: left; position: relative; top: 1px;" src="icons/tg.gif"></img><div class="talk-wrap"><ul class="talk-list">`;
var talkAfter = `</ul></div>`;
resultAll = talkBefore + result + talkAfter;
talkDom.innerHTML = resultAll;

// 相对时间
window.Lately && Lately.init({ target: 'time, .memosTime' });
});

// 滚动效果
setInterval(function () {
var talkWrap = document.querySelector(".talk-list");
var talkItem = talkWrap.querySelectorAll(".item");
for (i = 0; i < talkItem.length; i++) {
setTimeout(function () {
talkWrap.appendChild(talkItem[0]);
}, 2000);
}
}, 2000);

// 时间戳转换
function timeConver(t) {
t = t * 1000;
//年-月-日-时-分-秒
var now = new Date(t);
var year = now.getFullYear();
var month = now.getMonth() + 1;
var date = now.getDate();
var hour = now.getHours();
var minute = now.getMinutes();
var second = now.getSeconds();
// 补零
if (month < 10) {
month = "0" + month;
}
if (date < 10) {
date = "0" + date;
}
if (hour < 10) {
hour = "0" + hour;
}
if (minute < 10) {
minute = "0" + minute;
}
if (second < 10) {
second = "0" + second;
}
return (
//按需求拼接
// year + "-" + month + "-" + date
// year + "-" + month + "-" + date + " " + hour + ":" + minute + ":" + second
year + "-" + month + "-" + date + " " + hour + ":" + minute
);
}
</script>

jsonUrl 替换为自己的 Memos API 地址

「_custom.scss」中增加

1
2
// Memos
@import 'memos';

同级目录下增加「_memos.scss」

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
#memos {
display: block;
margin: 0 auto;
width: 36em;
}

.index-talk {
display: flex;
flex: 1 auto;
width: 100%;
text-align: left;
position: relative;
}

.talk-list {
padding-left: 0px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}

.talk-list li {
list-style: none;
margin-bottom: 10px;
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
display: inline;
vertical-align: middle;
}

.talk-list li:not(:first-child) {
display: none !important;
}

.talk-list li a:hover {
text-decoration: none;
color: #1890ff;
}

Thanks

[^1]: Zeabur 属于大学生创业产品,团队成员主要来自浙江大学,目前仍处于推广阶段,服务免费使用,后续则将采用免费额度 + 按量计费的模式

[^2]: Memos 一个开源且免费的自托管知识库