📝 我的 Butterfly 博客折腾手记

本文记录了我基于 Hexo Butterfly 主题进行的一系列个性化美化过程,包括侧边栏博主信息卡片、分类页面交互、归档页面时间轴的设计与实现。所有方案均为个人折腾记录,若你尝试使用,请根据自身主题版本和结构调整,不保证完全兼容。


🧩 一、侧边栏博主信息卡片 · 从渐变到杂志封面

侧边栏的博主卡片是博客的“门面”,我希望它能既突出个人特色,又能与整个站点的“一图流”背景融合。我先后尝试了两套方案。

🎨 方案二:柔光渐变卡片(未采用)

这套方案的风格是 渐变背景 + 头像光晕 + 渐变文字,整体感觉温暖、现代。

设计要点:

  • 卡片背景为从 #f9f9f9#ffffff 的线性渐变
  • 头像带有渐变色边框和光晕阴影
  • 博主姓名和描述使用渐变文字(蓝粉色渐变)

遇到的问题:

  1. 头像边框无法显示:原代码使用 mask 属性实现渐变边框,但在我浏览器中未生效。经排查,是 -webkit-mask 兼容性问题,且我的头像容器类名是 .avatar-img 而非 .avatar

  2. 选择器不匹配:Butterfly 主题的侧边栏 HTML 结构在不同版本有差异,我实际结构为:

    1
    2
    3
    4
    5
    6
    <div class="card-widget card-info text-center">
    <div class="avatar-img"><img src="/upload/avatar.jpg"></div>
    <div class="author-info-name">Damon_Zhang</div>
    <div class="author-info-description">热爱技术,乐于分享 🌟</div>
    ...
    </div>

原 CSS 选择器无法命中,导致所有样式失效。

结果:经过多次调试,最终放弃方案二。

📖 方案三:杂志封面风格(最终采用)

这套方案用一张背景图衬底,所有文字白色悬浮,干净利落,与“一图流”背景天然契合。

设计要点:

  • 卡片本身透明,通过 ::before 设置背景图并调暗亮度
  • 文字纯白,带轻微阴影保证可读性
  • 头像带白色边框和阴影,悬浮感强

最终 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
/* ===== 侧边栏博主信息 - 杂志封面风格 ===== */
.card-widget.card-info.text-center {
background: transparent !important;
border: none !important;
padding: 0 !important;
position: relative !important;
border-radius: 24px !important;
overflow: hidden !important;
box-shadow: 0 20px 30px -10px rgba(0, 0, 0, 0.2) !important;
}

/* 背景图 */
.card-widget.card-info.text-center::before {
content: '' !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
background-image: url('/img/profile-bg.jpg') !important; /* 替换为你自己的背景图 */
background-size: cover !important;
background-position: center !important;
filter: brightness(0.7) !important;
z-index: 0 !important;
}

/* 内容层 */
.card-widget.card-info.text-center > * {
position: relative !important;
z-index: 1 !important;
}

/* 头像容器 */
.card-widget.card-info.text-center .avatar-img {
width: 100px !important;
height: 100px !important;
margin: 30px auto 15px !important;
border-radius: 50% !important;
overflow: hidden !important;
border: 4px solid rgba(255, 255, 255, 0.8) !important;
box-shadow: 0 8px 20px rgba(0,0,0,0.3) !important;
transition: transform 0.3s !important;
}
.card-widget.card-info.text-center .avatar-img:hover {
transform: scale(1.05) !important;
}

/* 头像图片 */
.card-widget.card-info.text-center .avatar-img img {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
display: block !important;
}

/* 博主名称 */
.card-widget.card-info.text-center .author-info-name {
font-size: 1.8rem !important;
font-weight: 700 !important;
color: #ffffff !important;
text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.5) !important;
text-align: center !important;
margin: 5px 0 !important;
}

/* 描述文字 */
.card-widget.card-info.text-center .author-info-description {
color: #ffffff !important;
font-size: 1rem !important;
padding: 0 15px !important;
text-align: center !important;
margin: 5px 0 15px !important;
text-shadow: 1px 1px 4px rgba(0, 0, 0, 0.5) !important;
}

