本文不涉及对主题源代码的修改

在上一篇文章 站内文章自定义 Hexo 中的超链接样式 中介绍了 hexo-filter-custom-link 插件如何在:

  • 不修改主题源代码
  • 不使用 Hexo 标签语法

的条件下美化一般的超链接。为了提升可玩性, hexo-filter-custom-link 插件预留了更多的自定义模板,满足更丰富的需求,甚至可以替换一部分 Hexo 标签语法。

image.png

通过使用插件,我们只需要在 Markdown 中写下类似 [name](url "title") 形式的链接就可以实现:

  • 文章外链卡片

image.png

  • 作品评价卡片

image.png

  • Bilibili 视频卡片

image.png

插件安装与使用

安装与使用方式见项目 README 文档。

Readme Card

GitHub 项目地址:uuanqin/hexo-filter-custom-link: Customize the rendered HTML of Links. 自定义链接渲染后的 HTML

主要功能:

  • 自定义链接渲染后的样式,让形如 [text](url)[text](url "title") 的超链接渲染成你想要的模板。
  • 插件提供多种通配符方便设计更复杂的模板,通配符可以重复使用。
  • 在特殊情况下,可以为一些特殊链接指定不同的模板,让超链接的样式更加丰富。不管是通用模板还是自定义模板,它们都是可以共存的。
  • 对于复杂模板,支持模板间距以区分其他 Markdown 文本,避免渲染错误。

目前测试了该插件与以下「链接替换」插件并不冲突:

利用这个插件我们可以用特殊的符号 %% 去「标记」特殊的链接,让插件明白应该应用何种模板。 %% 标记后还可以传递模板参数,这样在设计模板时可以使用更多的自定义占位符。比如我们可以通过插件轻松实现博主们都爱使用的「文章外链卡片」,我们只需要手动指定链接的应用模板即可。

参数的形式就是仿照 URL 传参设计的,参数名可以自己自定义以方便模板的设计,和 Hexo 标签插件的传参相比能提升一定的可读性。

另外,像「卡片」这样的模板往往比较复杂,而且这种链接一般不会是 inline 的,所以可以使用插件的 spacing 选项为模板上下插入换行符,避免 Hexo 的渲染器渲染错误。

示例

这部分分享一些使用案例,可以加深对插件自定义模板的体会。案例几乎是在 文章 | Butterfly主题美化教程 上找的。熟练之后,你也可以试试用一个插件平替里面「标签外挂」相关的文章😉。

例 1:文章外链卡片

image.png

实例展示:

Markdown 写法:

1
2
[示例卡片](/p/8aa53d93/ "%%CARD%%")
[示例卡片](/p/8aa53d93/ "%%CARD%%TITLE=示例标题")

_config.yml 中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
custom_link:
enable: true # enable this plugin
general_template: ''
custom_templates:
- name: CARD
spacing: 1 # 表示模板前后插入一个换行符
template: |
<a class="tag-link" target="_blank" href="__URL__" title="__TITLE__">
<div class="tag-link-tips">引用站外地址</div>
<div class="tag-link-bottom">
<div class="tag-link-left"><i class="fa-solid fa-link"></i></div>
<div class="tag-link-right">
<div class="tag-link-title">__TEXT__</div>
<div class="tag-link-sitename">外部网站,请确认链接的正确性与安全性</div>
</div>
<i class="fa-solid fa-angle-right"></i>
</div>
</a>

