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、外层容器 1 hexo.extend.tag.register(name, callback, options)
具体参数
name:标签名
callback:function(args, content),返回 HTML 字符串
options:{ends: true} 表示这是配对标签,需要 {% endname %} 闭合
1 2 3 4 5 6 7 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 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 : '废弃' }; hexo.extend .tag .register ('chlogchange' , function (args, content ) { var type = (args[0 ] || 'improved' ).trim (); if (!labels[type]) type = 'improved' ; var label = labels[type]; 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: #3 dd9b6 [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(#54 B5A0, 0.3 ) background: #54 B5A0 // 时间线圆点 .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: #54 B5A0 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 : #16 a34a border-radius: 999px font-weight: 500 [color-scheme='dark'] & background: rgba(22 , 163 , 74 , 0.2 ) color : #4 ade80 // 卡片标题 .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
条评论