文章摘要
豆包AI
拼命加载中...

前言

呜嗷呜嗷(?)
其实早在很久很久以前,我就想要自己搭建一个今日热门侧边栏,我看Heo的今日热门侧边栏就很好看,访客可以直接找到博客今天最火的几篇文章。
然后我找到了这篇教程:

但是我看不懂…然后之后我懂了一点基础知识,回来再看这篇文章,发现这篇文章里的图片全部崩掉惹(悲)。
因此我就自己捣鼓,经过了曲折的道路,我终于完成了(不愧是我hh)!

效果

效果图里的文章因站点而异。

效果图
侧边栏卡片展示当日访问量最高的文章,带排名和悬停高亮,数据来源于自建Umami统计。

前置条件

  1. 已部署Umami;
  2. 有自己的Cloudflare账号;
  3. 博客基于Solitude主题。

值得一提的是,如果你的博客不是Solitude主题,也可以根据这个教程来试试。只是在前端配置时,需要注意你的主题与Solitude主题的异同。

教程

1.部署 Cloudflare Worker(数据代理)

进入Cloudflare。

本教程使用简体中文。可以通过点击右上角的用户头像,展开“Language”,点击“简体中文”来切换为简体中文。如果你非常懂英文,那不调也没事。

Cloudflare
如上图,展开左栏“计算”,点击“Workers 和 Pages”。
点击右上角蓝色的“创建应用程序”,然后会弹出下面的界面:
创建Worker
选择“从Hello world!开始”。
从Hello world!开始
Worker name随便,你喜欢就好。
然后点击“部署”即可。
点击右上角编辑代码

点击右上角“编辑代码”,将worker.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
export default {
async fetch(request, env) {
const url = new URL(request.url);
const days = parseInt(url.searchParams.get("days") || "1", 10);
const limit = parseInt(url.searchParams.get("limit") || "8", 10);
const blog = (env.BLOG_URL || "https://你的博客地址").replace(/\/$/, "");

const endAt = Date.now();
const startAt = endAt - days * 24 * 60 * 60 * 1000;

const base = env.UMAMI_URL.replace(/\/$/, "");
const apiBase = base + "/api/websites/" + env.WEBSITE_ID;

let token = env.UMAMI_TOKEN;
if (!token && env.UMAMI_USER && env.UMAMI_PASSWORD) {
const login = await fetch(base + "/api/auth/login", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ username: env.UMAMI_USER, password: env.UMAMI_PASSWORD }),
});
if (!login.ok) {
return new Response(JSON.stringify({ error: "Umami 登录失败: HTTP " + login.status }), {
status: 502,
headers: { "content-type": "application/json" },
});
}
const ld = await login.json();
token = ld.token;
}
if (!token) {
return new Response(JSON.stringify({ error: "缺少凭证" }), {
status: 500,
headers: { "content-type": "application/json" },
});
}
const headers = { Authorization: "Bearer " + token };

// 站点累计 PV/UV(侧边栏总访问量用,可选)
if (url.pathname === "/stats" || url.pathname.endsWith("/stats")) {
const startAt = endAt - 50 * 365 * 24 * 60 * 60 * 1000;
const q = `startAt=${startAt}&endAt=${endAt}&unit=year`;
const sr = await fetch(`${apiBase}/stats?${q}`, { headers });
const stats = await sr.json();
return new Response(JSON.stringify({ pv: stats.pageviews || 0, uv: stats.visitors || 0 }), {
headers: { "content-type": "application/json; charset=utf-8", "access-control-allow-origin": "*", "cache-control": "public, max-age=300" },
});
}

// path维度拿url和访问量
const q = `startAt=${startAt}&endAt=${endAt}&unit=day`;
const pr = await fetch(`${apiBase}/metrics?${q}&type=path&limit=100`, { headers });
const pathData = await pr.json();

const posts = (pathData || [])
.filter((p) => /^\/posts\/[^\/]+\.html$/.test(p.x))
.sort((a, b) => b.y - a.y)
.slice(0, limit);

// 解析title拿真实标题
const data = await Promise.all(
posts.map(async (p) => {
let title = p.x;
try {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 3000);
const r = await fetch(blog + p.x, { signal: ctrl.signal });
clearTimeout(timer);
const html = await r.text();
const m = html.match(/<title>([\s\S]*?)<\/title>/i);
if (m) title = m[1].split("|")[0].trim() || p.x;
} catch (e) {}
return { url: p.x, title, views: p.y };
})
);

return new Response(JSON.stringify(data), {
headers: { "content-type": "application/json; charset=utf-8", "access-control-allow-origin": "*", "cache-control": "public, max-age=300" },
});
},
};

别忘了把第六行改成你的博客地址。

然后点击右上角“部署”即可。
接着要设置环境变量
在Cloudflare>Worker页面点击设置
设置
然后点击“变量和密钥”右侧的添加,变量类型全部选文本,依次添加下面的环境变量。

变量 说明
UMAMI_URL 你的Umami地址,如 https://umami.example.com
WEBSITE_ID Umami后台网站设置里的ID
BLOG_URL 博客地址,如 https://quicer.js.cool(用于抓标题)
UMAMI_TOKEN 较新 Umami 的只读 token(优先);没有就填下面两项
UMAMI_USER Umami的登录账号(填了UMAMI_TOKEN就不填)
UMAMI_PASSWORD Umami的登录密码(填了UMAMI_TOKEN就不填)

然后,.workers.dev域名一般不稳定,这里需要自己弄个域名。在Cloudflare>Worker页面点击“域”,自己添加自己的域名。

值得一提的是,Cloudflare里Worker的域名必须要是绑定在Cloudflare,请确保你的域名已经绑定到Cloudflare。

2.前端js

