前言
呜嗷呜嗷(?)
其实早在很久很久以前,我就想要自己搭建一个今日热门侧边栏,我看Heo的今日热门侧边栏就很好看,访客可以直接找到博客今天最火的几篇文章。
然后我找到了这篇教程:
引用站外链接
Solitude
为你的网站增加一个漂亮的今日热门侧边栏,让访客一眼看到今日最热内容
但是我看不懂…然后之后我懂了一点基础知识,回来再看这篇文章,发现这篇文章里的图片全部崩掉惹(悲)。
因此我就自己捣鼓,经过了曲折的道路,我终于完成了(不愧是我hh)!
效果

侧边栏卡片展示当日访问量最高的文章,带排名和悬停高亮,数据来源于自建Umami统计。
前置条件
- 已部署Umami;
- 有自己的Cloudflare账号;
- 博客基于Solitude主题。
值得一提的是,如果你的博客不是Solitude主题,也可以根据这个教程来试试。只是在前端配置时,需要注意你的主题与Solitude主题的异同。
教程
1.部署 Cloudflare Worker(数据代理)
进入Cloudflare。
引用站外链接
本教程使用简体中文。可以通过点击右上角的用户头像,展开“Language”,点击“简体中文”来切换为简体中文。如果你非常懂英文,那不调也没事。

如上图,展开左栏“计算”,点击“Workers 和 Pages”。
点击右上角蓝色的“创建应用程序”,然后会弹出下面的界面:

选择“从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 };
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" }, }); }
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);
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);
const WORKER_URL = "https://你的worker地址"; const RANK_DAYS = 1; 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) => { 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挂掉时的本地回退,随便填几条你的文章即可):
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_ID、UMAMI_URL 是否漏了 https://)。
- 标题显示成URL而不是中文:Worker抓
<title>超时(已设3s)或被拦截,会用URL兜底,不影响榜单;可加 User-Agent 头或调大超时。
- 改了
hot.js 不生效:因为引入时带 ?v=2 缓存炸弹,改完代码记得把版本号改成 ?v=3(或修改文件后强刷)。
- 数据不更新:Worker缓存300s+Umami本身有几分钟延迟,属正常。
最后
我觉得我的办法还是太麻烦了。以后尝试找一些更加简单的方法。
有任何问题可以找ai询问,如果ai询问无果,可以在评论区里询问(有时间我就看。重点是看心情)。