此文介绍在Hexo的Stellar主题中添加侧边栏近期评论功能的方法。先部署Artalk评论系统,再在指定目录创建widgets.yml文件,添加含样式与脚本的近期评论组件,最后修改API地址、站点名称和评论数等变量,实现评论加载、展示及异常状态处理效果。

一、实体效果展示

  1. 评论加载中
  1. 暂无评论
  1. 评论加载失败
  1. 正常加载效果

一、部署Artalk评论系统

  • Artalk官方文档
    博主使用的是Docker-Compose文件部署,一键部署运行。

二、增加主题侧边栏组件

1. 创建文件widgets.yml

  • 在 /blog/source/_data/ 文件夹下,创建 widgets.yml 文件

2. 创建近期评论组件

  1. /blog/source/_data/widgets.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
recent_comments:
layout: markdown
title: 近期评论
content: |
<link rel="stylesheet" href="/path/to/your/recent-comments.css">

<div id="recent-comments" style="margin-top: 10px;">
<!-- 加载按钮容器 -->
<div class="linklist center" style="grid-template-columns: repeat(1, 1fr); margin-bottom: 10px;" id="loadButtonContainer">
<a class="link" title="加载评论" href="javascript:void(0);" id="loadingBtn">
<div class="flex">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<g fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5"></circle>
<path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M12 17v-6"></path>
<circle cx="1" cy="1" r="1" fill="currentColor" transform="matrix(1 0 0 -1 11 9)"></circle>
</g>
</svg>
<span>加载评论中...</span>
</div>
</a>
</div>
<div id="recent-comments-list"></div>
</div>
<script src="/path/to/your/recent-comments.js"></script>
  1. 对应的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
#recent-comments-list {
display: flex;
flex-direction: column;
gap: 12px;
}

#recent-comments-list .comment-card:first-child {
margin-top: 8px;
}

#recent-comments-list .comment-card:last-child {
margin-bottom: 8px;
}

.comment-card {
display: flex;
align-items: center;
padding: 8px 12px;
background: var(--block);
border-radius: 12px;
cursor: pointer;
user-select: none;
transition: background 0.3s;
}

.comment-card:hover {
background: var(--bg-a20);
}

.comment-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
}

.comment-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}

.comment-body {
flex: 1;
margin-left: 12px;
display: flex;
flex-direction: column;
justify-content: center;
min-width: 0;
}

.comment-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: var(--fsp);
color: var(--text-p1);
width: 100%;
min-width: 0;
}

.comment-nick {
font-weight: 600;
color: var(--text-p1);
white-space: nowrap;
flex-shrink: 0;
}

.comment-time {
font-size: calc(var(--fsp) * 0.85);
color: var(--text-p3);
white-space: nowrap;
flex-shrink: 0;
margin-left: 12px;
}

.comment-content {
font-size: var(--fsp);
color: var(--text-p2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 4px;
width: 100%;
min-width: 0;
}

.comment-content a {
color: inherit;
text-decoration: none;
}

.comment-content a:hover {
text-decoration: underline;
}
  1. 对应的JS代码
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
const API_BASE = 'artalk地址/api/v2';
const SITE_NAME = '网站名称';
const LIMIT = 8;

function timeAgo(dateString) {
const time = new Date(dateString.replace(' ', 'T'));
const now = new Date();
const seconds = Math.floor((now - time) / 1000);
const intervals = [
{ label: '年', seconds: 31536000 },
{ label: '个月', seconds: 2592000 },
{ label: '天', seconds: 86400 },
{ label: '小时', seconds: 3600 },
{ label: '分钟', seconds: 60 },
{ label: '秒', seconds: 1 }
];
for (const interval of intervals) {
const count = Math.floor(seconds / interval.seconds);
if (count > 0) return `${count}${interval.label}前`;
}
return '刚刚';
}

function getAvatarUrl(emailEncrypted) {
return `https://weavatar.com/avatar/${emailEncrypted}?s=80&d=identicon`;
}

async function loadRecentComments() {
const btn = document.getElementById('loadingBtn');
const btnContainer = document.getElementById('loadButtonContainer');
const list = document.getElementById('recent-comments-list');

const svgIcon = `
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<g fill="none">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5"></circle>
<path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M12 17v-6"></path>
<circle cx="1" cy="1" r="1" fill="currentColor" transform="matrix(1 0 0 -1 11 9)"></circle>
</g>
</svg>
`;

// 更新按钮为加载中状态
btn.innerHTML = `${svgIcon}<span>加载评论中...</span>`;
btn.removeAttribute('style'); // 移除按钮样式
btn.style.pointerEvents = 'none'; // 仅保留功能所需的样式

try {
const url = `${API_BASE}/stats/latest_comments?site_name=${SITE_NAME}&limit=${LIMIT}`;
const res = await fetch(url);
const json = await res.json();
const comments = json.data || [];

if (comments.length === 0) {
// 暂无评论状态
list.innerHTML = '';
btn.innerHTML = `${svgIcon}<span>暂无评论</span>`;
btn.removeAttribute('style'); // 移除按钮样式
btn.style.pointerEvents = 'none'; // 仅保留功能所需的样式
} else {
// 加载成功,渲染评论列表
list.innerHTML = comments.map(c => {
const timeText = timeAgo(c.date);
const avatarUrl = getAvatarUrl(c.email_encrypted);
const pageLink = `${c.page_url}#atk-comment-${c.id}`;
const contentPreview = c.content.replace(/<[^>]+>/g, '').slice(0, 60);

return `
<div class="comment-card" onclick="window.open('${pageLink}')">
<div class="comment-avatar">
<img src="${avatarUrl}" alt="头像" />
</div>
<div class="comment-body">
<div class="comment-header">
<span class="comment-nick">${c.nick || '匿名'}</span>
<span class="comment-time" title="${c.date}">${timeText}</span>
</div>
<div class="comment-content">
<a href="${pageLink}" rel="noopener">${contentPreview}</a>
</div>
</div>
</div>
`;
}).join('');
// 隐藏按钮容器
btnContainer.style.display = 'none';
}

} catch (err) {
console.error(err);
// 加载失败状态
btn.innerHTML = `${svgIcon}<span>加载失败,点击重试</span>`;
btn.removeAttribute('style'); // 移除按钮样式
btn.onclick = () => {
btn.onclick = null; // 防止重复点击
loadRecentComments();
};
list.innerHTML = '';
}
}
// 初始化加载评论
loadRecentComments();

3. 修改配置变量

  • 找到这三个变量名 API_BASE、SITE_NAME、LIMIT 分别是Artalk部署地址、网站名称、展示评论数。
  • 复制代码时注意代码缩进,不正确的缩进可能会使代码不生效。