/* 社交图标 */
.card-widget.card-info.text-center .card-info-social-icons {
text-align: center !important;
margin-bottom: 20px !important;
}
.card-widget.card-info.text-center .card-info-social-icons .social-icon {
display: inline-block !important;
margin: 0 10px !important;
color: #fff !important;
font-size: 1.4rem !important;
filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.5)) !important;
transition: transform 0.2s !important;
}
.card-widget.card-info.text-center .card-info-social-icons .social-icon:hover {
transform: scale(1.2) !important;
}

/* 按钮(如有) */
.card-widget.card-info.text-center .button a {
display: inline-block !important;
padding: 8px 30px !important;
background: rgba(255, 255, 255, 0.25) !important;
backdrop-filter: blur(8px) !important;
border: 1px solid rgba(255, 255, 255, 0.5) !important;
border-radius: 40px !important;
color: #fff !important;
font-weight: 600 !important;
transition: all 0.3s !important;
}
.card-widget.card-info.text-center .button a:hover {
background: rgba(255, 255, 255, 0.4) !important;
transform: scale(1.05) !important;
}

实现要点:

  • 背景图放在 source/img/profile-bg.jpg,通过 ::before 插入并调暗
  • 所有文字纯白,用 text-shadow 保证在亮背景上可读
  • 头像圆形白边,悬停放大,增加互动感
  • 深色模式无需额外适配(背景图已调暗,文字白色不受影响)

🗂️ 二、分类页面交互 · 点击展开文章列表

我希望在分类导航页面(/categories/)能直接看到每个分类下的文章标题,而不是跳转到分类归档页。效果:点击分类项右侧箭头(或卡片空白区域),下方展开该分类最近的文章(最多10篇),再点击收缩。

设计目标

  • 每个分类项右侧添加箭头 ,点击展开/收缩
  • 点击分类标题链接正常跳转到归档页
  • 点击卡片空白区域也能触发展开/收缩
  • 展开后显示文章标题列表,带日期
  • 页面通过 PJAX 切换后功能仍生效
  • 限制每分类最多显示10篇文章

实现步骤

