抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Hexo 标签插件(Tag Plugin)让你可以在 Markdown 中以 {% tagname args %} 的语法插入动态 HTML,而不用在文章里直接写一堆 <div>。本文以本站更新日志页面使用为例,拆解一个完整的标签插件是怎么写出来的。

最终效果

在 Markdown 中写,最终渲染为带彩色标签的时间线卡片。

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
{% chlog %}

{% chlogitem 2026年6月20日 %}

{% chlogchange improved %}
优化导航栏二级菜单样式
{% endchlogchange %}

{% endchlogitem %}

{% endchlog %}

一、创建脚本文件

在Hexo项目根目录下新建 scripts/chlog.js,Hexo 启动时会自动加载 scripts/ 目录下的所有 JS 文件。

1
2
3
4
// scripts/chlog.js
'use strict';

// 三个标签依次注册

1、外层容器

1
hexo.extend.tag.register(name, callback, options)

具体参数

name:标签名

callbackfunction(args, content),返回 HTML 字符串

options{ends: true} 表示这是配对标签,需要 {% endname %} 闭合

1
2
3
4
5
6
7
// {% chlog %} ... {% endchlog %}
hexo.extend.tag.register('chlog', function(args, content) {
return '<div class="timeline" id="timeline">'
+ '<div class="timeline__line"></div>'
+ content
+ '</div>';
}, {ends: true});

标签本身不处理参数,只提供一个 HTML 骨架包裹子标签的内容。content 是标签体内部的所有内容(已由 Hexo 递归渲染过)。

2、时间节点

点击查看代码
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
// {% chlogitem date::title %} ... {% endchlogitem %}
hexo.extend.tag.register('chlogitem', function(args, content) {
// 支持 :: 或 , 作为分隔符
if (/::/.test(args)) {
args = args.join(' ').split('::');
} else {
args = args.join(' ').split(',');
}
var date = (args[0] || '').trim();
var title = (args[1] || '').trim();

var header = '<div class="timeline-card__header">';
if (date) header += '<span class="timeline-card__date">' + date + '</span>';
header += '</div>';

var titleHTML = title
? '<h3 class="timeline-card__title">' + title + '</h3>'
: '';

return '<div class="timeline-item">'
+ '<div class="timeline-item__dot"></div>'
+ '<div class="timeline-card">'
+ header
+ titleHTML
+ '<ul class="timeline-card__changes">' + content + '</ul>'
+ '</div></div>';
}, {ends: true});

args 是一个数组,用 join(' ') 合并后再按分隔符切分,兼容 {% chlogitem 2026年6月,标题 %}{% chlogitem 2026年6月::标题 %} 两种写法。返回的是纯 HTML 字符串拼接,包括圆点装饰、日期、标题、变更列表容器。

3、变更条目

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
// 类型映射表
var labels = {
added: '新增',
fixed: '修复',
improved: '优化',
removed: '移除',
deprecated: '废弃'
};

// {% chlogchange type %} ... {% endchlogchange %}
hexo.extend.tag.register('chlogchange', function(args, content) {
var type = (args[0] || 'improved').trim();
if (!labels[type]) type = 'improved';
var label = labels[type];

// 将内容渲染为 Markdown,去掉外层的 <p> 标签
var body = hexo.render.renderSync({text: content, engine: 'markdown'})
.split('\n').join('');
body = body.replace(/^<p>/, '').replace(/<\/p>\s*$/, '');

return '<li class="change-item" data-type="' + type + '">'
+ '<span class="change-tag change-tag--' + type + '">' + label + '</span>'
+ '<span>' + body + '</span>'
+ '</li>';
}, {ends: true});

标签内容仍然是 Markdown 文本,需要手动调用 Hexo 渲染器将其转为 HTML。回调函数拿到 content 是已经渲染过的子标签 HTML,但 chlogchange 没有子标签,内容就是原始文本。

采用正则 replace(/^<p>/, '').replace(/<\/p>\s*$/, '') 去掉 Markdown 渲染自动包裹的 <p> 标签,防止在 <li> 内产生多余的块级元素。

data-type 属性让 CSS 可以按类型分别着色。

二、编写具体样式