新建source/hot/文件夹,在里面新建hot.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
document.addEventListener("DOMContentLoaded", initializeHot);
document.addEventListener("pjax:complete", initializeHot);

// 把 WORKER_URL 改成你部署的Worker地址。
const WORKER_URL = "https://你的worker地址";
const RANK_DAYS = 1; // 统计范围:1=今日热门,7=近7天,30=近30天
const TOP_N = 5; // 最多展示几条

function initializeHot() {
const container = document.getElementById("card-hotpost");
if (!container) return;

fetch(`${WORKER_URL}?days=${RANK_DAYS}&limit=${TOP_N}`)
.then((r) => {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
})
.then((data) => renderHot(container, data))
.catch((err) => {
// Worker获取失败时,回退到本地静态列表
console.warn("[今日热门] 代理获取失败,回退到本地 data.json:", err);
fetch("/hot/data.json")
.then((r) => r.json())
.then((d) => renderHot(container, d))
.catch(() => {
container.innerHTML = '<div class="hot-post-empty">暂无热门数据</div>';
});
});
}

function renderHot(container, data) {
container.innerHTML = "";
if (!Array.isArray(data) || data.length === 0) {
container.innerHTML = '<div class="hot-post-empty">暂无热门数据</div>';
return;
}
data.slice(0, TOP_N).forEach((item, i) => {
const rank = i + 1;
const a = document.createElement("a");
a.className = "hot-post-link";
a.href = item.url;
a.innerHTML = `<span class="post-rank rank-${rank === 1 ? "1" : "2"}">${rank}</span><div class="post-title-container"><span class="post-title">${item.title}</span></div>`;
container.appendChild(a);
});
}

3.前端css

新建source/hot/hot.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
#card-hotpost {
display: flex;
flex-direction: column;
gap: 4px;
}
#card-hotpost .post-rank {
background: var(--efu-secondbg);
border: var(--style-border-always);
color: var(--efu-fontcolor);
border-radius: 8px;
margin-right: 4px;
width: 25px;
height: 25px;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
margin-top: 2px;
flex-shrink: 0;
}
#card-hotpost .post-rank.rank-1 {
background: var(--efu-lighttext);
color: var(--efu-card-bg);
border-color: var(--efu-lighttext);
}
.hot-post-link {
display: flex;
font-size: 15px;
overflow: hidden;
padding: 0.3rem;
gap: 4px;
border-radius: 12px;
text-decoration: none;
color: var(--efu-fontcolor);
transition: background 0.2s ease, color 0.2s ease;
}
.hot-post-link .post-title-container {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
}
.hot-post-link .post-title {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-height: 1.38;
flex: 1;
overflow: hidden;
padding-top: 2px;
}
.hot-post-link:hover {
background: var(--efu-lighttext);
border-radius: 8px;
color: var(--efu-card-bg);
}
#card-hotpost .hot-post-link:hover .post-rank {
background: var(--efu-card-bg);
color: var(--efu-fontcolor);
border-color: var(--efu-card-bg);
}
#card-hotpost .hot-post-link:hover .post-rank.rank-1 {
background: var(--efu-maskbg);
color: var(--efu-white);
border-color: var(--efu-maskbg);
}
.hot-post-empty {
padding: 0.5rem;
text-align: center;
color: var(--efu-fontcolor);
opacity: 0.7;
font-size: 14px;
}

4.前端本地回退

新建source/hot/data.json(Worker挂掉时的本地回退,随便填几条你的文章即可):

建议数量与js文件TOP_N一致。

1
2
3
4
5
[
{"url": "/posts/xxxxxx.html", "title": "示例文章一"},
{"url": "/posts/yyyyyy.html", "title": "示例文章二"},
{"url": "/posts/zzzzzz.html", "title": "示例文章三"}
]

5.主题配置

aside,yml添加hot组件

source/_data/aside.yml(没有就新建)添加hot组件:

1
2
3
4
5
6
- name: hot
title: 今日热门
class: card-hotpost
icon: fas fa-fire
content_id: card-hotpost
content_html: ''

注意字段结构:主题会自动用title/icon生成带火焰图标的卡片标题,content_html留空,由hot.js注入到content_id指定的#card-hotpost容器。

引入资源

主题配置文件_config.solitude.yml里引入资源:

1
2
3
4
5
extends:
head:
- <link rel="stylesheet" href="/hot/hot.css">
body:
- <script src="/hot/hot.js?v=2"></script>

其中?v=2是缓存炸弹,以后改hot.js是把版本号升到?v=3强制刷新。

让卡片出现在侧边栏

aside各页面的noSticky列表里加入hot。配置如下:

仅实例,根据自己的喜好来填。

1
2
3
4
5
6
7
aside:
home: # 首页
noSticky: "about,hot"
post: # 文章页
noSticky: "about"
page: # 独立页面
noSticky: "about"

6.hexo三连击

1
2
3
hexo clean
hexo generate
hexo server

常见问题

  • 侧边栏空白或一直「暂无热门数据」:打开浏览器控制台看请求你的Worker地址?days=1&limit=5是否 200;检查Worker环境变量(WEBSITE_IDUMAMI_URL 是否漏了 https://)。
  • 标题显示成URL而不是中文:Worker抓<title>超时(已设3s)或被拦截,会用URL兜底,不影响榜单;可加 User-Agent 头或调大超时。
  • 改了 hot.js 不生效:因为引入时带 ?v=2 缓存炸弹,改完代码记得把版本号改成 ?v=3(或修改文件后强刷)。
  • 数据不更新:Worker缓存300s+Umami本身有几分钟延迟,属正常。

最后

我觉得我的办法还是太麻烦了。以后尝试找一些更加简单的方法。
有任何问题可以找ai询问,如果ai询问无果,可以在评论区里询问(有时间我就看。重点是看心情)。