引入以下 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
:root {
--tag-link-bg-color: #eeeeee;
--tag-link-text-color: #000;
--tag-link-border-color: #fff;
--tag-link-hover-bg-color: #d5f6ff;
--tag-link-hover-border-color: #ebebeb;
--tag-link-tips-border-color: #c2c2c2;
--tag-link-sitename-color: #909090;
--tag-link-hover-sitename-color: #000;
--tag-link-card-backgroud: #fff;
--tag-link-card-icon-color: #363636;
}
[data-theme=dark] {
--tag-link-bg-color: #282828;
--tag-link-text-color: #cccccc;
--tag-link-border-color: #000000;
--tag-link-hover-bg-color: #414141;
--tag-link-hover-border-color: #545454;
--tag-link-tips-border-color: #5d5d5d;
--tag-link-sitename-color: #909090;
--tag-link-hover-sitename-color: #fff;
--tag-link-card-backgroud: #727272;
--tag-link-card-icon-color: #e9e9e9;
}
#article-container .tag-link {
background: var(--tag-link-bg-color);
border-radius: 12px !important;
display: flex;
border: 1px solid var(--tag-link-border-color);
flex-direction: column;
padding: 0.5rem 1rem;
margin-top: 1rem;
text-decoration: none !important;
color: var(--tag-link-text-color);
margin-bottom: 10px;
transition: background-color 0.3s, border-color 0.3s, box-shadow 0.3s;
}
#article-container .tag-link:hover {
border-color: var(--tag-link-hover-border-color);
background-color: var(--tag-link-hover-bg-color);
box-shadow: 0 0 5px rgba(0,0,0,0.2);
}
#article-container .tag-link .tag-link-tips {
color: var(--tag-link-text-color);
border-bottom: 1px solid var(--tag-link-tips-border-color);
padding-bottom: 4px;
font-size: 0.6rem;
font-weight: normal;
}
#article-container .tag-link .tag-link-bottom {
display: flex;
margin-top: 0.5rem;
align-items: center;
justify-content: space-around;
}
#article-container .tag-link .tag-link-bottom .tag-link-left {
width: 50px;
min-width: 50px;
height: 50px;
background-size: cover !important;
border-radius: 8px;
background: var(--tag-link-card-backgroud);
pointer-events: none;
display: flex;
}

#article-container .tag-link .tag-link-bottom .tag-link-left i {
padding: 0;
margin: auto;
font-size: 24px;
color: var(--tag-link-card-icon-color);
}

#article-container .tag-link .tag-link-bottom .tag-link-right {
margin-left: 1rem;
}
#article-container .tag-link .tag-link-bottom .tag-link-right .tag-link-title {
font-size: 1rem;
line-height: 1.2;
}
#article-container .tag-link .tag-link-bottom .tag-link-right .tag-link-sitename {
font-size: 0.7rem;
color: var(--tag-link-sitename-color);
font-weight: normal;
margin-top: 4px;
transition: color 0.3s;
}
#article-container .tag-link .tag-link-bottom .tag-link-right:hover .tag-link-sitename {
color: var(--tag-link-hover-sitename-color);
}
#article-container .tag-link .tag-link-bottom i {
margin-left: auto;
}

此案例为比较简单的案例,只需要引入一个 CSS。

例 2:作品推荐卡片

本案例基于该项目进行修改:作品推荐卡片—标签外挂 | Leonus

图标使用了 font awesome。

image.png

实例展示:

Randal E. Bryant,1981年于麻省理工学院获得计算机博士学位,1984年至今一直任教于卡内基-梅隆大学。现任卡内基-梅隆大学计算机科学学院院长、教授,同时还受邀任教于电子和计算机工程系。 查看详情
教材
深入理解计算机系统

Markdown 写法:

1
[深入理解计算机系统](https://book.douban.com/subject/26912767/ "%%ART_CARD%%bg=https://cdn.gallery.uuanqin.top/img/202411280250546.webp&desr=Randal E. Bryant,1981年于麻省理工学院获得计算机博士学位,1984年至今一直任教于卡内基-梅隆大学。现任卡内基-梅隆大学计算机科学学院院长、教授,同时还受邀任教于电子和计算机工程系。”&icon=fa-solid fa-book-open&tag=教材&w=200px&h=275px&star=4.8")

别看上面的内容很多很杂,其实在普通的 Markdown 渲染器中展示的只是一个简单的链接而已。具体参数说明可以看原项目文章。

_config.yml 中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
custom_link:
enable: true # enable this plugin
custom_templates:
- name: ART_CARD
spacing: 1
template: |
<div title="__TEXT__" referrerPolicy="no-referrer" class="card_box" style="background-image: url(__bg__); width:__w__; height:__h__;">
<div class="card_mask">
<span>__desr__</span>
<a href="__URL__">查看详情</a>
</div>
<div class="card_top">
<i class="__icon__"></i>
<span>__tag__</span>
</div>
<div class="card_content">
<span>__TEXT__</span>
<div class="art-grade" data-value="__star__"></div>
</div>
</div>

引入以下 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
.card_box {
display: flex;
justify-content: space-between;
flex-direction: column;
background-position: center;
background-size: cover;
border-radius: 12px;
position: relative;
overflow: hidden;
padding: 10px;
color: #fff !important;
margin: 10px auto;
}
.card_box::after {
content: '';
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
background: rgba(0,0,0,0.1);
transition: 0.5s;
z-index: 0;
}
.card_box:hover .card_mask {
opacity: 1;
pointer-events: auto;
}
.card_box .card_top {
display: flex;
z-index: 1;
align-items: center;
justify-content: space-between;
}
.card_box .card_mask {
position: absolute;
pointer-events: none;
z-index: 2;
transition: 0.5s;
opacity: 0;
width: 100%;
height: 100%;
left: 0;
top: 0;
padding: 20px;
background: #333;
}
.card_box .card_mask span {
display: block;
height: calc(100% - 40px);
overflow: auto;
}
.card_box .card_mask a {
text-align: center;
background: #fff;
color: #333 !important;
border-radius: 5px;
position: absolute;
width: calc(100% - 40px);
bottom: 20px;
left: 20px;
}
.card_box .card_mask a:hover {
text-decoration: none !important;
color: #fff !important;
background: #49b1f5;
}
.card_box .card_content {
z-index: 1;
}
.card_box .card_content span {
font-size: 18px;
font-weight: bold;
}
[data-theme='dark'] .card_box {
color: #ddd !important;
}
[data-theme='dark'] .card_box::after {
background: rgba(0,0,0,0.4);
}

引入以下 js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 分数转成星星
function tostar(num) {
let tmp = ''
// console.log("星星",num)
for (let i = 0; i < Math.floor(num); i++) {
tmp += '<i class="fa-solid fa-star"></i>'
} // 整数部分加实心星星
if (num - Math.floor(num) !== 0) tmp += '<i class="fa-solid fa-star-half-alt"></i>' // 小数部分转成半星
for (let i = 0; i < 5 - Math.ceil(num); i++) {
tmp += '<i class="fa-regular fa-star"></i>'
} // 不够5个补空心星星
return tmp
}

document.querySelectorAll(".art-grade").forEach(block => {
const value = parseFloat(block.getAttribute("data-value"));
// console.log("星星",num)
block.innerHTML = tostar(value);
});


和第一个案例不一样的是,由于涉及星星函数的计算,我们把所有通用的逻辑提取出了 JavaScript。

例 3:Bilibili 卡片

本案例基于该项目进行修改:哔哩哔哩卡片标签外挂 | Leonus

image.png

实例展示:

Markdown 写法:

1
2
3
4
5
[地理中国](https://www.bilibili.com/video/BV1Vt411z7ty/ "%%BILIBILI_CARD%%time=29:59")

[偶像练习生](https://www.bilibili.com/video/BV1TE411h7vY/?p=2 "%%BILIBILI_CARD%%time=00:35")

[原神启动](https://www.bilibili.com/video/BV1bk4y1g73J/ "%%BILIBILI_CARD%%time=00:19&hide_desc=true")

具体参数详看原项目文章。

_config.yml 中配置:

1
2
3
4
5
6
7
8
9
10
11
12
custom_link:
enable: true # enable this plugin
general_template: ''
custom_templates:
- name: BILIBILI_CARD
spacing: 1
template: |
<div class="bilibili_card_res" title="__TEXT__">
<a href="__URL__" class="bilibili_box" data-url="__URL__"></a>
<script src="/js/example-2.js"></script>
<script>bilibili("__URL__","__time__","__hide_desc__")</script>
</div>

引入以下 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
123
124
125
126
127
128
129
130
131
132
.bilibili_box {
display: flex;
background: var(--card-bg);
border: 1px solid #e0e3ed;
border-radius: 10px;
overflow: hidden;
color: var(--font-color) !important;
text-decoration: none !important;
transition: .3s;
}
.bilibili_box:hover {
border-color: #4976f5;
}
@media (max-width: 768px) {
.bilibili_box {
flex-direction: column;
}
}
.bilibili_box .bilibili_cover {
width: 234px;
position: relative;
}
@media (max-width: 768px) {
.bilibili_box .bilibili_cover {
width: 100%;
}
}
.bilibili_box .bilibili_cover img {
width: 100%;
filter: none;
margin: 0 !important;
border-radius: 0 !important;
}
.bilibili_box .bilibili_cover .play_icon {
position: absolute;
width: 45px;
height: 45px;
opacity: .8;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.bilibili_box .bilibili_cover span {
position: absolute;
bottom: 0px;
right: 5px;
color: white;
text-shadow: 0 1px 3px #7a7a7a;
}
.bilibili_box .bilibili_info {
padding: 10px 10px 10px 18px;
line-height: 1;
width: calc(100% - 200px);
display: flex;
flex-direction: column;
justify-content: space-around;
}
@media (max-width: 768px) {
.bilibili_box .bilibili_info {
width: 100%;
padding-bottom: 25px;
line-height: 1.5;
}
}
.bilibili_box .bilibili_info .title {
font-size: 1.2rem;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.5;
}
.bilibili_box .bilibili_info .desc {
font-size: 15px;
margin: 2px 0 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 768px) {
.bilibili_box .bilibili_info .desc {
white-space: normal;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
}
.bilibili_box .bilibili_info .stat {
font-size: 15px;
}
.bilibili_box .bilibili_info .stat svg {
margin-right: 3px;
font-size: 18px;
width: 1em;
height: 1em;
}
.bilibili_box .bilibili_info .stat svg path {
fill: var(--font-color);
}
.bilibili_box .bilibili_info .stat span {
margin-right: 10px;
display: inline-flex;
align-items: center;
}
.bilibili_box .bilibili_info .owner {
display: flex;
align-items: center;
line-height: 1;
font-size: 15px;
}
.bilibili_box .bilibili_info .owner .tip {
color: #FF6699;
border: 1px solid;
padding: 3px 6px;
font-size: 12px;
border-radius: 5px;
margin-right: 10px;
}
.bilibili_box .bilibili_info .owner img {
width: 22px;
height: 22px;
border-radius: 50% !important;
object-fit: cover;
margin: 0 5px 0 0 !important;
}
[data-theme='light'] .bilibili_box .bilibili_info .stat svg {
opacity: .8;
}
[data-theme='dark'] .bilibili_cover {
opacity: .8;
}

引入以下 Javascript(不需要在主题配置文件 _config.butterfly.yml 中导入):

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
let playIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" class="icon"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.67735 4.2798C5.98983 4.1725 7.85812 4.0625 10 4.0625C12.1421 4.0625 14.0105 4.17252 15.323 4.27983C16.2216 4.3533 16.9184 5.04049 16.9989 5.9318C17.0962 7.00837 17.1875 8.43614 17.1875 10C17.1875 11.5639 17.0962 12.9916 16.9989 14.0682C16.9184 14.9595 16.2216 15.6467 15.323 15.7202C14.0105 15.8275 12.1421 15.9375 10 15.9375C7.85812 15.9375 5.98983 15.8275 4.67735 15.7202C3.77861 15.6467 3.08174 14.9593 3.00119 14.0678C2.90388 12.9908 2.8125 11.5627 2.8125 10C2.8125 8.43727 2.90388 7.00924 3.00119 5.93221C3.08174 5.04067 3.77861 4.35327 4.67735 4.2798ZM10 2.8125C7.81674 2.8125 5.9136 2.92456 4.5755 3.03395C3.07738 3.15643 1.8921 4.31616 1.75626 5.81973C1.65651 6.92379 1.5625 8.39058 1.5625 10C1.5625 11.6094 1.65651 13.0762 1.75626 14.1803C1.8921 15.6838 3.07738 16.8436 4.5755 16.966C5.9136 17.0754 7.81674 17.1875 10 17.1875C12.1835 17.1875 14.0868 17.0754 15.4249 16.966C16.9228 16.8436 18.108 15.6841 18.2438 14.1807C18.3435 13.077 18.4375 11.6105 18.4375 10C18.4375 8.38948 18.3435 6.92296 18.2438 5.81931C18.108 4.31588 16.9228 3.15645 15.4249 3.03398C14.0868 2.92458 12.1835 2.8125 10 2.8125ZM12.1876 10.722C12.7431 10.4013 12.7431 9.59941 12.1876 9.27866L9.06133 7.47373C8.50577 7.15298 7.81133 7.55392 7.81133 8.19542V11.8053C7.81133 12.4468 8.50577 12.8477 9.06133 12.527L12.1876 10.722Z" fill="#9499A0"/></svg>`
let likeIcon = `<svg width="36" height="36" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg" class="icon"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.77234 30.8573V11.7471H7.54573C5.50932 11.7471 3.85742 13.3931 3.85742 15.425V27.1794C3.85742 29.2112 5.50932 30.8573 7.54573 30.8573H9.77234ZM11.9902 30.8573V11.7054C14.9897 10.627 16.6942 7.8853 17.1055 3.33591C17.2666 1.55463 18.9633 0.814421 20.5803 1.59505C22.1847 2.36964 23.243 4.32583 23.243 6.93947C23.243 8.50265 23.0478 10.1054 22.6582 11.7471H29.7324C31.7739 11.7471 33.4289 13.402 33.4289 15.4435C33.4289 15.7416 33.3928 16.0386 33.3215 16.328L30.9883 25.7957C30.2558 28.7683 27.5894 30.8573 24.528 30.8573H11.9911H11.9902Z"></path></svg>`
let coinIcon = `<svg width="28" height="28" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg" class="icon" style="fill:;"><path fill-rule="evenodd" clip-rule="evenodd" d="M14.045 25.5454C7.69377 25.5454 2.54504 20.3967 2.54504 14.0454C2.54504 7.69413 7.69377 2.54541 14.045 2.54541C20.3963 2.54541 25.545 7.69413 25.545 14.0454C25.545 17.0954 24.3334 20.0205 22.1768 22.1771C20.0201 24.3338 17.095 25.5454 14.045 25.5454ZM9.66202 6.81624H18.2761C18.825 6.81624 19.27 7.22183 19.27 7.72216C19.27 8.22248 18.825 8.62807 18.2761 8.62807H14.95V10.2903C17.989 10.4444 20.3766 12.9487 20.3855 15.9916V17.1995C20.3854 17.6997 19.9799 18.1052 19.4796 18.1052C18.9793 18.1052 18.5738 17.6997 18.5737 17.1995V15.9916C18.5667 13.9478 16.9882 12.2535 14.95 12.1022V20.5574C14.95 21.0577 14.5444 21.4633 14.0441 21.4633C13.5437 21.4633 13.1382 21.0577 13.1382 20.5574V12.1022C11.1 12.2535 9.52148 13.9478 9.51448 15.9916V17.1995C9.5144 17.6997 9.10883 18.1052 8.60856 18.1052C8.1083 18.1052 7.70273 17.6997 7.70265 17.1995V15.9916C7.71158 12.9487 10.0992 10.4444 13.1382 10.2903V8.62807H9.66202C9.11309 8.62807 8.66809 8.22248 8.66809 7.72216C8.66809 7.22183 9.11309 6.81624 9.66202 6.81624Z"></path></svg>`

function bilibili(old_id, time, hidden_desc) {
const id = old_id.replace(/.*video\/([^\/]*)\/?.*/, '$1')
// 获取所有类名为 bilibili_card_res 的 div 元素
const bilibiliCards = document.querySelectorAll('.bilibili_card_res');
const targetUrl = old_id;

// 遍历每个 bilibili_card_res div 元素
bilibiliCards.forEach(card => {
// 在当前 div 元素下查找符合条件的 a 标签
const links = card.querySelectorAll(`a[data-url="${targetUrl}"]`);
links.forEach(link => {
fetch(`https://api.320.ink/api/b?id=${id}`).then(res => res.json()).then(data => {
link.innerHTML = `
<div class="bilibili_cover">
<img src="https://s1.hdslb.com/bfs/static/player/img/play.svg" class="play_icon no-lazyload">
<img src=${data.pic + '&h=300'}" class="no-lazyload">
${time ? `<span>${time}</span>` : ''}
</div>
<div class="bilibili_info">
<div class="title">${data.title}</div>
${hidden_desc === 'true' ? '' : `<div class="desc">${data.desc}</div>`}
<div class="stat">
<span>${playIcon}${data.view}</span>
<span>${likeIcon}${data.like}</span>
<span>${coinIcon}${data.coin}</span>
</div>
<div class="owner">
<span class="tip">视频</span>
<img src="${data.face + '&h=100'}" class="no-lazyload">
<span>${data.owner}</span>
</div>
</div>
`
})
});
});
}

本例涉及到了 API 的请求,修改项目时花了一定的时间调试。

本文参考