标签插件只负责生成 HTML 结构,样式由 Stylus 完成(source/_volantis/custom-style/timeline.styl

参考样式:

点击查看代码
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
// 颜色变量
$tl-added = #10b981
$tl-fixed = #ef4444
$tl-improved = #3b82f6
$tl-removed = #f59e0b
$tl-deprecated = #8b5cf6
$tl-primary = #6366f1

// 介绍区
.timeline-intro
text-align: center
margin-bottom: 1rem

.timeline-intro__title
font-size: 1.875rem
font-weight: 700
margin-bottom: 0.75rem
background: linear-gradient(to right, $tl-primary, #a855f7)
-webkit-background-clip: text
-webkit-text-fill-color: transparent
background-clip: text

.timeline-intro__desc
color: var(--color-meta)
max-width: 36rem
margin: 0 auto
font-size: 0.95rem

// 年份筛选
.timeline-filter
display: flex
flex-wrap: wrap
justify-content: center
gap: 0.5rem
margin-bottom: 2rem

.timeline-filter__btn
padding: 0.375rem 1rem
border: 1px solid var(--color-block)
border-radius: 999px
background: var(--color-card)
color: var(--color-meta)
font-size: 0.875rem
cursor: pointer
transition: all 0.2s ease

&:hover
border-color: #54B5A0
color: #54B5A0

&.is-active
background: #54B5A0
border-color: #54B5A0
color: #fff

// 时间线容器
.timeline
position: relative
padding-left: 2.5rem

@media screen and (min-width: 640px)
padding-left: 3rem

// 时间线竖线
.timeline__line
position: absolute
left: 1rem
top: 0
bottom: 0
width: 2px
background: #3dd9b6

[color-scheme='dark'] &
background: rgba(255, 255, 255, 0.15)

// 时间线条目
.timeline-item
position: relative
margin-bottom: 3rem

&.is-hidden
display: none

&:hover .timeline-item__dot
box-shadow: 0 0 0 4px rgba(#54B5A0, 0.3)
background: #54B5A0

// 时间线圆点
.timeline-item__dot
position: absolute
left: -2.5rem
top: 1.5rem
transform: translateX(-50%)
width: 14px
height: 14px
border-radius: 50%
border: 4px solid var(--color-site-bg)
background: #54B5A0
z-index: 10
transition: box-shadow 0.3s ease

@media screen and (min-width: 640px)
left: -3rem

// 版本卡片
.timeline-card
background: var(--color-card)
border-radius: 12px
border: 1px solid var(--color-block)
padding: 1.5rem
transition: box-shadow 0.3s ease, opacity 0.6s ease, transform 0.6s ease
opacity: 0
transform: translateY(30px)

&.is-visible
opacity: 1
transform: translateY(0)

&:hover
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08)

// 卡片头部:版本号 + 日期 + 徽章
.timeline-card__header
display: flex
flex-wrap: wrap
align-items: center
gap: 0.75rem
margin-bottom: 1rem

.timeline-card__version
padding: 0.25rem 0.75rem
background: var(--color-block)
font-weight: 600
border-radius: 6px
font-size: 0.875rem
color: var(--color-meta)

&.is-latest
background: rgba($tl-primary, 0.1)
color: $tl-primary

.timeline-card__date
font-size: 0.875rem
color: var(--color-meta)

.timeline-card__badge
margin-left: auto
font-size: 0.75rem
padding: 0.125rem 0.5rem
background: #dcfce7
color: #16a34a
border-radius: 999px
font-weight: 500

[color-scheme='dark'] &
background: rgba(22, 163, 74, 0.2)
color: #4ade80

// 卡片标题
.timeline-card__title
font-size: 1.125rem
font-weight: 600
margin-bottom: 1rem
color: var(--color-text)

// 变更列表
.timeline-card__changes
list-style: none
margin: 0
padding: 0

li + li
margin-top: 0.75rem

// 变更条目
.change-item
display: flex
gap: 0.75rem
font-size: 0.95rem
color: var(--color-meta)

// 变更标签
.change-tag
flex-shrink: 0
margin-top: 0.125rem
padding: 0.125rem 0.5rem
font-size: 0.75rem
font-weight: 500
border-radius: 4px

&--added
background: rgba($tl-added, 0.1)
color: $tl-added

[color-scheme='dark'] &
background: rgba($tl-added, 0.2)

&--fixed
background: rgba($tl-fixed, 0.1)
color: $tl-fixed

[color-scheme='dark'] &
background: rgba($tl-fixed, 0.2)

&--improved
background: rgba($tl-improved, 0.1)
color: $tl-improved

[color-scheme='dark'] &
background: rgba($tl-improved, 0.2)

&--removed
background: rgba($tl-removed, 0.1)
color: $tl-removed

[color-scheme='dark'] &
background: rgba($tl-removed, 0.2)

&--deprecated
background: rgba($tl-deprecated, 0.1)
color: $tl-deprecated

[color-scheme='dark'] &
background: rgba($tl-deprecated, 0.2)

// 加载更多按钮
.timeline-more
text-align: center
margin-top: 2rem

.timeline-more__btn
padding: 0.625rem 1.5rem
background: var(--color-card)
border: 1px solid var(--color-block)
border-radius: 8px
font-size: 0.875rem
font-weight: 500
color: var(--color-meta)
cursor: pointer
transition: background 0.2s ease

&:hover
background: var(--color-block)

.timeline-more.is-hidden
display: none

// 移动端适配
@media screen and (max-width: 768px)
.timeline-intro__title
font-size: 1.5rem

.timeline-card
padding: 1rem

.timeline-card__header
gap: 0.5rem

.timeline-card__badge
margin-left: 0

.timeline
padding-left: 2rem

.timeline-item__dot
left: -2rem

三、使用方式

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
{% chlog %}

{% chlogitem 日期::标题 %}

{% chlogchange added %}
新增的内容描述
{% endchlogchange %}

{% chlogchange fixed %}
修复的内容描述
{% endchlogchange %}

{% chlogchange improved %}
优化的内容描述
{% endchlogchange %}

{% chlogchange removed %}
移除的内容描述
{% endchlogchange %}

{% chlogchange deprecated %}
弃用的内容描述
{% endchlogchange %}

{% endchlogitem %}

{% endchlog %}
属性 可选值
标签 added/fixed/improved/removed/deprecated

灌水

灌水