1. 修改分类页面模板(categories.pug

Butterfly 的分类导航页面默认使用 != list_categories() 自动生成 HTML,无法直接插入文章列表。因此需要在模板中将 site.categories 数据以 JSON 形式嵌入页面。

最终 categories.pug 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.category-lists!= list_categories()

- var categoriesData = []
- var maxPostsPerCategory = 10
- var cats = site.categories.toArray()
- for (var i = 0; i < cats.length; i++)
- var cat = cats[i]
- var catData = { name: cat.name, path: cat.path, posts: [] }
- var posts = cat.posts.toArray().slice(0, maxPostsPerCategory)
- for (var j = 0; j < posts.length; j++)
- var post = posts[j]
- catData.posts.push({ title: post.title, path: post.path, date: post.date.format('YYYY-MM-DD') })
- // 如果有子分类,递归处理(略,可按需扩展)
- categoriesData.push(catData)

script.
window.categoryData = !{JSON.stringify(categoriesData)};

2. 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
/* ===== 分类页面交互样式 ===== */
.category-list-item {
position: relative !important;
padding-right: 40px !important;
transition: all 0.3s ease !important;
}

.expand-arrow {
position: absolute !important;
right: 15px !important;
top: 50% !important;
transform: translateY(-50%) !important;
width: 24px !important;
height: 24px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
color: #49b1f5 !important;
font-size: 1.2rem !important;
font-weight: bold !important;
cursor: pointer !important;
z-index: 10 !important;
transition: transform 0.3s ease !important;
}

.category-list-item.active .expand-arrow {
transform: translateY(-50%) rotate(90deg) !important;
}

.category-articles {
max-height: 0 !important;
overflow: hidden !important;
transition: max-height 0.3s ease !important;
margin-left: 20px !important;
padding-left: 15px !important;
border-left: 2px dashed rgba(73, 177, 245, 0.3) !important;
margin-top: 5px !important;
}

.category-list-item.active .category-articles {
max-height: 500px !important; /* 足够容纳10篇文章 */
margin-top: 10px !important;
}

.article-item {
padding: 8px 12px !important;
margin: 5px 0 !important;
background: rgba(255, 255, 255, 0.3) !important;
backdrop-filter: blur(4px) !important;
border-radius: 12px !important;
transition: all 0.2s !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
}
.article-item:hover {
background: rgba(73, 177, 245, 0.15) !important;
transform: translateX(5px) !important;
}
.article-item a {
color: #2c3e50 !important;
text-decoration: none !important;
display: block !important;
}
.article-item a:hover {
color: #49b1f5 !important;
}
.article-date {
font-family: monospace;
margin-right: 8px;
color: #666;
}

/* 深色模式 */
[data-theme="dark"] .expand-arrow {
color: #ff6b6b !important;
}
[data-theme="dark"] .category-articles {
border-left-color: rgba(255, 107, 107, 0.3) !important;
}
[data-theme="dark"] .article-item {
background: rgba(0, 0, 0, 0.3) !important;
}
[data-theme="dark"] .article-item a {
color: #eee !important;
}
[data-theme="dark"] .article-item a:hover {
color: #ff6b6b !important;
}
[data-theme="dark"] .article-date {
color: #aaa !important;
}

3. JavaScript 交互逻辑(category.js

主要处理:

  • 动态为每个分类项添加箭头和文章列表容器
  • window.categoryData 中读取对应文章并渲染
  • 绑定点击事件:箭头点击切换;卡片空白区域点击触发切换(排除箭头和链接)
  • PJAX 完成后重新初始化
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
// category.js
function initCategoryExpand() {
const categoryItems = document.querySelectorAll('.category-list-item');

categoryItems.forEach(item => {
// 防止重复添加
if (item.querySelector('.expand-arrow')) return;

const link = item.querySelector('.category-list-link');
const categoryName = link ? link.textContent.trim() : '';

// 添加箭头
const arrow = document.createElement('span');
arrow.className = 'expand-arrow';
arrow.innerHTML = '▶';
arrow.onclick = function(e) {
e.stopPropagation();
toggleCategory(this);
};
item.appendChild(arrow);

// 创建文章列表容器
const articlesDiv = document.createElement('div');
articlesDiv.className = 'category-articles';
item.appendChild(articlesDiv);

// 从 window.categoryData 中查找该分类的文章
if (window.categoryData && categoryName) {
const cat = findCategory(window.categoryData, categoryName);
if (cat && cat.posts && cat.posts.length) {
cat.posts.forEach(post => {
const articleItem = document.createElement('div');
articleItem.className = 'article-item';
const href = post.path.startsWith('/') ? post.path : '/' + post.path;
articleItem.innerHTML = `
<a href="${href}">
<span class="article-date">${post.date}</span>
<span class="article-title"> ${post.title}</span>
</a>
`;
articlesDiv.appendChild(articleItem);
});
}
}

// 卡片空白区域点击事件
item.addEventListener('click', function(e) {
// 如果点击的元素是箭头、链接或它们的后代,则不处理
if (e.target.closest('.expand-arrow') || e.target.closest('a')) {
return;
}
const arrow = this.querySelector('.expand-arrow');
if (arrow) toggleCategory(arrow);
});
});
}

// 递归查找分类(支持嵌套)
function findCategory(categories, name) {
for (let cat of categories) {
if (cat.name === name) return cat;
if (cat.children && cat.children.length) {
const found = findCategory(cat.children, name);
if (found) return found;
}
}
return null;
}

// 切换展开/折叠
function toggleCategory(arrow) {
if (!arrow) return;
const item = arrow.closest('.category-list-item');
if (item) {
item.classList.toggle('active');
arrow.innerHTML = item.classList.contains('active') ? '▼' : '▶';
}
}

// 初始化
document.addEventListener('DOMContentLoaded', initCategoryExpand);
document.addEventListener('pjax:complete', initCategoryExpand);

4. 引入 JS 文件

在主题配置文件 _config.butterfly.ymlinject 中添加:

1
2
3
inject:
bottom:
- <script src="/js/category.js"></script>

遇到的坑与解决

问题 原因 解决
JSON.stringify(site.categories) 报循环引用错误 site.categories 包含循环引用对象 在 Pug 中手动提取纯净数据(只保留 name, path, posts 必要字段)
箭头点击后也触发了卡片空白区域事件 事件冒泡 在箭头点击时 e.stopPropagation(),并在卡片监听中排除箭头元素
PJAX 跳转后箭头消失 初始化函数未在 PJAX 完成后执行 监听 pjax:complete 事件重新执行
子分类的文章列表不显示 数据构建时未处理子分类 递归构建子分类数据,并在 findCategory 中支持嵌套查找
文章路径错误(缺少 / post.path 可能不是绝对路径 在拼接 href 时检查并补全开头的 /

⏳ 三、归档页面时间轴 · 杂志封面式 + 移动端适配

归档页我希望做成时间轴样式,让文章按时间排列更有仪式感。我选择了方案一(杂志封面式时间轴),并增加了移动端适配。

设计要点

  • 每个归档项呈卡片状,左侧有渐变装饰条
  • 日期放大显示,与标题形成对比
  • 悬停时卡片轻微上浮并左移
  • 移动端改为纵向布局,装饰条变为顶部横线

最终 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
133
134
135
136
137
138
139
140
141
/* ===== 归档页 - 杂志封面式时间轴 ===== */
.article-sort {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}

.article-sort-item {
display: flex;
align-items: center;
margin-bottom: 25px;
padding: 20px 25px;
background: #ffffff;
border-radius: 20px;
box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.1);
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
border: 1px solid rgba(0, 0, 0, 0.02);
position: relative;
overflow: hidden;
}

.article-sort-item:hover {
transform: translateX(8px) scale(1.02);
box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.2);
border-color: #49b1f5;
}

.article-sort-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 6px;
background: linear-gradient(135deg, #49b1f5, #ff6b6b);
}

.article-sort-item-time {
font-size: 1.8rem;
font-weight: 700;
color: #49b1f5;
min-width: 100px;
text-align: center;
letter-spacing: 2px;
position: relative;
z-index: 1;
}

.article-sort-item-time::after {
content: '·';
font-size: 2rem;
color: #ff6b6b;
margin-left: 10px;
}

.article-sort-item-title {
font-size: 1.3rem;
color: #2c3e50;
text-decoration: none;
flex: 1;
padding-left: 20px;
border-left: 2px dashed rgba(73, 177, 245, 0.3);
transition: color 0.2s;
font-weight: 500;
}

.article-sort-item-title:hover {
color: #49b1f5;
}

/* 深色模式 */
[data-theme="dark"] .article-sort-item {
background: #1e1e1e;
border-color: #333;
}
[data-theme="dark"] .article-sort-item-title {
color: #eee;
border-left-color: #444;
}
[data-theme="dark"] .article-sort-item:hover {
border-color: #ff6b6b;
}

/* ===== 移动端适配 ===== */
@media screen and (max-width: 768px) {
.article-sort {
padding: 10px;
}

.article-sort-item {
flex-direction: column;
align-items: flex-start;
padding: 15px;
margin-bottom: 20px;
transform: none !important;
}

.article-sort-item:hover {
transform: none !important;
}

.article-sort-item::before {
width: 100%;
height: 4px;
background: linear-gradient(90deg, #49b1f5, #ff6b6b);
}

.article-sort-item-time {
font-size: 1.4rem;
min-width: auto;
width: 100%;
text-align: left;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: space-between;
}

.article-sort-item-time::after {
content: '·';
margin-left: auto;
margin-right: 10px;
}

.article-sort-item-title {
font-size: 1.1rem;
padding-left: 0;
padding-top: 8px;
border-left: none;
border-top: 2px dashed rgba(73, 177, 245, 0.3);
width: 100%;
}

.article-sort-item-title:hover {
transform: none;
}

[data-theme="dark"] .article-sort-item-title {
border-top-color: #444;
}
}

说明:

  • 桌面端:卡片横向布局,日期居左,标题居右,左侧渐变竖线装饰
  • 移动端:改为纵向,日期在上,标题在下,装饰条变为顶部横线
  • 所有数值可根据个人喜好调整

📌 结语

以上便是我在 Butterfly 主题上折腾的侧边栏、分类页和归档页的美化记录。每个方案都经历了“设想 - 尝试 - 踩坑 - 修正”的过程,最终效果符合预期。需要注意的是,这些代码基于我当前的主题版本和博客结构,如果你也想尝试,请务必根据实际 HTML 类名和主题版本进行适配,否则可能无法正常工作。

折腾无止境,博客的个性化本身就是一种乐趣。