rename content/post to content/posts

This commit is contained in:
Vaica committed 2026-06-02 15:37:49 +08:00
1 parent cda4dbb24b
commit bcd4bc9282
866 files changed
+43 -25

No files matched your search

@@ -0,0 +1,472 @@
---
pid: 138
title: 如何hugo静态实现友联朋友圈功能
date: 2026-01-21T06:30:00.000Z
slug: 20260121
draft: false
categories:
- 教程
- 折腾
tags:
- Hugo
- 朋友圈
- 静态博客
author: 小赵同学
layout: post
ai_comment: >-
这个技术方案巧妙地用静态生成+定时更新的方式模拟了动态功能,把动态数据问题转化为静态资源构建问题。Node.js脚本的异常处理和headers设置很老练,但可以考虑加入缓存机制避免重复抓取未更新的feed。前端部分用AJAX加载JSON确实简单直接,不过如果追求更极致的静态化,也可以考虑在Hugo构建时直接注入数据。
---
前段时间把博客从 Typecho 迁移到了 Hugo,整体感觉就是快,静态博客真的省心。但是,心里总觉得少点什么。没错,就是那个能看到朋友们最新动态的“朋友圈”功能。
在 Typecho 时代,有现成的插件可以用,转到 Hugo 后,发现很多现成的主题并没有集成这个功能,或者样式不是我喜欢的。最近逛博客,看到 [liushen.fun](https://blog.liushen.fun/fcircle/) 的友圈样式挺不错的,卡片风格,毛玻璃特效,甚是喜欢。于是心血来潮,想着能不能自己在 Hugo 上也整一个。
本来想着直接把仓库开源给大家参考,但因为仓库里有一些私密的配置和魔改的烂代码,实在不好意思(也不方便)公开。所以,既然大家想要,那我就把**完整的代码**和**实现步骤**都贴在这里,主打一个“喂饭级”教程,大家直接复制粘贴就能用!
## 实现思路
因为 Hugo 是静态博客,没有数据库,所以不能像 Typecho 那样实时读取数据库。我的思路是:
1. **后端抓取**:利用 GitHub Actions 定时运行一个 Node.js 脚本。
2. **数据处理**:脚本读取 `links.yaml`(或者你的友链 JSON),抓取每个朋友的 RSS Feed,提取文章标题、链接、时间、摘要和封面图。
3. **数据生成**:将处理好的数据保存为 `friend_circle_data.json` 文件。
4. **前端渲染**:在 Hugo 的页面模板中,通过 AJAX 请求这个 JSON 文件,动态渲染成卡片列表。
这样既保持了博客的静态特性,又实现了动态更新。
## 第一步:后端脚本 (Node.js)
首先,我们需要一个脚本来干脏活累活。我在项目根目录的 `scripts` 文件夹下新建了一个 `generate_circle_data.js`。
你需要先安装一个依赖:
```bash
npm install rss-parser
```
下面是完整的脚本代码,支持自动识别 RSS(如果友链里没写),支持提取文章里的第一张图作为封面,支持提取摘要:
```javascript
const fs = require('fs');
const path = require('path');
const Parser = require('rss-parser');
// 你的友链数据文件路径,根据实际情况修改
const LINK_LITE_PATH = path.join(__dirname, '../themes/Ying/static/json/link_lite.json');
// 输出文件路径
const OUTPUT_PATH = path.join(__dirname, '../themes/Ying/static/json/friend_circle_data.json');
const MAX_POSTS_PER_FRIEND = 5; // 每个朋友取最近5篇
const MAX_TOTAL_POSTS = 100; // 总共展示100篇
const parser = new Parser({
timeout: 10000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
});
async function fetchFeed(url) {
try {
const feed = await parser.parseURL(url);
return feed;
} catch (error) {
return null;
}
}
async function main() {
console.log('Starting Friend Circle generation...');
if (!fs.existsSync(LINK_LITE_PATH)) {
console.error('link_lite.json not found!');
process.exit(1);
}
const data = JSON.parse(fs.readFileSync(LINK_LITE_PATH, 'utf8'));
// 假设 data.friends 是一个数组: [name, url, avatar, rss?]
// 如果你的格式不一样,请自行调整下面的 map 逻辑
let allPosts = [];
const BATCH_SIZE = 5; // 并发控制
const friends = data.friends;
for (let i = 0; i < friends.length; i += BATCH_SIZE) {
const batch = friends.slice(i, i + BATCH_SIZE);
const promises = batch.map(async (friend) => {
const name = friend[0];
const blogUrl = friend[1];
const avatar = friend[2];
let rssUrl = friend.length >= 4 ? friend[3] : null;
// 如果没有提供 RSS,尝试盲猜
const candidates = [];
if (rssUrl) {
candidates.push(rssUrl);
} else {
const cleanUrl = blogUrl.replace(/\/$/, '');
candidates.push(`${cleanUrl}/atom.xml`);
candidates.push(`${cleanUrl}/rss.xml`);
candidates.push(`${cleanUrl}/feed`);
candidates.push(`${cleanUrl}/index.xml`);
}
let feed = null;
for (const url of candidates) {
feed = await fetchFeed(url);
if (feed) break;
}
if (!feed) {
console.log(`[${name}] No valid RSS found.`);
return;
}
// 提取文章
const posts = feed.items.slice(0, MAX_POSTS_PER_FRIEND).map(item => {
let img = null;
// 优先从 content 中提取图片
const content = item['content:encoded'] || item.content || item.description || '';
const imgMatch = content.match(/<img[^>]+src=['"]([^'"]+)['"]/i);
if (imgMatch) {
img = imgMatch[1];
} else if (item.enclosure && item.enclosure.url && item.enclosure.type && item.enclosure.type.startsWith('image')) {
img = item.enclosure.url;
}
// 提取摘要
let snippet = '';
if (content) {
snippet = content.replace(/<[^>]+>/g, '');
snippet = snippet.replace(/\s+/g, ' ').trim();
if (snippet.length > 120) {
snippet = snippet.substring(0, 120) + '...';
}
}
return {
title: item.title,
link: item.link,
date: item.isoDate || item.pubDate,
author: name,
avatar: avatar,
blogUrl: blogUrl,
image: img,
description: snippet
};
});
allPosts = allPosts.concat(posts);
});
await Promise.all(promises);
}
// 按时间倒序排序
allPosts.sort((a, b) => new Date(b.date) - new Date(a.date));
const finalPosts = allPosts.slice(0, MAX_TOTAL_POSTS);
const output = {
updated: new Date().toISOString(),
posts: finalPosts
};
fs.writeFileSync(OUTPUT_PATH, JSON.stringify(output, null, 2));
console.log(`Saved ${finalPosts.length} posts to ${OUTPUT_PATH}`);
}
main();
```
## 第二步:自动化 (GitHub Actions)
脚本写好了,总不能每天自己手动跑吧?这时候 GitHub Actions 就派上用场了。
在 `.github/workflows` 下创建一个 `upy.yml` (或者其他名字),设置定时任务。我是设置每天跑一次,或者每次 push 代码时触发。
```yaml
- name: Install Node.js dependencies
run: npm install rss-parser
- name: Generate Friend Circle Data
run: node scripts/generate_circle_data.js
```
把这两步加到你现有的构建流程里,放在 `hugo` 命令之前就行。
## 第三步:前端颜值 (HTML & CSS)
这一步是重头戏!为了达到那个卡片式、毛玻璃、还有丝滑动效的效果,我可是调了半天 CSS。
我直接把我的 `themes/Ying/layouts/_default/circles.html` 文件内容贴出来。这个文件集成了 HTML 结构、CSS 样式和 JS 逻辑(包括 PJAX 适配和加载更多功能)。
你可以直接在你的 Hugo 主题里新建一个 `layouts/_default/circles.html`,然后复制下面的代码:
```html
{{ define "main" }}
<div id="pjax-container">
<div class="post-content" itemprop="articleBody">
{{ .Content }}
<div id="friend-circle-container" class="friend-circle-list">
<div class="loading-container">
<div class="loading-spinner"></div>
<p>正在探索朋友们的新动态...</p>
</div>
</div>
</div>
<style>
/* 布局:PC端两列,间距紧凑 */
.friend-circle-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-top: 20px;
width: 100%;
}
/* 移动端一列 */
@media (max-width: 768px) {
.friend-circle-list {
grid-template-columns: 1fr;
gap: 10px;
}
}
/* 卡片核心样式:毛玻璃 + 动效 */
.fc-item {
display: flex;
flex-direction: column;
padding: 20px;
border-radius: 10px;
position: relative;
height: 160px;
justify-content: space-between;
background: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
border: 1px dashed rgba(255, 255, 255, 0.3); /* 虚线边框 */
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
overflow: hidden;
opacity: 0;
animation: ukFadeInUp 0.8s cubic-bezier(0.165, 0.84, 0.44, 1) forwards;
text-decoration: none;
color: #333;
}
/* 暗黑模式适配 */
[data-theme="dark"] .fc-item {
background: rgba(35, 35, 35, 0.65);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #fff;
}
/* 鼠标悬停特效 */
.fc-item:hover {
transform: translateY(-8px) scale(1.02);
background: rgba(255, 255, 255, 0.6);
border-color: #000;
z-index: 10;
}
/* 标题样式 */
.fc-title {
font-size: 18px;
font-weight: 700;
line-height: 1.4;
z-index: 2;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-right: 60px;
}
/* 背景装饰图(文章封面) */
.fc-bg-image {
position: absolute;
right: -20px;
bottom: -20px;
width: 140px;
height: 140px;
border-radius: 50%;
object-fit: cover;
opacity: 0.15;
transition: all 0.5s ease;
z-index: 0;
pointer-events: none;
filter: grayscale(20%);
}
.fc-item:hover .fc-bg-image {
opacity: 0.3;
transform: scale(1.1) rotate(5deg);
filter: grayscale(0%);
}
/* 底部信息栏 */
.fc-bottom {
display: flex;
justify-content: space-between;
align-items: flex-end;
z-index: 2;
}
/* 作者胶囊标签 */
.fc-author-pill {
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.05);
padding: 4px 10px 4px 4px;
border-radius: 20px;
transition: background 0.3s;
}
[data-theme="dark"] .fc-author-pill {
background: #1a1a1a;
border: 1px solid #333;
}
.fc-avatar {
width: 28px;
height: 28px;
border-radius: 50%;
margin-right: 8px;
}
.fc-author-name {
font-size: 13px;
font-weight: 600;
}
/* 加载更多按钮 */
.fc-load-more-btn {
grid-column: 1 / -1;
padding: 12px 30px;
margin: 30px auto 10px;
background: #000;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 30px;
color: #fff;
cursor: pointer;
font-size: 14px;
backdrop-filter: blur(10px);
}
/* 动画定义 */
@keyframes ukFadeInUp {
0% { opacity: 0; transform: translateY(40px) scale(0.95); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-spinner {
width: 40px; height: 40px;
border: 3px solid rgba(127,127,127,0.2);
border-top-color: #3498db;
border-radius: 50%;
animation: spin 1s infinite linear;
margin: 0 auto 15px;
}
</style>
<script>
// 时间格式化:YYYY-MM-DD
function timeAgo(dateStr) {
const date = new Date(dateStr);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
let allPosts = [];
let currentIndex = 0;
const BATCH_SIZE = 10;
function renderPosts(posts) {
const container = document.getElementById('friend-circle-container');
const existingBtn = document.getElementById('fc-load-more-btn');
if (existingBtn) existingBtn.remove();
let html = '';
posts.forEach((post, index) => {
const dateStr = timeAgo(post.date);
const delay = index * 0.05; // 瀑布流延迟动画
const bgImageSrc = post.image || post.avatar; // 有封面图用封面,没封面用头像
html += `
<a href="${post.link}" target="_blank" class="fc-item" style="animation-delay: ${delay}s">
<div class="fc-title" title="${post.title}">${post.title}</div>
<img src="${bgImageSrc}" class="fc-bg-image" loading="lazy" onerror="this.style.opacity=0">
<div class="fc-bottom">
<div class="fc-author-pill">
<img src="${post.avatar}" class="fc-avatar" loading="lazy">
<span class="fc-author-name">${post.author}</span>
</div>
<div class="fc-date">${dateStr}</div>
</div>
</a>
`;
});
container.insertAdjacentHTML('beforeend', html);
if (currentIndex < allPosts.length) {
const btn = document.createElement('button');
btn.id = 'fc-load-more-btn';
btn.className = 'fc-load-more-btn';
btn.innerText = '加载更多';
btn.onclick = loadMore;
container.appendChild(btn);
}
}
function loadMore() {
const nextBatch = allPosts.slice(currentIndex, currentIndex + BATCH_SIZE);
currentIndex += BATCH_SIZE;
renderPosts(nextBatch);
}
function loadFriendCircle() {
const container = document.getElementById('friend-circle-container');
if (!container) return;
fetch('/json/friend_circle_data.json?t=' + new Date().getTime())
.then(res => res.json())
.then(data => {
allPosts = data.posts;
currentIndex = 0;
container.innerHTML = ''; // 清除 loading
loadMore();
})
.catch(err => {
console.error('Error:', err);
container.innerHTML = '加载失败';
});
}
// 初始化加载
document.addEventListener('DOMContentLoaded', loadFriendCircle);
// 适配 PJAX
document.addEventListener('pjax:complete', loadFriendCircle);
</script>
</div>
{{ end }}
```
## 避坑指南
1. **CORS 问题**:因为我们是请求同域名的 JSON 文件,所以一般不会有跨域问题。但如果你把 JSON 传到了其他 CDN,记得配置 CORS。
2. **PJAX 白屏**:如果你像我一样用了 PJAX,一定要加 `document.addEventListener('pjax:complete', ...)` 这一行,否则跳转回来会白屏。
3. **样式冲突**:CSS 里的 `.friend-circle-list` 使用了 Grid 布局,如果你的主题有全局 CSS 冲突,可能需要微调。
## 总结
这一套下来,其实核心就三块:Node 脚本抓数据、Github Action 跑脚本、HTML 渲染页面。
虽然代码看起来有点多,但逻辑是很清晰的。把这些文件放到对应位置,基本上就能跑起来了。如果你也喜欢这种卡片风格,不妨试一试!
好了,代码都毫无保留地交出来了,希望能帮到同样在折腾 Hugo 的朋友们。如果有问题,欢迎在评论区留言(虽然我也不一定能解决哈哈)。
@@ -0,0 +1,56 @@
---
title: 博客六周年:从折腾到回归平淡
date: 2026-01-22T06:00:00.000Z
slug: 20260122
draft: false
categories:
- 生活
tags:
- 博客
- 年终总结
author: 小赵同学
ai_comment: >-
从动态博客到静态站点的技术迁移路径很有代表性,反映了技术成熟度曲线:初期追求功能密度,中期关注维护成本,最终回归内容本质。Hugo的文档确实是个筛选器,能坚持下来的都掌握了静态站点生成器的核心价值。建议后续可以展开谈谈从动态数据到静态内容的范式转换中,如何设计内容结构才能兼顾写作效率和呈现效果。
---
不知不觉,博客已经六岁了。
时间过得真快,回看第一篇文章的时间,仿佛还在昨天。2020年,那时的我还是个刚踏入大学校园的懵懂新生,怀揣着对互联网的好奇,搭建了这个属于自己的小天地。
## 关于折腾
这六年,如果要用一个词来形容,那一定是“折腾”。
最开始的时候,总觉得博客要越花哨越好。各种特效、各种插件,恨不得把网页塞得满满当当。那时候最快乐的事,就是给博客加个新功能,然后沾沾自喜地看半天。
后来,经历了无数次的“换主题”。Typecho 的主题被我换了个遍,正如我之前文章里写的,“又又又双叒叕换主题啦”。那时候不懂代码,就到处找教程,魔改 CSS,把好好的主题改得面目全非,但乐在其中。
直到后来,随着文章数量的增加,我开始意识到,博客的本质应该是内容,而不是花哨的外表。于是,我开始做减法,追求极致的简约。
## 关于迁移
最大的技术变动,莫过于从 Typecho 迁移到了 Hugo。
以前用 Typecho,虽然方便,但总要操心服务器、数据库、SSL 证书。服务器到期了要续费,数据库怕崩了要备份。对于我这种“贫穷”的博主来说,维护成本确实是个问题。
后来狠下心,把博客全站静态化,迁移到了 Hugo。虽然刚开始接触 Hugo 的时候,被那晦涩的文档折磨得不轻,甚至一度想放弃。但好在坚持下来了,还顺手把之前的 Ying 主题移植了过来。
现在的博客,托管在 GitHub,自动构建,全球 CDN 加速。不用担心服务器到期,不用担心数据库崩溃,甚至连 SSL 证书都是自动续期的。这种“省心”的感觉,是用过静态博客后就回不去的。
## 关于成长
博客记录了我从学生到“打工人”的转变。
翻看以前的博文,有高考前的紧张,有军训时的吐槽,有大学生活的琐碎,也有毕业时的迷茫。每一篇文章,都是当时心境的真实写照。
现在的我,已经毕业一年多了,每天忙碌于工作和生活之间,留给博客的时间越来越少。有时候想写点什么,打开编辑器,却又不知道从何下笔。
但这个博客,我会一直保留下去。它就像一个老朋友,默默地记录着我的成长轨迹。也许更新频率会变慢,但它永远都在。
## 写在最后
感谢这六年来,一直关注和支持我的朋友们。是你们的评论和鼓励,让我坚持到了现在。
未来的日子里,希望我们都能保持那份“折腾”的热爱,但也别忘了生活的本质。
六年,你好。
Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

@@ -0,0 +1,68 @@
---
title: 为什么我觉得网页昼夜切换那么重要
date: '2026-01-23T19:34:10+08:00'
slug: 20260123
draft: false
categories:
- 技术
- 生活
tags:
- TailwindCSS
- 暗黑模式
- 用户体验
- 吐槽
author: 小赵同学
layout: post
status: public
ai_comment: >-
Tailwind的dark:前缀确实把暗黑模式的实现成本降到了最低。不过建议考虑添加基于prefers-color-scheme的自动切换,毕竟手动点击对躺平用户不够友好。Slate色系的选择很专业,它的灰度曲线恰好避开了OLED屏幕的PWM调光敏感区。至于公司系统,或许可以写个油猴脚本把Dark
Reader的配置固化下来?
---
这篇是我下班在地铁通勤路上编写的,我认为一个网站支持暗黑模式(Dark Mode),真的很重要。
不知道大家有没有这样的经历:大晚上躲在被窝里刷手机,打开一个网页,突然一道白光刺瞎双眼,那种感觉简直就像是被闪光弹击中一样。从那一刻起,我就深刻意识到,博客的昼夜切换不仅仅是一个“酷炫”的功能,更是对博友眼睛最基本的尊重。
## 心中的一个疙瘩
其实,我的这个博客主题 **Ying** 在此之前一直是没有暗黑模式的。这一直是我心中的一个疙瘩。
这个主题我也用了快三年了,早期的代码里堆积了大量魔改的 CSS 样式,可以说是“屎山”也不为过。每次想加个暗黑模式,一看到那错综复杂的层叠样式表,我就头大,只能作罢。
但每次逛别人的博客,看到右上角那个精致的太阳/月亮切换按钮,轻轻一点,页面瞬间从明亮转为深邃,我都羡慕不已。尤其是在深夜,那种深色背景带来的沉浸感和舒适感,真的是白色背景无法比拟的。每次回到自己的博客,看到那惨白惨白的背景,总感觉少了点什么,既不护眼,也不够酷。
## Tailwind CSS 4:真香警告
最近这段时间,我开始接触并深度使用 **Tailwind CSS v4**。不得不说,这个原子化的 CSS 框架真的是太优秀了。
以前写 CSS,总是要在各种类名之间纠结,还要处理复杂的层叠关系。用了 Tailwind 之后,原子化的写法简直不要太爽,想要什么样式直接堆 class 就行,开发效率直线起飞。
最让我惊喜的是它对昼夜切换(Dark Mode)的适配,简直是“手拿把掐”。只需要在 class 前面加一个 `dark:` 前缀,就能轻松定义深色模式下的样式。
特别是官网推荐的 **Color Slate** 色系,简直是为夜晚而生。它不是那种死黑死黑的纯黑(#000000),而是一种带有蓝灰色调的高级灰。我毫不犹豫地选择了它作为 Ying 主题暗夜模式的主色调。这种颜色既保留了深色的沉稳,又不会让文字显得过于刺眼,阅读体验极佳。这种“高级感”正是我一直想要的。
![](image.png)
## 来自“瞎眼”打工人的吐槽
说完了开心的技术探索,再来吐槽一下糟心的工作。
我们公司的产品是一个 Web 端的数据运维平台。作为一名苦逼的打工人,我每天的工作就是盯着这个屏幕,监控数据、排查故障。
关键是,这个平台它**没有暗黑模式**!
你能想象吗?每天对着一个高亮的大白屏超过 8 个小时,尤其是有时候加班到晚上,办公室灯光一关,那个屏幕亮得简直像个探照灯,直射我的视网膜。每天下班回家,眼睛都酸胀得不行,感觉快要瞎了。
跟同事吐槽过无数次,希望能加个暗黑模式,结果永远是现在能跑起来已经很不错了。
无奈之下,我只能自力更生,给浏览器装了个 **Dark Reader** 插件,强制把公司的网页转成暗黑模式。虽然有时候颜色渲染会有点怪,图标也会变糊,但至少,我的眼睛算是保住了。
## 结语
正是因为自己在工作中饱受“光污染”的折磨,我才更坚定地要把博客的暗黑模式做好。
所以,无论是为了满足自己的强迫症,还是为了给深夜来访的朋友们一双“护眼符”,我都必须把 Ying 主题的昼夜切换功能做出来,而且要用最舒服的 Slate 配色,做到极致。
悄悄告诉你,我已经在 Ying 主题中实现了暗黑模式,并且用 Slate 配色,这里有个小机关,点击头像即可切换哦!
希望今晚的你,能在这个深色的页面里,读得舒服,睡得安稳。
Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

@@ -0,0 +1,262 @@
---
title: Artalk评论系统实现段落评论功能
date: 2026-01-24T11:40:00.000Z
draft: false
tags:
- Artalk
- 博客优化
- JS
- CSS
categories:
- 折腾记录
ai_comment: >-
段落评论功能的实现思路很清晰,技术选型也合理。mouseup监听+动态定位是经典方案,性能优化方面will-change的运用很到位。建议可以加入选区有效性校验,过滤掉过短或纯符号的选中内容。代码结构上,或许将事件监听和DOM操作封装成独立模块会更利于维护。暗黑模式适配这种细节考虑得很周到。
slug: 20260124
---
最近在番茄小说上看书,发现它的**段落评论**功能是真的好用。看到有共鸣的句子,选中就能直接看别人的评论或者自己吐槽两句。
![Artalk评论系统实现段落评论功能](0ed440d66514652540edc35c76a676b7.jpg)
我就在想,我的博客是不是也能整一个?毕竟现在还在坚持写博客的,谁还没点表达欲呢?
下午没事折腾了一下,试着给我的 Artalk 评论系统加上了这个功能。效果还凑合,选中文字后会弹出一个“引用评论”的按钮,点击就能自动跳转到评论区并引用选中的内容。
![Artalk评论系统实现段落评论功能](3.png)
稍微整理了一下代码,分享给有需要的朋友。我是个前端小白,代码写得比较粗糙,大佬们见笑了。无论你用什么主题,只要是 Artalk 评论系统,理论上都能适配。
## 实现思路
逻辑其实挺简单的,主要是以下几步:
1. **监听选择**:监听用户的鼠标选取操作 (`mouseup`)。
2. **计算位置**:获取选中范围的坐标,把按钮定位到鼠标或者选区上方。
3. **点击交互**:点击按钮后,获取选中的文字,平滑滚动到评论区,把文字填进输入框。
4. **细节优化**:做了一些防抖处理,简单适配了一下移动端和暗黑模式(这个必须有!)。
## 代码实现
### 1. HTML 结构
不需要手动写 HTML,直接用 JS 动态生成插入到 body 里就行,这样省事点。
### 2. CSS 样式
这块我调了半天。一开始用了毛玻璃效果 (`backdrop-filter`),结果发现页面滚动的时候有点掉帧,为了性能我把它去掉了,改成了更实用的深色/浅色背景,顺便加了 `will-change` 属性开启硬件加速。
把下面这段代码加到你的自定义 CSS 里:
```css
/* 选中弹出按钮样式 */
#selection-popup {
position: fixed;
display: none;
background: rgba(30, 30, 30, 0.9);
color: #fff;
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
z-index: 2147483647; /* 堆叠层级拉满 */
transform: translate(-50%, -100%);
pointer-events: auto;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transition: opacity 0.2s, transform 0.2s;
font-weight: 500;
line-height: 1.4;
user-select: none;
-webkit-user-select: none;
align-items: center;
gap: 6px;
white-space: nowrap;
will-change: transform, opacity; /* 性能优化关键 */
animation: popIn 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28);
}
/* 弹出动画 */
@keyframes popIn {
0% { transform: translate(-50%, -80%) scale(0.9); opacity: 0; }
100% { transform: translate(-50%, -100%) scale(1); opacity: 1; }
}
/* 小三角箭头 */
#selection-popup::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
margin-left: -6px;
border-width: 6px;
border-style: solid;
border-color: rgba(30, 30, 30, 0.9) transparent transparent transparent;
}
/* 悬停效果 */
#selection-popup:hover {
transform: translate(-50%, -110%) scale(1.05);
background: #000;
box-shadow: 0 6px 16px rgba(0,0,0,0.25);
}
/* 暗黑模式适配 */
[data-theme="dark"] #selection-popup {
background: rgba(255, 255, 255, 0.95);
color: #000;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
[data-theme="dark"] #selection-popup::after {
border-color: rgba(255, 255, 255, 0.95) transparent transparent transparent;
}
[data-theme="dark"] #selection-popup:hover {
background: #fff;
}
```
### 3. JavaScript 逻辑
把这段 JS 加到你的 `main.js` 或者 footer 的 `<script>` 标签里。
**注意**:为了性能,我这里用了 `requestAnimationFrame`,而且去掉了 `getBoundingClientRect` 这种耗性能的操作,直接用鼠标坐标定位,感觉会流畅一些。
```javascript
document.addEventListener('DOMContentLoaded', function() {
initSelectionPopup();
});
let selectionPopup = null;
let selectionTimeout;
function initSelectionPopup() {
// 避免重复创建
if (document.getElementById('selection-popup')) return;
// 动态创建按钮
selectionPopup = document.createElement('div');
selectionPopup.id = 'selection-popup';
// 这里用了 RemixIcon,你可以换成你自己的图标库
selectionPopup.innerHTML = '<i class="ri-chat-quote-line"></i> <span>引用评论</span>';
document.body.appendChild(selectionPopup);
// 防止在按钮上点击时触发选区清除
selectionPopup.addEventListener('mousedown', function(e) {
e.preventDefault();
e.stopPropagation();
});
// 点击按钮的逻辑
selectionPopup.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
let selection = window.getSelection().toString();
if (selection) {
// 简单的文本清理
selection = selection.split('\n')
.map(line => line.trim())
.filter(line => line !== '')
.join('\n');
// 限制长度,防止太长刷屏
if (selection.length > 500) {
selection = selection.substring(0, 500) + '...';
}
scrollToCommentsAndQuote(selection);
hideSelectionPopup();
window.getSelection().removeAllRanges(); // 清除选区
}
});
// 监听全局选择事件
document.addEventListener('mouseup', handleSelectionChange);
document.addEventListener('keyup', handleSelectionChange);
// 页面滚动时隐藏
document.addEventListener('scroll', hideSelectionPopup, { passive: true });
}
function hideSelectionPopup() {
if (selectionPopup) {
selectionPopup.style.display = 'none';
}
}
function handleSelectionChange(e) {
// 防抖处理,别频繁触发
clearTimeout(selectionTimeout);
selectionTimeout = setTimeout(() => {
const selection = window.getSelection();
const text = selection.toString().trim();
// 记得改成你文章容器的类名,我这里是 .post-content
const content = document.querySelector('.post-content');
if (!selectionPopup || !content) return;
// 没选中文字就隐藏
if (!text || selection.rangeCount === 0) {
hideSelectionPopup();
return;
}
const range = selection.getRangeAt(0);
// 确保选中的是文章里的内容,别把侧边栏也选进去了
if (!content.contains(range.commonAncestorContainer)) {
hideSelectionPopup();
return;
}
// 使用 requestAnimationFrame 优化 UI 更新
requestAnimationFrame(() => {
selectionPopup.style.display = 'flex';
// 定位逻辑:鼠标抬起时跟随鼠标,键盘选择时跟随选区
if (e && e.type === 'mouseup') {
// 直接用鼠标坐标,比 getBoundingClientRect 性能好多了
selectionPopup.style.top = `${e.clientY - 40}px`;
selectionPopup.style.left = `${e.clientX}px`;
} else {
// 键盘操作没办法,只能计算矩形了
const rect = range.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
hideSelectionPopup();
return;
}
selectionPopup.style.top = `${rect.top}px`;
selectionPopup.style.left = `${rect.left + rect.width / 2}px`;
}
});
}, 150);
}
// 跳转评论区并填充内容
function scrollToCommentsAndQuote(text) {
const comments = document.getElementById('Comments'); // 评论区 ID
if (comments) {
comments.scrollIntoView({ behavior: 'smooth' });
// 等滚动完了再填内容
setTimeout(() => {
const textarea = document.querySelector('.atk-textarea'); // Artalk 输入框 class
if (textarea) {
const quote = `> ${text}\n\n`;
textarea.value = quote;
textarea.focus();
// 触发 input 事件,适配 Vue/React 框架的数据绑定
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.dispatchEvent(new Event('change', { bubbles: true }));
}
}, 500);
}
}
```
## 简单总结
代码复制进去,刷新页面(如果有缓存记得清一下),选中这段文字试试?
这个功能不仅能提高读者的互动率,还能让评论更有针对性。我也就是瞎折腾,如果有更好的实现方式,欢迎大家在评论区指教!
@@ -0,0 +1,138 @@
---
layout: post
title: Hugo静态博客如何实现搜索功能
date: 2026-01-28T02:00:00.000Z
updated: 2026-01-28T02:00:00.000Z
draft: false
author: 小赵同学
categories:
- 建站
tags:
- Hugo
- Search
- PJAX
- 折腾
gemini_comment: null
location: null
show_theme: 1
top_img: 'https://picsum.photos/800/400'
description: 文章慢慢多了起来,有时候自己想找以前写的内容都得翻半天。于是抽空给博客加了个简单的搜索功能,没用什么高大上的第三方服务,纯前端实现,简单好用。
ai_comment: >-
这个自建搜索方案很好地体现了技术选型的平衡感。用静态JSON+前端匹配解决小体量检索需求,避免引入重型依赖,是典型的80/20法则实践。PJAX适配和防抖处理这些细节显示出实战经验。不过建议可以加入简单的TF-IDF权重计算,让标题匹配的结果排在摘要匹配之前。另外考虑在JSON模板里加入文章标签字段,未来扩展按标签过滤会很容易。
slug: 20260128
---
最近发现博客文章数量慢慢上来了,有时候想找自己以前写的某个知识点,翻半天都翻不到,确实有点不方便。
其实早就想加个搜索功能了,但看了一圈,像 Algolia 这种第三方服务虽然强大,但配置起来感觉有点繁琐,而且对于咱们这种访问量不大的静态博客来说,是不是有点“杀鸡用牛刀”了?
想了想,还是决定用最简单的方式来实现:直接生成一个 JSON 索引文件,用 JS 在前端搜一下。虽然比较基础,但胜在轻量,而且完全可控。
折腾了几个小时,终于搞定了,顺便把交互做得稍微优化了一下,支持了 PJAX 和暗黑模式。这里简单记录一下实现过程,希望能给同样在折腾 Hugo 的朋友一点参考。
## 效果展示
为了不破坏页面的简洁感,我把搜索入口藏在了两个地方:
1. **右下角悬浮工具栏**:点那个放大镜图标,会弹出一个搜索框。
2. **首页“随笔”标题**:这个算是个小彩蛋吧。在首页找到“随笔”这两个字,鼠标放上去会有“点击搜索”的提示,点一下就能直接在原地搜索,不用跳转。
## 实现思路
原理其实非常简单,主要就三步:
1. 让 Hugo 构建时多生成一个 `index.json` 文件,里面包含所有文章的标题、链接和摘要。
2. 前端 JS 通过 `fetch` 请求这个 JSON 文件。
3. 根据用户输入的关键词,在 JSON 数据里进行匹配,然后把结果渲染出来。
## 代码实现
### 1. 配置 Hugo 输出 JSON
首先在 `hugo.toml` (或者 `config.toml`) 里配置一下输出格式,告诉 Hugo 首页除了 HTML 还要输出 JSON。
```toml
[outputs]
home = ["HTML", "RSS", "JSON"]
```
### 2. 创建 JSON 模板
在主题的 `layouts/index.json` 创建一个模板文件,定义 JSON 的数据结构。这里我只取了标题、日期、链接和摘要,尽量让文件小一点。
```go
[
{{- range $index, $e := where .Site.RegularPages "Type" "post" -}}
{{- if $index -}}, {{- end -}}
{
"title": {{ .Title | jsonify }},
"date": {{ .Date.Format "2006-01-02" | jsonify }},
"permalink": {{ .Permalink | jsonify }},
"summary": {{ .Summary | plainify | jsonify }}
}
{{- end -}}
]
```
这样每次 `hugo` 构建的时候,网站根目录就会生成一个 `index.json`。
### 3. 前端 JS 逻辑
逻辑主要在 `main.js` 里。为了提升体验,我做了一些小优化:
* **懒加载**:不会一打开网页就下载 JSON,而是等你点击搜索按钮时才去加载,帮大家省点流量。
* **防抖处理**:输入时不会每敲一个字都去搜索,而是稍微停顿一下再触发,避免频繁计算。
* **PJAX 适配**:因为博客用了 PJAX 做无刷新跳转,一开始遇到个坑,点击搜索结果页面会白屏或者刷新。后来发现需要在渲染结果后,手动调用一下 `window.pjax.refresh()`,让 PJAX 重新接管这些新生成的链接。
```javascript
// 简单的搜索逻辑
const results = window.searchIndex.filter(item => {
// 简单的关键词匹配
return item.title.includes(query) || item.summary.includes(query);
});
displayResults(results);
```
### 4. 那个“随笔”变搜索框的交互
这个交互其实主要是 CSS 在控制。
默认是一个普通的 `<h2>` 标题,点击后通过 JS 隐藏标题,显示输入框。为了过渡自然一点,加了一些简单的 CSS 动画。
```css
/* 默认隐藏提示 */
.search-hint {
opacity: 0;
transition: all 0.3s ease;
}
/* 鼠标放上去显示 */
.section-header.normal-mode:hover .search-hint {
opacity: 1;
transform: translateX(0);
}
```
为了照顾移动端用户,我在 CSS 里加了媒体查询,手机上直接显示“点击搜索”的提示,不然手机上没有 hover 状态,可能会不知道这里可以点。
## 遇到的坑
折腾过程中最大的坑还是 **PJAX**。
一开始写好的时候,搜索功能本身没问题,但是点搜索结果跳转的时候,要么页面刷新,要么音乐播放器断了。查了半天文档,才发现是因为动态插入的 HTML(搜索结果列表),PJAX 默认是监听不到的。
解决办法就是在插入 HTML 后,手动通知一下 PJAX:
```javascript
if (window.pjax) window.pjax.refresh(searchResults);
```
另外,为了体验好一点,我还加了个逻辑:点击搜索结果后,自动关闭搜索框,不然跳转完搜索框还挡在那里,确实有点傻。
## 总结
虽然只是个小功能,但前前后后也调了好久,特别是细节上的打磨。不过看到最后丝滑的搜索体验,感觉还是挺值得的。
如果你也在用 Hugo,不想接复杂的第三方搜索,不妨试试这个方案,轻量、简单,完全够用了。
代码就不全贴了,核心逻辑都在上面,有兴趣的朋友可以自己折腾一下。如果有更好的实现方式,也欢迎留言交流哈!
@@ -0,0 +1,128 @@
---
layout: post
title: 给Hugo博客添加瀑布流相册功能
date: 2026-01-30T05:00:00.000Z
updated: 2026-01-30T05:00:00.000Z
draft: false
author: 小赵同学
categories:
- 折腾
- 建站
tags:
- Hugo
- CSS
- 瀑布流
- 相册
ai_comment: >-
数据与表现分离的思路很清晰,YAML+CSS
column-count的方案确实轻量。注意到你处理了图片懒加载和移动端适配这两个关键点,这是很多初级实现容易忽略的。如果考虑进一步优化,可以测试下图片宽高比固定的情况下,用aspect-ratio属性避免布局抖动。技术上,这种纯CSS方案在图片高度差异较大时可能出现空白间隙,但作为技术博客的相册已经足够实用。
gemini_comment: null
location: null
show_theme: 1
top_img: 'https://picsum.photos/800/400'
slug: 20260130
---
前两天逛博客,偶然看到了 [immmmm](https://immmmm.com/album/) 大佬的相册页面,觉得那种瀑布流的布局方式展示照片很有感觉,错落有致。
正好最近也想整理一下自己的照片,于是想着能不能也在自己的 Hugo 博客上实现类似的效果。
自己也是在摸索中前进,参考了不少前辈的教程,这里简单记录一下实现的思路,希望能给同样有需求的朋友一点参考。
## 实现思路
我的想法比较简单,主要是想利用 Hugo 的 `Data Templates` 功能,把照片的数据和页面的展示分离开来,这样管理起来比较方便。
1. **数据层**:用 YAML 文件存储照片的链接、描述等信息。
2. **展示层**:编写一个专门的 Layout 模板来读取并渲染这些数据。
3. **样式层**:尝试使用 CSS 的 `column-count` 属性来实现瀑布流布局。
## 第一步:准备数据
首先在 `data` 目录下新建了一个 `album.yml` 文件。
格式如下,每张照片作为一个 item:
```yaml
- title: "随手拍的风景"
image: "https://picsum.photos/300/400"
date: "2024-01-29"
description: "记录生活中的一角"
- title: "本地图片测试"
image: "/album/my-photo.jpg" # 图片放在 static/album/ 目录下
date: "2026-01-30"
```
这样以后添加照片,只需要在这个文件里追加内容就可以了。
## 第二步:编写模板
接着在 `layouts/_default/` 下新建了 `album.html` 模板文件。
主要逻辑是遍历 `site.Data.album` 中的数据:
```html
{{ define "main" }}
<div id="pjax-container">
<div class="post-content">
<div class="album-content">
{{ range site.Data.album }}
<div class="album-item">
<div class="album-img-box">
<img src="{{ .image }}" loading="lazy">
{{ if .date }}<span class="album-date-badge">{{ .date }}</span>{{ end }}
<div class="album-info">
<span class="album-title">{{ .title }}</span>
</div>
</div>
</div>
{{ end }}
</div>
</div>
</div>
{{ end }}
```
为了优化加载体验,给 `img` 标签加上了 `loading="lazy"` 属性。
## 第三步:CSS 样式
瀑布流的实现方式有很多种,我选择了一种比较简单的 CSS 方案:
```css
.album-content {
column-count: 4; /* 设置列数 */
column-gap: 10px;
}
.album-item {
break-inside: avoid; /* 防止元素被分割 */
margin-bottom: 10px;
/* 简单的淡入动画 */
animation: fadeIn 0.6s ease forwards;
}
/* 简单的移动端适配 */
@media (max-width: 768px) {
.album-content {
column-count: 2;
}
}
```
使用 `column-count` 可以快速实现多列布局,配合 `break-inside: avoid` 避免图片在列之间断开。虽然可能不如 JS 库那么完美,但对于简单的相册展示来说,我觉得够用了。
## 一些小细节
在实现过程中,也做了一点微小的调整:
1. **加载动画**:加了一个简单的淡入效果,让图片出现得不那么突兀。
2. **本地图片**:在 `static` 目录下建了 `album` 文件夹,方便存放一些本地的照片。
3. **导航调整**:考虑到手机屏幕较小,在移动端隐藏了相册的导航入口,保持界面的整洁。
## 总结
折腾了一番,总算是把相册页面搭建起来了。虽然代码写得比较简单,样式也还有优化的空间,但看着自己的照片能以这样的方式展示出来,还是挺开心的。
如果有更好的实现方式,也欢迎大家指教。
@@ -0,0 +1,137 @@
---
title: Artalk评论区接入AI摘要的尝试
date: 2026-01-31T01:00:00.000Z
draft: false
categories:
- 尝试
- 技术
tags:
- Artalk
- AI
- GitHub Actions
- SiliconFlow
ai_comment: >-
把AI摘要伪装成首条评论的创意很巧妙,既保持了界面简洁又增强了互动感。技术上把生成逻辑放在CI/CD环节是明智选择,不过建议在AI提示词里加入"避免使用第一人称"的约束——否则当AI说"我很喜欢这篇文章"时,读者可能会困惑这是真实用户还是机器。另外,GitHub
Actions自动提交的颗粒度可以细化到单篇文章,避免全量扫描带来的不必要计算开销。
slug: 20260131
---
最近看到很多博客都加上了 AI 摘要功能,觉得挺有意思,于是也尝试给自己的博客加了一个。
我的需求很简单:
1. **自动生成**:发布新文章时自动调用 API 生成摘要。
2. **融入评论区**:不想额外增加 UI 模块,最好能像一条普通的“置顶评论”一样展示。
3. **零成本**:利用现有的免费 API(DeepSeek V3)。
目前已经实现,效果还算满意。简单记录一下实现过程。
## 1. 后端:生成 AI 评论
写了一个简单的 Node.js 脚本,遍历文章目录,提取正文并调用 SiliconFlow 的 API 生成评论,最后回写到 Front Matter 中。
核心代码 (`scripts/ai_comment.js`):
```javascript
// ... (imports & config)
async function generateComment(content) {
const prompt = `你是一个热情的博客读者。请阅读以下文章内容,并写一段简短的评论(100字以内)。
...
文章内容:${content.substring(0, 2000)}...`;
// 调用 SiliconFlow API
const response = await axios.post(SILICONFLOW_URL, {
model: 'deepseek-ai/DeepSeek-V3',
messages: [{ role: 'user', content: prompt }],
// ...
}, { headers: { 'Authorization': `Bearer ${process.env.SILICONFLOW_API_KEY}` } });
return response.data.choices[0].message.content.trim();
}
// 遍历文件并处理
async function processFiles(dir) {
// ...
if (parsed.data.ai_comment) return; // 跳过已生成的文章
const comment = await generateComment(cleanContent);
if (comment) {
parsed.data.ai_comment = comment;
// 回写文件
await fs.promises.writeFile(filePath, matter.stringify(parsed.content, parsed.data));
}
}
```
## 2. 自动化:GitHub Actions
为了省去手动运行脚本的麻烦,将其集成到了 CI/CD 流程中。
在 `.github/workflows/upy.yml` 中添加步骤:
```yaml
# 生成 AI 评论
- name: Generate AI Comments
env:
SILICONFLOW_API_KEY: ${{ secrets.SILICONFLOW_API_KEY }}
run: node scripts/ai_comment.js
# 提交变更 (注意需要 permissions: contents: write)
- name: Commit AI Comments
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"
git add content/post
if ! git diff-index --quiet HEAD; then
git commit -m "chore: auto-generate ai comments [skip ci]"
git push
fi
```
## 3. 前端:Artalk 集成
最后一步是在前端展示。利用 Artalk 的事件机制,将 AI 评论动态插入到评论列表头部,并加上打字机特效。
核心代码 (`assets/js/main.js`):
```javascript
window.artalk.on('list-loaded', function() {
// 检查是否有 AI 评论数据
if (window.artalkConfig.aiComment && !document.querySelector('.atk-ai-comment')) {
var listBody = document.querySelector('.atk-list-body') || document.querySelector('.atk-list');
if (listBody) {
// 避免同时出现“此时无声胜有声”
var noComment = document.querySelector('.atk-list-no-comment');
if (noComment) noComment.style.display = 'none';
// 创建 DOM 结构
var aiDiv = document.createElement('div');
aiDiv.className = 'atk-comment atk-ai-comment';
aiDiv.innerHTML = `...`; // 填充 HTML
// 插入到列表头部
listBody.prepend(aiDiv);
// ... (打字机效果)
}
}
});
```
## 4. 适配 PJAX
由于博客使用了 PJAX,页面切换时需要重新初始化 Artalk。我们将初始化逻辑封装在 `initArtalk` 函数中,并在 `pjax:complete` 事件中调用。
```javascript
// mypjax.js
document.addEventListener("pjax:complete", function () {
// ...
// 重载评论
if (typeof window.initArtalk === 'function') {
window.initArtalk();
}
});
```
这样就完成了一个简单实用的 AI 摘要功能。既丰富了页面内容,又没有过度干扰阅读体验。
@@ -0,0 +1,190 @@
---
title: 博客友链实时健康监测方案
date: 2026-02-01T01:00:00.000Z
draft: false
categories:
- 技术
tags:
- GitHub Actions
- Node.js
- 博客优化
- 自动化
ai_comment: >-
这个友链监测方案设计得很周全,技术上亮点不少。并发控制+连接复用的组合拳很实用,能有效降低检测延迟。建议可以补充一个重试退避机制,避免因临时网络抖动误判。前端展示的克制设计值得点赞,不过可以考虑在hover时显示具体状态码,方便调试。GitHub
Actions的邮件通知处理得很优雅,把运维细节藏在了后台。
slug: 20260201
---
最近给博客加上了一个实用的功能:**友链实时健康监测**。
起因是备案审查越来越严格。对于个人博客来说,如果友链中的某个域名过期被抢注,摇身一变成为博彩或色情网站,而我的博客还挂着它的链接,那么我的域名很有可能因为“导流违规内容”而被注销备案,甚至面临关站风险。
为了避免这种“人在家中坐,锅从天上来”的情况,手动每天一个个点开检查显然是不现实的。于是,利用 GitHub Actions 和简单的 Node.js 脚本实现自动化监测,就成了最佳解决方案。
## 实现思路
整体流程分为三个环节:
1. **数据源**:读取博客现有的友链配置文件(YAML 格式)。
2. **监测脚本**:编写 Node.js 脚本,利用并发和连接复用技术,批量检测 HTTP 状态码。
3. **自动化与通知**:通过 GitHub Actions 定时执行,生成状态报告(JSON),并在发现异常时发送邮件通知。
4. **前端展示**:在页面上以不打扰的 UI 风格(黑白极简风格)展示友链状态。
## 1. 编写监测脚本
在 `scripts/check_links.js` 中,我使用了 `axios` 发送请求,并配合 `https.Agent` 实现了连接复用(Keep-Alive),同时通过并发池控制请求速率。
这个脚本的核心亮点是:
- **并发控制**:开启 10 个 Worker 并行检测,大幅缩短耗时。
- **连接复用**:开启 `keepAlive: true`,减少 TCP/TLS 握手开销。
- **智能重试**:优先使用 `HEAD` 请求(省流量),失败后自动降级为 `GET` 请求。
- **异常汇总**:将失效链接写入 `failed_links_summary.txt`,用于后续邮件通知。
```javascript
const fs = require('fs');
const yaml = require('js-yaml');
const axios = require('axios');
const path = require('path');
const https = require('https');
const http = require('http');
// ... 路径配置 ...
// 优化 Axios 实例:开启 Keep-Alive 复用连接
const axiosInstance = axios.create({
timeout: 10000,
maxRedirects: 5,
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true }),
headers: {
'User-Agent': 'Mozilla/5.0 ...' // 模拟浏览器 UA
},
validateStatus: (status) => status >= 200 && status < 400
});
async function checkUrl(url) {
try {
await axiosInstance.head(url);
return true;
} catch (error) {
try {
await axiosInstance.get(url); // HEAD 失败尝试 GET
return true;
} catch (err2) {
return false;
}
}
}
// ... 并发 Worker 逻辑 ...
```
## 2. 配置 GitHub Actions
为了保证数据的实时性,我在 `.github/workflows/upy.yml` 中配置了定时任务。
- **定时触发**:每 12 小时自动运行一次 (`cron: '0 */12 * * *'`)。
- **邮件通知**:如果发现失效友链,Workflow 会读取脚本生成的摘要文件,并通过邮件推送到我的邮箱。
```yaml
on:
schedule:
- cron: '0 */12 * * *' # 每12小时自动检查一次
jobs:
deploy:
steps:
# ... 其他步骤 ...
- name: Check Link Status
run: node scripts/check_links.js
- name: Read Failed Links Summary
if: always()
run: |
if [ -f failed_links_summary.txt ]; then
echo "LINK_SUMMARY<<EOF" >> $GITHUB_ENV
cat failed_links_summary.txt >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
fi
# ... 构建步骤 ...
- name: Send deployment success notification
if: success()
uses: dawidd6/action-send-mail@v3
with:
# ... 邮件配置 ...
body: |
# ... 其他信息 ...
📊 友链监测报告
----------------------------------
${{ env.LINK_SUMMARY }}
```
## 3. 前端展示:极简设计
为了不破坏博客原本的黑白极简风格,我并没有使用传统的“红/绿”大色块,而是设计了一套低调的状态指示器。
### JS 逻辑 (`main.js`)
前端脚本会异步加载 `link_status.json`,并智能匹配 URL(自动处理 `https` 和尾部 `/` 的差异)。
```javascript
window.initLinkStatus = function() {
fetch('/link_status.json?t=' + new Date().getTime())
.then(response => response.json())
.then(data => {
// ... 遍历 DOM 元素 ...
// getUrlStatus 逻辑:尝试匹配 url, url/, https/http 变体
if (status === 'alive') {
$item.append('<span class="link-active-badge" title="可访问"></span>');
} else if (status === 'dead') {
$item.append('<span class="link-dead-badge" title="无法访问"></span>');
}
});
};
```
### CSS 样式 (`main.css`)
这里花了一些心思调整配色。为了避免“红绿灯”配色的突兀感:
- **正常状态**:使用**实心灰点** (`#9ca3af`),在暗色模式下为浅灰 (`#d1d5db`)。
- **失效状态**:使用**空心灰圈**(白色/深色中心 + 灰色边框)。
这种设计既能传达状态信息,又完美融入了黑白主题。
```css
/* Card View - Active (实心灰点) */
.link-active-badge {
position: absolute;
top: 8px; right: 8px;
width: 8px; height: 8px;
background-color: #9ca3af;
border-radius: 50%;
border: 2px solid #fff;
}
/* Card View - Dead (空心灰圈) */
.link-dead-badge {
position: absolute;
top: 8px; right: 8px;
width: 8px; height: 8px;
background-color: #fff;
border-radius: 50%;
border: 2px solid #9ca3af; /* 灰色边框形成空心效果 */
}
```
## 总结
这套方案最大的好处是**零成本、自动化且优雅**。
1. **零成本**:完全利用 GitHub Actions 的计算资源。
2. **自动化**:定时检查 + 邮件报警,无需人工干预。
3. **优雅**:前端 UI 克制且统一,不喧宾夺主。
对于注重 SEO 和网站安全的博主来说,这是一个性价比极高的“基建”工作。既对自己负责,也对访客负责。毕竟,谁也不想点击一个链接后发现是 404,或者跳转到奇怪的页面吧。
@@ -0,0 +1,109 @@
---
title: 我把Vercel换成了EdgeOne Pages
date: 2026-02-02T01:00:00.000Z
draft: false
categories:
- 技术
- 碎碎念
tags:
- Hugo
- EdgeOne
- Upyun
ai_comment: >-
这种双线部署策略很务实,像给网站穿了件"两面穿"外套。EdgeOne Pages 的初期适配问题确实典型,您用 GitHub Actions
做构建层隔离的思路漂亮——相当于给不成熟的服务加了层适配器。DNS
分流配置写得清晰,不过建议补充测试不同地区解析生效时间,我曾遇到部分东南亚节点缓存更新延迟的情况。腾讯云的文档有时像迷宫,但您这实操记录比官方教程更有参考价值。
slug: 20260202
---
最近感觉 Vercel 虽然好用,但在国内或者部分亚洲地区的访问速度有时候还是不太稳定。前段时间油老师给我推荐了这个 EdgeOne Pages,今天有空试了试腾讯云新出的 EdgeOne Pages。
听说 EdgeOne 的节点覆盖很不错,而且对个人开发者也有免费额度,于是心血来潮,决定把博客的境外加速线路从 Vercel 换成 EdgeOne。
## 现在的部署方案
为了保证访问速度和稳定性,我现在的策略是:
* **国内(境内)**:继续用 **又拍云 (Upyun)**。老牌子了,国内节点多,速度快,用了很久一直很稳,没必要换。
* **国外(境外)**:换成了 **腾讯云 EdgeOne Pages**。用来替代 Vercel,主要负责港澳台和海外的流量。
这样既能保证国内访客秒开,又能照顾到海外访问的体验,相当于搞了个“双保险”。
## 迁移过程
其实过程没有我想象的顺利,EdgeOne Pages 给我更多的感觉是一个半成品,很多功能还在完善中。
我看了支持的框架,还没有适配 Hugo,实际部署起来坑不少。我试了三次,用自定义配置也没跑通,老是报错。
既然官方的构建环境不太稳,那我就把构建这一步放在自己手里。转变思路:**在 GitHub Actions 里构建好,然后直接把生成的静态文件推送到 EdgeOne Pages**。
### 1. 准备一个部署脚本
参考一下其他博主的文档,以及询问豆包,下面分享我的思路。首先,在项目里加个脚本 `scripts/deploy_edgeone.sh`,利用 EdgeOne 的 CLI 工具来上传文件。
```bash
#!/bin/bash
# EdgeOne Pages Deployment Script
# 1. 检查环境变量
if [ -z "$EDGEONE_API_TOKEN" ]; then
echo "Error: EDGEONE_API_TOKEN is not set."
exit 1
fi
# 2. 设置默认值
PROJECT_NAME=${1:-"hugo-blog"}
BUILD_DIR=${2:-"./public"}
echo "Deploying to EdgeOne Pages..."
echo "Project: $PROJECT_NAME"
echo "Directory: $BUILD_DIR"
# 3. 执行部署命令
# 使用 npx 运行 edgeone CLI,无需全局安装
npx edgeone pages deploy "$BUILD_DIR" -n "$PROJECT_NAME" -t "$EDGEONE_API_TOKEN"
# 4. 检查结果
if [ $? -eq 0 ]; then
echo "Deployment successful!"
else
echo "Deployment failed!"
exit 1
fi
```
### 2. 配置 GitHub Actions
然后,在 `.github/workflows/deploy.yml` 里加上这一步。记得先去 EdgeOne 控制台生成一个 API Token,然后放到 GitHub 仓库的 Secrets 里(名字叫 `EDGEONE_API_TOKEN`)。
```yaml
- name: Deploy to EdgeOne Pages
env:
EDGEONE_API_TOKEN: ${{ secrets.EDGEONE_API_TOKEN }}
run: |
chmod +x scripts/deploy_edgeone.sh
./scripts/deploy_edgeone.sh hugo-blog ./public
```
这样一来,每次我推送代码,GitHub Actions 就会自动把 Hugo 构建好的 `public` 文件夹推送到 EdgeOne,既省心又稳定。
### 关键的 DNS 设置
部署好之后,最关键的一步就是在域名解析里做分流。
我是在 dnspod 服务商那里这样设置的:
1. **默认线路(Default)**:CNAME 指向 EdgeOne 提供的域名。这样海外用户就会走 EdgeOne 的加速节点。
2. **境内线路(China)**:CNAME 指向又拍云的域名。这样国内用户依然走又拍云,速度不受影响。
## 体验总结
切过来,简单测试一下,整体感觉挺香的。
* **速度方面**:简单测了一下,海外节点的响应确实挺快,感觉比 Vercel 要稳一些。
* **管理方便**:毕竟是腾讯云的产品,控制台全中文,功能看着也直观。
总之,这次调整算是一次小小的升级吧,既保留了国内的优势,又补齐了境外的短板。对于像我这样喜欢把博客托管在静态服务上的朋友,EdgeOne Pages 确实是个不错的替代方案。
Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

@@ -0,0 +1,70 @@
---
title: 周末闲暇时间翻修了一下博客
date: 2026-02-03T01:00:00.000Z
draft: false
categories:
- 碎碎念
- 博客日记
tags:
- Hugo
- 体验优化
ai_comment: >-
Glassmorphism的应用是个明智选择,模糊层级处理得当。不过建议测试下低端设备的渲染性能,背景模糊有时会触发重绘。搜索框位置调整符合F型阅读动线,但可以考虑增加搜索图标的热区面积。懒加载动画的缓动函数如果改用cubic-bezier(0.25,0.1,0.25,1)会更接近物理惯性效果。技术债偿还和用户体验提升的平衡做得很好。
slug: 20260203
---
这周末不用加班,人闲下来了,打开电脑,chorme浏览器键入 usj.cc ,简单看了一下博客的样式,有几处我还不是很满意。
怎么说呢,虽然一直都在用,但总感觉在一些交互细节和视觉呈现上,还有提升的空间。既然发现了问题,那就趁着有空,把这些“边边角角”打磨一下。
为了避免毫无意义的折腾,这次我并没有什么大刀阔斧的重构,只是做了一些修修补补的工作,希望能让来访的朋友(包括我自己)看着更舒服一点。
毕竟新年新气象,博客作为心灵的镜子,也需要“翻新”一下。
## 视觉上的“透气感”
之前的界面设计,色块感比较强,看久了难免觉得有些沉闷。
最近留意到一种叫 **Glassmorphism(毛玻璃)** 的设计风格,通过背景模糊和透明度,能营造出一种层次感。于是尝试着给顶栏和文章卡片加上了这种效果。
调整后的界面,背景若隐若现,整体视觉上轻盈了不少。就像是给窗户擦去了灰尘,通透感出来了,心情也跟着明亮了一些。
## 首页搜索框的“改造”
之前的搜索框,一直放在右下角作为工具栏存在,思来想去,还是感觉和整体的极简风格不太搭。
于是在蹲坑的时候,奇思妙想,我为何不放到随笔上,这样既能保证风格的简约,又能增加一个搜索文章的入口
就是简单CSS的调整,然后把之前的搜索文章逻辑进行复用,于是就有了现在的效果,做完,还美美的欣赏了一遍,真别说,还不错!
## 一些贴心的小功能
除了视觉上的调整,也加了两个实用的小功能:
* **昼夜切换**:对于像我这样喜欢深夜阅读的人来说,刺眼的白底黑字简直是“视力杀手”。加上这个切换按钮,是对眼睛最基本的尊重,你可以点击头像进行切换,也可以在其他页面通过工具栏进行切换昼夜模式,这里听取了一个博友的建议,改了昼夜切换的逻辑,默认第一次获取访客的深色模式状态,后续再根据访客的操作进行切换。
* **蕉太狼温馨提示**:在首页右下角加了一个小小的“蕉太狼”挂件。它会在早晨问好,深夜催睡。虽然只是简单的几行代码逻辑,但让博客多了一点“人情味”。
* **丝滑的图片加载**:给文章里的图片加了个“懒加载”动画。以前图片是“刷”的一下出来的,现在是当你滑到它的时候,它会优雅地从下往上浮现出来,那种顺滑的感觉,就像是德芙巧克力一样(夸张了哈),反正我是盯着看了好几遍。
## 效果展示
旧样式
![old style](1900538379.png)
新样式
![new style](Snipaste_2026-02-02_19-50-41.png)
![](Snipaste_2026-02-02_19-52-27.png)
![new style](Snipaste_2026-02-02_19-52-37.png)
## 写在最后
其实做博客这么久,越来越觉得,**内容始终是核心,而样式只是容器**。
我们花时间去优化样式,不是无休止的折腾,而是为了让盛在里面的内容能以更优雅、更舒适的姿态呈现给读者。
这次的调整,算是一次小小的“大扫除”吧。看着焕然一新的小站,心里也觉得踏实了不少。希望你们也会喜欢这些细微的变化。
Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

@@ -0,0 +1,35 @@
---
layout: post
title: 一个人烧烤被拒单了
date: 2026-02-05T14:00:00.000Z
author: 小赵同学
categories:
- 生活
tags:
- 碎碎念
- 孤独
ai_comment: >-
这篇对微交易的观察很有趣。烧烤摊的"最低消费"机制像极了云计算服务的阶梯定价——资源粒度不匹配用户需求时,双方都陷入尴尬。建议老板可以学AWS的按量付费模式,把调料成本折算进单价,毕竟现代人"饿点"越来越碎片化了。
slug: 20260205
---
今天下班不是很饿,路过了一家烧烤摊,好久没吃烧烤了,突然有点馋。
走到摊位前,看着五花八门的食材,我挑了两串羊肉,两串牛肉,还有其他素串,前前后后四十来块钱。
老板说:“5串起步,一串两串的,佐料都撒不上去。”
我愣了一下,拿着单子不知所措。
那一瞬间,尴尬多过了食欲。
“那……算了。”
我放下手里的单子,转身走了。
既然老板看不上,也正好省钱了
![](17bdf4781bbe68f781129619a772026c.jpg)
Binary file not shown.

After

Width:  |  Height:  |  Size: 817 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

@@ -0,0 +1,63 @@
---
layout: post
title: 搓了一个仿朋友圈的Hugo主题
date: 2026-02-07T12:20:00.000Z
author: 小赵同学
categories:
- 折腾
tags:
- Hugo
- Artalk
- 开源
ai_comment: >-
静态博客实现动态交互确实是个有趣的挑战。利用Artalk作为外部数据层是个聪明的折中方案,类似将React组件状态托管给Redux。不过这种强依赖第三方服务的架构,后续可以考虑用Service
Worker+IndexedDB实现离线缓存,或通过GitHub Actions自动构建时预取评论数据。代码潦草是原型阶段的特权,建议用Hugo的Data
Templates将外部API数据固化到构建流程中。
slug: 20260207
---
上周,我的项目组所在的项目进入了项目维护阶段
果不其然,公司以资金无法周转的理由,进行了一波裁员
裁员那几天,坐在工位都是蒙的,凳子如同热炕,浑身不自在
可能是在项目上没怎么出过岔子,这次裁员名单没有我,我也是幸运的被留了下来
项目闲下来了,就想做点demo玩一下,上班的时候,突发奇想
想从0开始尝试模仿朋友圈的风格,搓一个Hugo主题,当然WordPress和typecho已经有很多作者实现
说干就干,从搭建hugo基础项目到第一行css,添砖加瓦
其实从写代码到部署还是踩了不少坑的,但是最后查文档或者问ai,还是解决了
## 一点碎碎念
其实功能上没什么太多新意,为了浏览顺畅一些,加上了 **Pjax** 实现无刷新跳转,也适配了 **昼夜模式**
做这个主题过程中,遇到的最大难题其实是 **静态博客的先天局限性** —— Hugo 生成的是静态 HTML,没有后端数据库,没法持久化存储数据。这就导致很多朋友圈该有的动态交互(比如点赞、评论互动)很难实现
所以,这个主题目前能实现 **文章点赞**、**评论点赞** 以及 **首页直接展示评论** 这些功能,是完全依赖了 **Artalk (v2.8.7)** 提供的 API
通过调用 Artalk 的接口,把评论区的数据取出来展示在首页,利用它的存储能力来实现点赞和交互
如果离开了 Artalk,基本上很多功能就做不了,而动态博客可以通过博客的接口去查询数据库,实现一些丰富的功能
目前代码写得还比较潦草,还有不少细节需要打磨。等我把逻辑理顺、Bug 修得差不多了,会整理开源到 GitHub
## 演示站点
演示站放在这,感兴趣的朋友可以看看:
[https://5b2.cn](https://5b2.cn)
## 演示图
![演示图](1.png)
![演示图](2.png)
![演示图](3.png)
写主题还是很累的,就这样,下机了!
Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

@@ -0,0 +1,163 @@
---
layout: post
title: 一个朋友圈风格 Hugo 主题
date: 2026-02-15T10:00:00.000Z
author: 小赵同学
categories:
- 折腾
tags:
- Hugo
- EdgeOne
- 开源
- GitHub Actions
ai_comment: >-
技术层面将社交动态复刻到静态博客的思路很巧妙,尤其通过Artalk
API实现点赞/评论动态化是个讨巧的解法。不过版本强绑定2.8.7确实存在维护风险,或许可以考虑加个API兼容层?另外,PJAX的实现是否考虑了回退兼容性,这对纯静态站点很关键。字体选择很有品味,但建议在文档中注明各字体的版权状态。
slug: 20260215
---
首先祝大家新年快乐,身体健康,阖家幸福,事业长虹!
在这个碎片化阅读的时代,我们习惯了在朋友圈分享生活的点滴,却又常常在各种社交平台的喧嚣中感到疲惫。
作为一名博主,我一直在想:能不能在静态的 Hugo 博客里,复刻出一份属于自己的“朋友圈”?
## 所谓“拙作”的由来
其实,仿朋友圈风格的主题在 Typecho 和 WordPress 社区早已有了非常优秀且成熟的先例。比如小胖脸大佬的 [Icefox](https://github.com/xiaopanglian/icefox),或是 [Kam](https://kam.space) 那款售价 499 元、精致到极致的商业主题。看着这些前辈的作品,我深知自己水平有限,代码写得比较稚嫩,很多逻辑甚至可以用邪修来形容。
但我还是想在 Hugo 这个“静态”的领域发起一点小小的挑战。主要动力源于一点:我没有长期稳定的服务器(说白了就是穷)
趁着过年这段时间,我把之前攒下的 Bug 勉强修了修,理顺了 README。虽然它还只是个“半成品”,但我还是决定把它开源出来。
作为作者,有人使用我的主题,我还是很开心的。虽然它不够完美,但我希望它能给喜欢折腾的朋友们带来一些乐趣。
## 🎨 那些勉强称得上“特性”的功能
在开发 **Amigo** 的过程中,我尽量在还原视觉体验和保持轻量之间寻找平衡。
既不花里胡哨,也不完全放弃一些小细节。以下是我觉得还算有点特色的几个功能:
### 1. 极致还原的“朋友圈”视觉
我不只是想做一个列表,而是想还原那份熟悉感。主题支持:
- **九宫格图片展示**:自动提取正文中的图片,并根据数量自动排列成 1-9 宫格。
- **地理位置标记**:通过 Front Matter 中的 `location` 字段,你可以在动态右下角标记你当时所在的位置,比如“武汉·东湖”。
- **毛玻璃效果 (Glassmorphism)**:在顶栏、友链卡片等处大量运用了毛玻璃特效,希望能增加一点现代感(老机子风扇可能风扇会狂转,不过这样说可能有点夸张,主题其实没有过多的毛玻璃特效)。
### 2. 追求丝滑的 PJAX 无刷新体验
为了让切换页面不那么生硬,我集成了 PJAX。这意味着你在点击文章、进入其他页面时,页面不会闪烁白屏。
虽然适配 pjax 踩了不少坑,但实现流畅的页面切换体验还是很有必要的。
### 3. 三模评论系统
考虑到不同人的需求,我内置了三种模式:
- **Artalk (强烈推荐)**:深度适配,支持首页直接显示点赞和评论。
- **Giscus**:基于 GitHub Discussions 的方案,适合程序员。
- **None**:如果你只想安静地记录,也可以选择关闭。
- **Twikoo**:暂时没有适配,老铁精力有限,后续如果有时间再看看能不能适配一下
### 4. 个性化字体支持
我内置了三种我觉得还不错的字体:`ZQL`、`PingFangQiaoMuTi` 和 `AlimamaFangYuanTi`。
你只需要在配置文件里改一个配置,即可瞬间切换字体。
## 🛠️ 核心命门:深度依赖 Artalk 2.8.7
这是我最想和大家交待的一点。由于 Hugo 本身不产生任何动态数据,所有的“朋友圈灵魂”——即**首页点赞显示**和**实时评论流**,全部都是通过 JavaScript 调用 **Artalk** 的 API 接口实现的。
如果你没有部署 Artalk,或者部署的版本过高/过低导致 API 变动,这个主题的功能会瞬间“塌陷”。我目前测试最稳定的版本是 **2.8.7**。
如果你想尝试,我建议使用官方的 Docker 镜像快速起步:
```bash
docker run -d \
--name artalk \
-p 3378:23366 \
-v $(pwd)/data:/data \
-e "TZ=Asia/Shanghai" \
-e "ATK_LOCALE=zh-CN" \
artalk/artalk-go:2.8.7
```
## 📝 撰写动态的小贴士 (避坑指南)
为了让主题能够正确解析图片,我建议采用 **Page Bundles** 模式。
1. **目录结构**:
建议在 `content/posts/` 目录下为每篇动态创建一个独立的文件夹,里面放一个 `index.md` 和配套的图片。
2. **图片插入**:
直接在 Markdown 里使用 `![描述](./image.jpg)` 语法即可。主题会自动抓取这些图片并生成九宫格预览,同时在详情页里也会做排版优化。
3. **“全文/收起”逻辑**:
参考了微信的设计,首页动态如果文字超过 6 行,会自动折叠并显示“全文”按钮。点击“收起”时,页面会自动平滑回滚到动态顶部,防止迷失视口。
## 🚀 顺带聊聊 EdgeOne 国际版部署
最近我把博客从 Vercel 迁到了 **EdgeOne 国际版**(EdgeOne International),体验确实很香。
如果你也想通过 GitHub Actions 实现“推送即发布”,可以参考我的工作流配置。在 Secrets 中添加 `EDGEONE_API_TOKEN` 后,创建一个 `.github/workflows/deploy.yml`:
```yaml
name: Deploy to EdgeOne
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Build Hugo
run: |
# 建议使用 Hugo 0.128.2 保证兼容性
hugo --gc --minify
- name: Deploy
env:
EDGEONE_API_TOKEN: ${{ secrets.EDGEONE_API_TOKEN }}
run: |
# 关键!如果是国际版,必须带上 --area overseas
npx edgeone pages deploy ./public -n your-site-name -t $EDGEONE_API_TOKEN --area overseas
```
**特别提醒**:EdgeOne 国内版和国际版是完全隔离的。如果你用的是国际版却没加 `--area overseas`,CLI 会报 `Project not found` 错误,这个坑我替大家踩过了。
## 📦 仓库与致谢
代码水平虽然一般,但开源的心是真的。
👉 **仓库地址**:[https://github.com/zqlit/Hugo-Theme-Amigo](https://github.com/zqlit/Hugo-Theme-Amigo)
如果你觉得还行,麻烦点个 **Star** 支持一下,这将是我继续维护(修 Bug)的最大动力。
最后,要特别感谢以下项目和博友的启发与支持:
- [Hugo 官方](https://gohugo.io)
- [Artalk](https://artalk.js.org/)
- [ViewImage.js](https://github.com/tokimeitoshite/ViewImage)
- [EdgeOne](https://www.edgeone.com/)
- [Icefox](https://github.com/xiaopanglian/icefox)
- 以及所有在开发期间提供建议的朋友们。
如果在使用过程中遇到了困难,或者有更好的改进建议,非常欢迎在 GitHub 提 Issue 指教。
![c974daedc1de4aed8c7053d0cc0d71a5.webp](c974daedc1de4aed8c7053d0cc0d71a5.webp)
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

@@ -0,0 +1,43 @@
---
title: EdgeOne Pages 部署 Twikoo 评论
date: '2026-02-21T17:46:10+08:00'
slug: '20260221'
draft: false
categories:
- 技术
tags: []
author: 小赵
layout: post
ai_comment: >-
用EdgeOne
Pages部署Twikoo确实是个聪明的选择,KV存储的简洁性让我想起SQLite的设计哲学——精简但够用。测试速度优于giscus可能源于边缘计算的近距离优势。不过建议补充说明KV存储的读写配额限制,这对高流量站点可能是隐形瓶颈。键值备份虽然方便,但缺乏事务性保障,数据一致性需要开发者自己兜底。
---
最近几天空闲的时候,尝试在适配 Twikoo 评论系统,在GitHub主页偶然刷到一个项目
https://github.com/Mintimate/twikoo-eo
Twikoo EdgeOne Pages 版本 , 可以将 Twikoo 部署在 EdgeOne Pages,通过 EdgeOne Pages 的KV服务作为数据持久化存储信息
感觉很香,创建 EdgeOne Pages 账号很简单,这里不再赘述
## 使用教程
直接fork https://github.com/Mintimate/twikoo-eo 项目到自己仓库
然后登入自己的EdgeOne Pages 账号,这里配置一下环境变量
`CORS_ALLOW_ORIGIN` 这个配置类似于Artalk的可信域名,格式:example.com,blog.example.com(多个域名用逗号分隔)
如果不设置则允许所有域名访问,这里也可以不配置,可以根据自己需要进行配置
创建KV存储之后,需要关联已经部署好的EdgeOne Pages项目
![bb7f17109f754b24ba64805ebb667ca1.png](bb7f17109f754b24ba64805ebb667ca1.png)
然后就结束了,在前端填写你绑定好的域名,实际测试下来,速度很不错,体验感比giscus评论体验感好上不少
KV存储的方式是以键值对的形式存储,也很好备份,只需copy键对应的值即可
![fc8040b47a14463ca50e875e67c80ef5.png](fc8040b47a14463ca50e875e67c80ef5.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

@@ -0,0 +1,39 @@
---
title: 还是弃用了使用多年的全拼输入法
slug: '202603011634'
author: 小赵
date: '2026-03-01T16:35:58+08:00'
draft: false
ai_comment: >-
从全拼切换到双拼确实是个效率提升的好选择。小鹤方案将韵母映射到固定键位的设计很巧妙,本质上是用空间换时间。不过双拼的"死板"问题其实反映了输入法智能联想和用户习惯的耦合度,建议可以研究下双拼方案的自定义词库功能。另外,双拼+形码的"音形输入"可能是更优解,能在减少击键同时保持较高首字准确率。
---
我用了很多年的全拼输入法,他很简单,很容易上手,毕竟我们从小学就开始学习汉语拼音,但是它并不完美,比如打 `双` 这个字的时候,我需要敲击按键六次,s-h-u-a-n-g ,日常工作基本上都是打螺丝的活,则需要大量的敲击键盘,我一直在找其他方案来替代全拼输入。
有时我在想,能否学习一门新的输入法,来增加我的打字速度呢,于是我把目标投向五笔输入法,实际学了几天,发现这个玩意,学习成本极高,从全拼输入转到五笔输入,则是由音码转为形码,对于小白来说,需要打一个字,我需要先想一想,我想打的字该怎么拆,则多了一个思考的过程,其次就是需要记忆字根,不过前辈已经提供了记忆字根的口诀,可以快速的记忆五笔字根,但是在短时间掌握还是有难度的
## 初识双拼
双拼是我在逛b站的时候,一个评论区的小伙伴分享的,他说:双拼的速度比全拼的速度快很多,只需要敲击两个键位,汉字就出来了,我抱着好奇搜了一下,于是开始学习,up主讲的很详细,先讲原理,然后教记忆韵母键位,差不多三个小时就能记忆个七七八八,慢慢的打,就可以打出很多字了
我的双拼方案是小鹤双拼方案,也是大众推荐的双拼方案,双拼顾名思义,就是敲击两个键位,第一个键位敲击声母,紧接着敲击韵母,即可打出来
例如打 双拼 这两个字,就是 ulpb 即可键出 双拼,比传统全拼可以少敲击很多次,从而减少按键的输入
输入法我每天都要用,通过双拼方案,我可以大大减少敲击键盘次数,累计起来对打字速度的提升也是巨大的
![7fa64274e3954b3f9096d2c0db7503e2.png](7fa64274e3954b3f9096d2c0db7503e2.png)
## 双拼的缺点
因为我使用的双拼输入时间并不长,我目前发现一个缺点就是,全拼两个字母能自动联想的词语,双拼只能死板地当做 声母 + 韵母,最典型的就是 哈哈,全拼的时候,输入法记忆之后,我只需要打 hh,即可敲击哈哈,如果双拼方案则需要依然死板的敲击haha,才可键入 哈哈
如果你对双拼有兴趣,可以参考以下视频
https://www.bilibili.com/video/BV1bv4y1P7Hs/?spm_id_from=333.1391.0.0&vd_source=03f609196865dbb1517e87152cab7791
小鹤双拼官网
https://flypy.cc/help/#/up
当然,我的这篇文章,全部使用双拼输入,感兴趣的朋友可以试一试
Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

@@ -0,0 +1,15 @@
---
title: 临走前一份襄阳牛肉面
slug: '202603011133'
author: 小赵
date: '2026-03-01T11:40:12+08:00'
draft: false
ai_comment: >-
Github Action 的额度问题确实是个痛点,尤其对高频 CI/CD 需求的项目。考虑自托管 runner
可能是更经济的长期方案,虽然需要额外维护成本。这张配图意外成了技术债务的绝佳隐喻——系统限制往往在关键时刻卡住流程。建议下次提前监控额度使用率,像对待服务器磁盘空间一样设置预警阈值。
---
其实很早已经来上班了,但是 Github Action 额度已经满了,文章发不出去,笑死😂
![7b00b82d52254ce3841cb93f2a9c6596.jpg](7b00b82d52254ce3841cb93f2a9c6596.jpg)
Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

@@ -0,0 +1,92 @@
---
title: 我的5G路由器方案:中兴F50+cudy tr3000 256MB
slug: '202603111244'
author: 小赵同学
date: '2026-03-12T12:45:02+08:00'
draft: false
categories:
- 生活
tags:
- 5G随身WiFi
- cudy tr3000 路由器
ai_comment: >-
这套分体式方案确实是个理性选择,从架构上看分离了基带处理和射频分发,类似云计算中计算与存储分离的设计思路。F50的发热问题印证了香农极限定律——高频信号处理必然伴随能耗提升。建议后续可以测试不同负载下的设备温度曲线,这对理解5G
CPE的功耗模型会很有帮助。180元淘到刷好OpenWrt的TR3000算是捡漏了,这价格连树莓派都买不到。
---
## 一点碎碎念 ##
先说说目前的情况,我现在住的出租屋一直是没有牵网线的,之前还和朋友合计着要不要一起牵个宽带,但是算来算去觉得成本还是有点高,于是这个计划就不了了之了。
之前电脑上网主要是通过手机热点来解决,我的流量卡是广电的350G流量卡,每个月还可以结转套餐的120G基本流量,所以每个月月初的时候大概有420G左右的流量,说实话个人用其实完全够用了,刷视频、打游戏、下载东西都绰绰有余。
![](1.jpg)
让我苦恼的是,手机长时间开热点,电量流的跟水一样,其次就是手机后台应用开得比较多的话,手机就会异常发热,冬天还好,夏天那就有点夸张了。
## 5G CPE ##
既然不打算装宽带,那我的选择就只有5G CPE了。这里先给不太了解的朋友解释一下,什么是5G CPE?
网上专业解释是这样的:
> 5G CPE(Customer Premises Equipment)是一种用户终端设备,它可以接收5G信号,并将其转换为Wi-Fi信号或有线网络信号,供多个设备同时使用。
说白话一点,就是把5G信号变成Wi-Fi,让你的手机、电脑、平板都能上网,相当于一个不用插网线的路由器,就是一个能插卡的随身WiFi。
## 方案选择 ##
我在狗东和bilibili搜索了很多CPE,比如比较出名的烽火,口碑蛮不错的,但是价格偏高
选了一圈,最后还是直接选择了酷安上比较火的方案:**中兴F50 + Cudy TR3000 256MB**方案,也是直接抄作业!
![](IMG_20260311_124816.webp)
为什么选256MB版本呢?其实还有128MB的版本,但是考虑到以后可能要刷OpenWrt玩一下,内存大点还是有好处的,差不了多少钱,一步到位比较好。
## 为什么选择这种组合方案? ##
可能有人会问,为什么不直接买一台一体机5G CPE?比如那种自带Wi-Fi功能的5G CPE,不是更方便吗?这里我说说我的几个考虑:
- **第一个是灵活性更高** : 分开买的话,5G CPE和Wi-Fi路由器是分开的,这样以后如果想换个更好的Wi-Fi路由器,或者换个更好的5G CPE,都很方便,不需要重新买一整套设备了,而一体机的话,如果想升级其中一个功能,就只能换整台了,成本就高了很多。
- **第二个是成本更低** : 分开买确实比买高端一体机便宜很多,我这一套下来也就五百多块钱,要是买个好点的一体机5G CPE,可能得上千了,对于我这种预算有限的租房党来说,分开买的性价比反而更高一些。
- **第三个是性能更好** :F50我实际单独使用了几天,发现这玩意散热真的不行,如果发射WiFi型号,链接的设备多了,就会莫名奇妙的发烫,如果外接cudy tr3000,F50就只负责5G信号转换,不需要处理Wi-Fi的发射,发热问题就能得到很好的控制,性能也更稳定。
- **第四个是可玩性高** :Cudy TR3000可以刷OpenWrt,我目前刷入了immortalwrt,科学、去广告,插件很丰富、这里强烈推荐 https://github.com/kanoqwq/UFI-TOOLS,感谢kanoqwq大佬的工具,F50刷完高级后台可玩性很高,里面内置很多插件,甚至可以在路由器上养虾
![](image.png)
![](image-1.png)
## 购买价格 ##
事实上在购买阶段,我发现现在内存和硬盘涨价都挺厉害的,导致中兴F50 + Cudy TR3000 256MB的价格也有小幅增长,这里我盘算了一下购机价格,给大家一个参考。
- **中兴F50**:这个是在拼多多上买的全新的,移动大存储版,自带256GB存储,花了399块钱,这个价格我觉得还算合理,毕竟是全新的,带保修的,价格也不算太离谱
- **Cudy TR3000 256MB**:我是在闲鱼上收的二手,花了180块钱,卖家很好,帮忙刷了immortalwrt,直接就能用,价格也很实在,现在内存价格涨得厉害,256MB的版本二手价也涨了不少,这个价格我觉得很划算了
- **总计**:399 + 180 = 579块钱
说实话这个价格我还是能接受的,各位如果想买的话,可以多看看拼多多、京东、淘宝等平台,价格可能会有一些波动,建议多比较一下,找到性价比最高的购买渠道。
## 使用体验 ##
到现在为止,这套设备我已经用了一段时间了,说说我的真实使用感受吧。
![](IMG_20260306_185315.webp)
- **首先说说速度** :在我住的地方,广电5G信号还不错,测速的话下行能到300-400Mbps,上行50-80Mbps,这个速度日常使用完全够用了,刷4K视频一点都不卡,下载东西也很快。当然速度这个东西还是要看当地的5G信号覆盖情况,如果信号不好的话速度肯定会慢一些。
- **再说说稳定性** :这一点我还是挺满意的,连续开机几周了,没出现过断流情况,很稳定,打游戏也不会突然掉线。之前用手机热点的时候,有时候会突然断一下,虽然很快就恢复了,但还是挺烦人的,至少现在没有遇到过。
- **发热情况** :发热方面也还行,F50摸起来是温热的,不烫手,TR3000的温度更低,几乎感觉不到什么热量,都在可接受范围内。夏天的话可能温度会高一些,但应该也没问题,到时候怼个风扇散热就好了。
## 尾声 ##
目前使用了一段时间,中兴F50 + Cudy TR3000这套组合对于租房、临时办公或者懒得拉宽带的朋友来说,确实是一个不错的选择,性价比也挺高的。只要你手里有充足的流量卡,或者能搞到便宜的流量卡,这个方案完全可以替代传统宽带,用起来基本没什么差别。
如果当地5G信号不太好,或者对网络质量要求特别高,那还是装宽带比较稳妥,毕竟有线网络摆在那儿就是更稳定。不过要真的在琢磨5G上网方案的话,可以考虑参考一下这个组合。而且5G方案还有个巨大的优势——搬家很方便,不用折腾注销宽带、迁移宽带那些麻烦事儿,直接把设备装进包里就走了,主要是他不大,到新住处直接插电就能用。
![](20251231-223456-80d08f.webp)
Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

@@ -0,0 +1,64 @@
---
title: Claude Code 启动脚本
date: '2026-03-21T20:48:15+08:00'
slug: '202603212047'
draft: false
categories:
- 技术
tags: []
author: 小赵
layout: post
ai_comment: >-
从trae转向Claude
Code+火山引擎的决策很务实,token计费确实容易造成隐性成本。CLI工具链的设计思路值得借鉴,特别是cc-switch的抽象层处理,把模型配置变成了可插拔模块。一键启动脚本解决了CLI的核心痛点——路径依赖,但可以考虑用环境变量进一步简化部署流程。国内模型在工具链整合上的灵活度反而成了优势,这倒是挺有意思的观察角度。
---
之前一直使用 trae 国际版作为我的 AI 编程解决方案,还开通了几个月的 Pro 版本,体验一直不错。但自从 trae 官方将计费模式从按次数计费改为按 token 计费后,使用成本有所上升,我就很少再用了。
目前我主力使用的组合是 Claude Code + 火山引擎 Coding Plan。相较于国外的主流模型,国内模型的编程能力确实稍逊一筹,但对于非开发岗位的我来说,完全能满足日常工作需求,毕竟我的核心需求不是复杂的代码开发,简单的代码辅助、问题排查已经足够用了。
选择 Claude Code 最核心的原因,在于它是基于 CLI(命令行界面)开发的工具,很灵活。它无需下载额外客户端软件,只要在任意地方打开终端,cd 到指定工作文件夹,输入`claude` 命令,就能快速进入 AI 编程面板,随时随地开展工作。而且它也是支持自行接入自定义模型厂商,可根据需求灵活切换,比如我就是用的国内大模型的plan,便宜够用。
## 安装 Claude Code
安装过程比较简单,就是几行命令,前提是保证本机已安装 Node 环境(Node 环境的安装方法这里就不再赘述)。
```bash
# 安装全局依赖
npm i -g @anthropic-ai/claude-code@latest
# 运行 Claude Code
claude
```
执行完上述命令,我们还需要安装一下模型切换工具,这里比较推荐 cc switch 可以管理多个模型,支持一键切换
## 安装 CC Switch
`https://github.com/farion1231/cc-switch/releases/tag/v3.12.2`
如果你的设备是 Windows 系统,直接选择对应版本安装即可(其他系统可参考官方适配版本)。
CC Switch 是一款实用的辅助工具,核心作用就是帮我们统一管理 Claude Code 的模型配置,无需手动修改配置文件,通过图形界面点击操作,就能轻松切换自定义模型厂商,很方便。
![a7004594089f4a0ab55a07fafdb65247.png](a7004594089f4a0ab55a07fafdb65247.png)
## 安装 一键启动 Claude 脚本
我个人不太习惯使用 VS Code 内置的 Claude 插件,一直坚持用 Claude Code 的 CLI 模式。但工作中经常需要同时打开多个终端,每次都要手动 cd 到指定工作文件夹,再输入命令启动,步骤繁琐且耗时,久而久之就觉得有些麻烦。
今天抽了点时间,让 AI 帮忙实现了一个 Windows 专用的 Claude Code 一键启动脚本,彻底解决了这个痛点。
脚本源码已放在 GitHub 上,有需要的小伙伴可以自行前往自取:[https://github.com/zqlit/add-menu-claude-code](https://github.com/zqlit/add-menu-claude-code)
将文件夹放到指定自定义位置,右键管理员运行 add-menu.cmd 即可
![a354b43dd956483d8ffae998bee1cc19.png](a354b43dd956483d8ffae998bee1cc19.png)
安装脚本后,只需在任意文件夹空白处右键,选择「Claude Code Here」
就能一键启动终端并进入 Claude Code 面板,省去了手动切换目录、输入命令的步骤,方便不少。
![220b0ea78d544a499c9ed2595c48af41.png](220b0ea78d544a499c9ed2595c48af41.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

@@ -0,0 +1,272 @@
---
title: 我的静态博客动态化方案
date: '2026-05-20T00:00:00.000Z'
slug: '202605202304'
draft: false
categories:
- 技术
tags: []
author: 小赵
layout: post
ai_comment: >-
从技术选型看,Next.js+Tailwind确实能提供流畅的本地写作体验,既保留静态博客的简洁性,又弥补了动态编辑的便利。GitHub
API的速率限制问题很典型,你们的本地化方案是个优雅解法——直接读写Markdown文件既规避了网络依赖,又符合Hugo哲学。回收站设计体现工程思维,建议可扩展为自动备份到对象存储,兼顾容灾与serverless原则。
---
## 碎碎念
前段时间,DeepSeek官方宣布发布了V4版本。在当下AI浪潮如火如荼的背景下,这倒也不算意外。我一直在用的Vibe coding方案是火山引擎的coding plan,主要因为用习惯了,所以一直续费着。但说实话,豆包的编码能力确实一般,一个需求往往要反复调整好几遍,有时候真的恨铁不成钢。
## 友链
前几天五一假期,我逛了逛博友的网站,发现不少人的站点已经打不开了。为了避免跳转到黄色链接,我在这里做了隐藏处理,如果站点恢复,请在留言区告知即可。
## 发现
前几天在浏览博友博客时,我发现了几个很有意思的文章,核心思路都是用静态博客实现一个后台,主要分为以下三个方案:
小十的博客 https://www.xiaoten.com/posts/2026/03/ai-build-hugo-cms——用AI为Hugo打造了博客文章发布平台;
东风博主的博客 https://easte.cc/posts/feishu-sync-bot-deploy ——飞书同步机器人,利用飞书提供的接口,将飞书文档同步到博客中,这个想法非常有趣;
小饿博主的博客 https://dongjunke.cn/posts/2026/05/05/50369.html ——我的独立博客,终于拥有了一个像样的后台。
经朋友推荐,我尝试使用了DeepSeek V4 Pro。DeepSeek的收费模式跟其他家有点不一样,主要是通过充值余额,直接消耗余额来使用,没有其他家所谓的coding plan。对于我这种使用频率较低的用户来说,还是很不错的。
我首先尝试了东风博主的思路,对接飞书,期间遇到了几个极其纠缠的问题,始终没能解决,只好放弃。接着试了小十的思路,通过接入GitHub API实现在线编辑博客,于是就有了我后台在线编辑器的第一版。
![第一版后台管理系统](image.png)
DeepSeek V4 Pro的表现还算不错,我提出的需求,基本一次两次就能实现我想要的效果。
但很快我又遇到一个新问题:这个方法非常依赖GitHub的API,频繁请求时会出现拒绝请求的情况。我暂时没有一个好的方法解决,我尝试通过edgeone的kv存储,缓存一些数据,从而减轻GitHub API的访问压力,但是我发现我大部分的时间都消耗在跟第三方平台进行对接上了。
其次就是GitHub在国内的访问速度一般,电信网络如果不接入代理,基本很难顺利访问,只有在代理环境下才能有较好的体验。
GitHub的API其中也有几个坑,不断搜资料也没找到我满意的解决方法。
一度冲动,甚至想直接切换到Typecho或WordPress。
不得不说动态博客真的舒服,可以省了好多事。
后面转变思路,于是,我开始尝试小饿博主的方案——通过Next.js实现一个本地写作平台。
这样就能从根源上减轻GitHub API的依赖了。
## 技术选型
关于技术选型,我保持跟小饿博主一致,框架选了Next.js 16,样式用Tailwind CSS 4。
配色参考了森同学的主题配色,暖白纸色背景,深蓝文字,整体偏书卷气,看着不累。
森同学博客:https://oxxx.cn
数据存储方面,坚持serverless原则,我坚持不用数据库。
直接读写Hugo的Markdown文件,跟Hugo本身的哲学一致:简单、直接、可控。
整个项目放在博客根目录下的`/write`文件夹里,跟Hugo本身井水不犯河水。
端口映射到8016,就本地,也不打算映射到公网,不然又要去折腾那该死的GitHub API了。
## 文章管理
这是最核心的模块。打开后台首页,所有文章以卡片列表展示,标题、日期、分类、发布状态一目了然。支持搜索和分页,每页20条。
![](image-1.png)
点击标题直接进编辑页,不用到处找编辑按钮。hover会浮出预览、编辑、删除三个操作。
![](image-2.png)
预览会自动启动Hugo dev server(如果没自己手动执行的话),在新标签页打开`localhost:1313`,也可以点击右上角的眼睛。
![](image-3.png)
删除不会真删,进了回收站,保留30天,误删了还能捞回来。
这个主要是参考了小饿博主的idea。
![](image-4.png)
## 撰写体验
新建文章时,Slug自动生成`YYYYMMDDHHmm`格式的数字时间戳,跟我之前文章的命名风格保持一致。
文件夹名则是`日期-中文标题`,比如`2026-05-20-我的静态博客动态化方案`,找起来方便。
编辑器右侧挂了两个面板:上面是快捷插入(H2、H3、代码块、引用、图片等),下面是AI助手。
AI浪潮怎能不加点AI元素,选中文字可以润色、扩写、缩写、纠错;填了标题能自动生成摘要和标签。
![](image-5.png)
AI接的是DeepSeek的API,OpenAI兼容格式,也可以切Anthropic格式。
图片上传到文章同级文件夹里,跟`index.md`放在一起,Markdown直接引用文件名就行。
不像以前要手动拖文件、改路径。
发布流程分了三个按钮:草稿(`draft:true`,前台不可见)、保存(`draft:false`,存本地)、发布(保存+自动执行`git add -A && git commit && git push`)。
不用再切终端敲命令了,真正解放双手。
还有个贴心的小功能:每5秒自动缓存到浏览器localStorage。
万一电脑崩了或者不小心关了标签页,重新打开会自动恢复,不会丢稿。
## 评论管理
我的博客用的是Artalk评论系统。后台对接了Artalk的API v2,管理员登录一次后token和凭据缓存在本地,下次自动登录。
![](image-6.png)
界面是左右分栏:左边文章列表,按MD文件日期倒序排列,最新的在最上面。
点开一篇文章,右侧按需加载评论,不会像第一版那样全量拉取6000条导致卡死。
支持在线回复、隐藏、删除评论。
回复时会显示被回复者的昵称和内容摘要,知道上下文。
## 订阅源和友链
这两个模块,我都一定程度实现了动态化,都对接了我自己搭建的API。
订阅源管理可以增删RSS源,支持批量检测存活状态。
![](image-7.png)
友链管理更完整一些:增删改查、隐藏/显示切换、单条或批量存活检测。
![](image-8.png)
检测走的是腾讯云SCF代理,因为有些博客在境外,直连可能不通。
友链列表里每个站点前面有个WiFi图标,绿色表示在线,红色表示失联。
这样我能及时发现哪些博友的网站打不开了,该隐藏就隐藏,保持友链页面的健康度。
![](image-9.png)
## 微信公众号同步
最近我也开始尝试写公众号。我在自己的接口站新增了微信公众号素材接口,可以把文章同步到公众号草稿箱。
![](image-10.png)
流程很简单:后台里选一篇文章,填个封面图URL,点「同步到微信草稿」就行了。
Markdown会自动转成微信兼容的HTML——标题有深蓝底边线、代码块是Dracula深色主题、引用有蓝色左边框、底部有个居中的「—END—」收尾。
在公众号后台的草稿箱里预览一下就能直接群发,不用再排版一遍,这样可以保证博客和公众号的同步更新。
## 图片管理
单独做了一个图片管理页面,把所有文章文件夹里的图片扫出来,网格预览,能看到每张图的文件大小和所属目录。
一键复制Markdown引用地址,多余的图片可以直接删。
以前图片散落在几十个文件夹里,根本不知道哪些在用哪些没用了。现在一目了然。
同时也可以当做一个回忆相册,哈哈,不是吗?
![](image-11.webp)
## 开源提示词
因为本系统是基于自己的主题深度定制的,不适合开源给大家。
这里把Vibe coding的技术选型开源出来,方便大家自己Vibe coding自己的后台系统。
```txt
# 写作后台
Hugo 博客的本地管理后台,Next.js 16 + Tailwind CSS 4,运行在 `localhost:8016`。
## 核心模块
### 1. 文章管理 `/`
- 读取 Hugo `content/post/` 下的 Markdown 文件,列表展示、搜索、分页
- 点击标题进入编辑,hover 显示预览、编辑、删除按钮
- 预览自动启动 `hugo serve`(kill 端口冲突后重启)
### 2. 撰写/编辑 `/edit`
- 新建文章自动生成 `YYYYMMDDHHmm` 格式 slug,文件夹命名 `YYYY-MM-DD-slug`
- 文章元信息编辑(标题、日期、分类、标签、作者、AI 评论)
- Markdown 正文编辑,支持插入图片(上传到文章同级文件夹)
- 三个发布状态:
- **草稿** — `draft: true`,前台不可见
- **保存** — `draft: false`,存本地不推送
- **发布** — 保存 + `git add -A && git commit && git push`
### 3. 评论管理 `/comments`
- Artalk 管理员登录(邮箱+密码),token 缓存 localStorage
- 文章列表按 MD 文件日期倒序,默认选中最新文章,按需加载评论
- 支持在线回复、隐藏/显示、删除评论
- 回复评论时显示父级评论引用
### 4. 订阅源管理 `/feeds`
- 对接 api,token 存储在 `.env` 服务端
- 增删 RSS 订阅源,批量检测存活状态
### 5. 友链管理 `/links`
- 对接 api,token 存储在 `.env` 服务端
- 增删改友链,支持隐藏/显示切换
- 单个/批量存活检测,失联标记
### 6. 回收站 `/recycle`
- 软删除文章移至 `.recycle/`,保留 30 天
- 支持恢复和永久删除
## API 路由
| 路径 | 方法 | 说明 |
|------|------|------|
| `/api/posts` | GET/POST | 文章列表、新建 |
| `/api/posts/[slug]` | GET/PUT/DELETE | 单篇文章 CRUD |
| `/api/comments` | GET/POST | 评论列表、回复 |
| `/api/comments/[id]` | PUT/DELETE | 评论操作 |
| `/api/comments/login` | POST | Artalk 登录 |
| `/api/rss/feeds` | GET/POST/DELETE | 订阅源管理 |
| `/api/rss/links` | GET/POST/PUT/DELETE | 友链管理 |
| `/api/recycle` | GET/POST/DELETE | 回收站 |
| `/api/upload` | POST | 图片上传 |
| `/api/deploy` | POST | Git 部署 |
| `/api/hugo` | GET | Hugo 启动/检测 |
## 技术栈
- **框架**: Next.js 16 (App Router, Turbopack)
- **样式**: Tailwind CSS 4,SEN 主题暖色系
- **数据**: 直接读写 Hugo Markdown 文件(gray-matter),无数据库
- **评论**: Artalk API v2
- **RSS/友链**: 自建 API + SCF 代理
- **部署**: 执行 shell git push
## 提示词指南
### 新增页面/模块
> "在写作后台新增一个 XXX 模块,左侧侧边栏加入口,页面风格与现有文章管理一致"
### 修改现有功能
> "优化文章管理页面的 XXX 功能,保持 SEN 配色和卡片式布局"
> "修复评论管理页面的 XXX bug,不要改动其他功能"
### 样式调整
> "把 XXX 页面的色调整体改为 SEN 暖色系(bg: #fcfaf5, text: #223d66, accent: #1d3d6c, line: #e4dfd2)"
### API 对接
> "对接 api.usj.cc 的 XXX 接口,token 在 .env 的 RSS_API_TOKEN,后端代理不要暴露到前端"
### 常见问题描述
- "布局太窄了" → 去掉 max-w 限制
- "左右比例不对" → sidebar w-1/4,main flex-1
- "最新数据没排前面" → sort by date desc
- "不知道点哪里" → 标题做成可点击,加明显按钮
```
希望来访的朋友,可以关注一下我的公众号,感谢!
![](image-13.png)
@@ -0,0 +1,173 @@
---
title: Ubuntu 24.04 安装 Zabbix 8.0 全记录
date: '2026-05-22T00:00:00.000Z'
slug: '202605221646'
draft: false
categories:
- 笔记
tags: []
author: 小赵
layout: post
ai_comment: >-
MySQL 8.0的binlog和权限管理确实严格了不少。建议在导入前临时关闭binlog(SET
SQL_LOG_BIN=0)或者给zabbix用户加SUPER权限。另外AllowUnsupportedDBVersions=1这个参数很关键,Zabbix
8.0对MySQL版本校验很严格,遇到类似问题的同仁可以直接参考这条解决方案。路径变更问题也提醒我们,版本升级时文档结构可能调整,find命令比记忆路径更可靠。
---
## 环境说明
新到了一台装了 Ubuntu 24.04 的机器,需要搭个 Zabbix 8.0 。用的 MySQL 8.0.45 做数据库,整个过程踩了不少坑,记录一下方便以后翻看。
---
## 安装流程
### 添加官方源
Ubuntu 自带源里的 Zabbix 版本太老,直接去官方拉最新的仓库包:
```bash
wget https://repo.zabbix.com/zabbix/8.0/release/ubuntu/pool/main/z/zabbix-release/zabbix-release_latest_8.0+ubuntu24.04_all.deb
sudo dpkg -i zabbix-release_latest_8.0+ubuntu24.04_all.deb
sudo apt update
```
### 安装组件
一条命令把服务端、前端、数据库、Agent 全装上:
```bash
sudo apt install -y zabbix-server-mysql zabbix-frontend-php zabbix-apache-conf zabbix-sql-scripts zabbix-agent mysql-server
```
### 配置数据库
先登进 MySQL:
```bash
sudo mysql -u root
```
建库、建用户、授权,三步走:
```sql
CREATE DATABASE zabbix CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
CREATE USER 'zabbix'@'localhost' IDENTIFIED BY '你的密码';
GRANT ALL PRIVILEGES ON zabbix.* TO 'zabbix'@'localhost';
FLUSH PRIVILEGES;
QUIT;
```
### 导入初始数据
```bash
sudo zcat /usr/share/zabbix/sql-scripts/mysql/server.sql.gz | mysql -uzabbix -p zabbix
```
### 改配置文件
编辑 `/etc/zabbix/zabbix_server.conf`,加上这两行:
```
DBPassword=你的密码
AllowUnsupportedDBVersions=1
```
第二行后面会讲到为什么需要,第二行配置文件搞了我很久。
### 修复语言环境
```bash
sudo apt install -y locales
sudo locale-gen en_US.UTF-8
```
### 启动服务
```bash
sudo systemctl restart zabbix-server zabbix-agent apache2
sudo systemctl enable zabbix-server zabbix-agent apache2
```
### 网页初始化
浏览器打开 `http://你的服务器IP/zabbix`,跟着向导填:
- 数据库类型选 MySQL
- 主机填 localhost
- 库名 zabbix,用户名 zabbix,密码填你设的那个
- 时区选 Asia/Shanghai
完成后用 `Admin / zabbix` 登录就行。
---
## 踩过的坑
### 坑一:数据文件路径不对
照着网上教程写的路径 `/usr/share/zabbix-sql-scripts/mysql/server.sql.gz`,结果报文件不存在。用 `find /usr/share -name "server.sql.gz"` 搜了一下才发现 8.0 版本路径变了,实际在 `/usr/share/zabbix/sql-scripts/mysql/server.sql.gz`,中间少了一层目录。
### 坑二:数据库权限报错
导入数据时报这个:
```
ERROR 1419 (HY000): You do not have the SUPER privilege and binary logging is enabled...
```
MySQL 开了 binlog,zabbix 用户没有 SUPER 权限,没法创建存储过程。先登录 root 执行一下 `SET GLOBAL log_bin_trust_function_creators = 1;` 临时放开限制,再重新导入就好了。
### 坑三:配置文件变量名大小写
用 `grep "DBpassword"` 查配置啥也没找到,折腾半天发现是大小写问题,变量名是 `DBPassword`(P 大写)。Linux 下这种大小写敏感的地方挺多的,习惯了就好。
### 坑四:MySQL 版本不被支持
服务死活起不来,日志里写着:
```
Unable to start Zabbix server due to unsupported MySQL database version (8.00.45).
Must be at least (8.04.00).
```
Ubuntu 24.04 自带的 MySQL 8.0.45 版本太新,Zabbix 8.0 的白名单里还没有它。其实数据库完全兼容,只是校验逻辑没跟上。解决方法是配置文件里加一行 `AllowUnsupportedDBVersions=1`,跳过版本检查就好。当然这行得自己承担风险,不过单机用完全没问题。
### 坑五:语言环境检测不通过
网页安装那步卡在系统语言检查,`en_US` 那项是红色的。因为系统缺了英文语言包,装上 `locales` 并生成 `en_US.UTF-8`,重启 Apache 和 Zabbix Server 就过了。
### 坑六:数据没完全导进去
前面权限问题导致导入中断,虽然部分表已经建了但 `users` 表是空的,启动时直接报:
```
cannot use database "zabbix": its "users" table is empty
```
干脆删库重建,重新完整导入一遍,最后 `SHOW TABLES` 出来 211 张表就对了。
---
## 顺手学到的几个知识点
- **`mysql -p` 跟密码**时中间不能有空格,建议直接回车然后交互式输入,更安全
- **`sed -i`** 是原地改文件,加 `.bak` 后缀可以自动备份原文件
- **`sed` 分隔符不一定是 `/`**,遇到 URL 这种含斜杠的内容时用 `s#原内容#新内容#` 更方便
- **`AllowUnsupportedDBVersions=1`** 可以跳过数据库版本校验,适合用较新数据库版本的情况
- **Zabbix 前端需要 `en_US.UTF-8` 语言包**,最小化安装的系统可能要手动装
---
## 验证是否跑起来了
```bash
# 三个服务都看看状态
sudo systemctl status zabbix-server zabbix-agent apache2
# 翻翻日志有没有报错
sudo tail -20 /var/log/zabbix/zabbix_server.log
```
然后浏览器访问页面,用默认账号登进去,去"数据采集 -> 主机"里瞅一眼
本机那个 ZBX 图标变绿就说明整套东西跑通了。
Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

@@ -0,0 +1,133 @@
---
title: OpenWrt 路由器改纯 AP 模式记录(Cudy TR3000 + 中兴 F50)
date: '2026-05-23T00:00:00.000Z'
slug: '202605231938'
draft: false
categories:
- 笔记
tags: []
author: 小赵
layout: post
ai_comment: >-
改AP模式的思路很正确,既避免了双重NAT带来的延迟问题,又解决了DHCP冲突这个根源性问题。文中提到的网口桥接是关键步骤,这相当于把路由器降级为二层交换机,所有端口同属一个广播域。建议补充说明一下,这种模式下TR3000的QoS/防火墙功能会失效,由主路由统一负责这些功能会更合理。
---
## 起因
今天打开 Chrome 的时候,发现所有网站都打不开了。起初以为是手机欠费,就续了一个月的月租。
重启随身 Wi-Fi 后还是上不了网,查了一圈,最终定位到是 IP 冲突导致的问题。
我的设备是中兴 F50 和 Cudy TR3000 路由器。之前写过一篇文章介绍过:[https://usj.cc/202603111244.html]
我一直用的是二级路由模式:F50 做主路由,TR3000 做下级路由。
查了资料之后,决定改成 AP 模式。原因有两个:
第一,可以减少一次 NAT,延迟更低。二级路由模式是设备到下级路由做一次 NAT,再到主路由做一次 NAT,双重 NAT。
AP 模式下设备直接到主路由做一次 NAT 就行。
第二,由主路由统一管理 DHCP,这样就不会出现设备撞 IP 导致上不了网的问题。
OpenWrt 界面不算友好,折腾一圈总算搞定了。下面记录过程和原理,方便以后自查,也给需要的朋友参考。
## 我的设备网络环境
主路由是中兴 F50,默认 IP 是 192.168.0.1。Cudy TR3000 刷了 OpenWrt,默认 IP 是 192.168.10.1。
两个设备不在同一个网段,直接改 AP 模式会访问不到 TR3000。所以需要先把 TR3000 的 IP 改成和主路由同网段,比如 192.168.0.2。
主路由的 DHCP 地址池我也改了一下,范围设为 192.168.0.100 到 192.168.0.200。这样其他设备拿到的 IP 都在这个区间里,不会跟 TR3000 的 192.168.0.2 冲突。
## 配置步骤
### 第一步:修改主路由 DHCP 地址池
进入 F50 后台 192.168.0.1,默认账号密码一般是 admin。找到 DHCP 设置,把 IP 池范围改成 192.168.0.100 - 192.168.0.200。
![高级设置](image.png)
![DHCP设置](image-1.png)
### 第二步:修改 TR3000 的管理 IP
进入 TR3000 后台,默认地址是 192.168.10.1。输入你的管理账号和密码。
进入接口设置,删除所有 WAN 口,只保留 LAN 口。然后把网线插到 TR3000 的 LAN 口,连接电脑。
![接口设置](image-2.png)
点击 LAN 口的常规设置,修改几个参数:
![LAN设置](image-3.png)
把 IPv4 地址改成 192.168.0.2,网关改成 192.168.0.1,DNS 也改成 192.168.0.1。
保存之后,需要用 192.168.0.2 重新登录 OpenWrt,这样设置才会生效。
### 第三步:关闭 TR3000 的 DHCP
进入接口设置,找到 LAN 口的 DHCP 服务器选项卡,勾选「忽略此接口」,然后保存应用。
![关闭DHCP](image-4.png)
这一步很关键,不关的话 TR3000 还是会给设备乱发 IP。
### 第四步:删除 WAN 口并桥接所有网口
删除 WAN:网络 -> 接口,找到 WAN 直接删掉。
![](image-6.png)
桥接 LAN:编辑 LAN 接口,进入物理设置,勾选「桥接接口」,然后把 eth0、eth1 这些以太网适配器全部勾上。
![](image-7.png)
这样所有网口都变成了交换机的 LAN 口,插哪个都能用。
### 第五步:接线
中兴 F50 的 LAN 口用网线接到 Cudy TR3000 的 LAN 口。不要插 WAN 口。
### 第六步:验证
重启一下 TR3000,然后用手机连它的 Wi-Fi,看看手机拿到的 IP。
![验证结果](image-5.png)
我的手机拿到的是 192.168.0.172,在 100-200 范围内,说明 DHCP 已经由主路由统一分配,配置成功了。
## 为什么要改 AP 模式?
OpenWrt 默认是路由器模式,直接接到主路由会有两个问题。
第一个是 DHCP 冲突。OpenWrt 默认开了 DHCP 服务器,会和主路由抢着发 IP。设备如果拿到 OpenWrt 发的 IP,网关就错了,自然上不了网。
第二个是 WAN/LAN 隔离。默认模式下 WAN 口和 LAN 口是分开的,物理连接不对就上不了网。
解决思路其实很简单:把 OpenWrt 从路由器模式变成交换机加 AP 的模式。
核心就三件事:给 OpenWrt 设一个固定 IP,这个 IP 要在主路由的 DHCP 池之外;关掉它的 DHCP 服务器;删掉 WAN 口,把所有网口桥接到 LAN。
## 如果进不去后台了怎么办?
我也遇到过这种情况,一般是因为电脑和 AP 不在同一个网段。
解决方法:拔掉 cudy tr3000 和F50之间的网线,电脑用网线直接插 cudy tr3000 的 LAN 口。
然后手动给电脑设一个同网段的 IP,比如 192.168.0.88,子网掩码 255.255.255.0,网关如图所示。
![](image-8.png)
浏览器访问 192.168.0.2 就能进去了。
## 几个核心知识点
DHCP 就是负责自动分配 IP 的设备,一个网络里只能有一个,就是主路由。AP 必须关掉自己的 DHCP。
DHCP 地址池是主路由用来分配 IP 的范围。AP 的静态 IP 必须在这个池子外面,否则会被主路由发给其他设备,造成冲突。
桥接就是把多个网口合并成一个交换机组,所有口都能互相通信。AP 模式下所有口都应该桥接到 LAN。
WAN 口在路由模式下用来连上一级网络,AP 模式下用不着,删掉省事。
Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

@@ -0,0 +1,54 @@
---
title: 尝试接入 telegram 机器人
date: '2026-06-02T00:00:00.000Z'
slug: '202606021139'
draft: false
categories:
- 生活
tags:
- telegram
author: 小赵
layout: post
---
最近有位博友给我发邮件,他的博客更换了域名,需要改一下友链的链接
果然男人过了二十五就是五十二了
在 hugo 的项目根目录下咋都没找到友联的 json 文件
因为之前懒的缘故,我也没有在 readme 记录,最后翻看了之前的屎山源码,终于找到了散落在角落的 json 数据
然后 git push,发现冲突了,又折腾一会儿,处理 git 版本冲突,原来笔记本本地文件好久没有同步了
深圳这鬼天气是真的热,晚上躺床上,翻来覆去睡不着
转念一想,这样管理友联订阅数据,也太不优雅了!
之前在学校的时候做 Java 项目研究过 telegram bot
我决定把他接入 telegram bot,起初就想实现个改友联和改订阅链接,后面想想干脆把写文章也加进来,也许这样还能增加点便利性,下班回家,谁还愿意坐电脑桌呢
于是,通过写需求文档,把需求拆成 todo ,先确认需求,再做需求,避免不必要的 token 浪费,给claude code 跑了一遍,别说,效果还真像那么回事
![image](1780330451237-AgACAgQA.jpg)
不需要太复杂的功能,主要是解决一些机械性的痛点,其实大部分操作交互式的操作反而是不如网页操作的
![image](1780330453301-AgACAgQA.jpg)
![image](1780330454866-AgACAgQA.jpg)
实际上我的服务是跑在本地的,因为这玩意也没必要跑在公网
一顿框框框写完,擦!电脑还没关!
我岂不是还得下床关电脑,这也太不优雅了!
于是,写了个远程关机重启需求
![image](1780330606155-AgACAgQA.jpg)
这下终于舒服了
当然,本文也是由 telegram bot 交互式撰写