rename content/post to content/posts
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 证书都是自动续期的。这种“省心”的感觉,是用过静态博客后就回不去的。
|
||||
|
||||
## 关于成长
|
||||
|
||||
博客记录了我从学生到“打工人”的转变。
|
||||
|
||||
翻看以前的博文,有高考前的紧张,有军训时的吐槽,有大学生活的琐碎,也有毕业时的迷茫。每一篇文章,都是当时心境的真实写照。
|
||||
|
||||
现在的我,已经毕业一年多了,每天忙碌于工作和生活之间,留给博客的时间越来越少。有时候想写点什么,打开编辑器,却又不知道从何下笔。
|
||||
|
||||
但这个博客,我会一直保留下去。它就像一个老朋友,默默地记录着我的成长轨迹。也许更新频率会变慢,但它永远都在。
|
||||
|
||||
## 写在最后
|
||||
|
||||
感谢这六年来,一直关注和支持我的朋友们。是你们的评论和鼓励,让我坚持到了现在。
|
||||
|
||||
未来的日子里,希望我们都能保持那份“折腾”的热爱,但也别忘了生活的本质。
|
||||
|
||||
六年,你好。
|
||||
|
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 主题暗夜模式的主色调。这种颜色既保留了深色的沉稳,又不会让文字显得过于刺眼,阅读体验极佳。这种“高级感”正是我一直想要的。
|
||||
|
||||

|
||||
|
||||
## 来自“瞎眼”打工人的吐槽
|
||||
|
||||
说完了开心的技术探索,再来吐槽一下糟心的工作。
|
||||
|
||||
我们公司的产品是一个 Web 端的数据运维平台。作为一名苦逼的打工人,我每天的工作就是盯着这个屏幕,监控数据、排查故障。
|
||||
|
||||
关键是,这个平台它**没有暗黑模式**!
|
||||
|
||||
你能想象吗?每天对着一个高亮的大白屏超过 8 个小时,尤其是有时候加班到晚上,办公室灯光一关,那个屏幕亮得简直像个探照灯,直射我的视网膜。每天下班回家,眼睛都酸胀得不行,感觉快要瞎了。
|
||||
|
||||
跟同事吐槽过无数次,希望能加个暗黑模式,结果永远是现在能跑起来已经很不错了。
|
||||
|
||||
无奈之下,我只能自力更生,给浏览器装了个 **Dark Reader** 插件,强制把公司的网页转成暗黑模式。虽然有时候颜色渲染会有点怪,图标也会变糊,但至少,我的眼睛算是保住了。
|
||||
|
||||
## 结语
|
||||
|
||||
正是因为自己在工作中饱受“光污染”的折磨,我才更坚定地要把博客的暗黑模式做好。
|
||||
|
||||
所以,无论是为了满足自己的强迫症,还是为了给深夜来访的朋友们一双“护眼符”,我都必须把 Ying 主题的昼夜切换功能做出来,而且要用最舒服的 Slate 配色,做到极致。
|
||||
|
||||
悄悄告诉你,我已经在 Ying 主题中实现了暗黑模式,并且用 Slate 配色,这里有个小机关,点击头像即可切换哦!
|
||||
|
||||
希望今晚的你,能在这个深色的页面里,读得舒服,睡得安稳。
|
||||
|
After Width: | Height: | Size: 422 KiB |
|
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 评论系统加上了这个功能。效果还凑合,选中文字后会弹出一个“引用评论”的按钮,点击就能自动跳转到评论区并引用选中的内容。
|
||||
|
||||

|
||||
|
||||
稍微整理了一下代码,分享给有需要的朋友。我是个前端小白,代码写得比较粗糙,大佬们见笑了。无论你用什么主题,只要是 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 确实是个不错的替代方案。
|
||||
|
After Width: | Height: | Size: 620 KiB |
|
After Width: | Height: | Size: 277 KiB |
|
After Width: | Height: | Size: 393 KiB |
|
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的调整,然后把之前的搜索文章逻辑进行复用,于是就有了现在的效果,做完,还美美的欣赏了一遍,真别说,还不错!
|
||||
|
||||
## 一些贴心的小功能
|
||||
|
||||
除了视觉上的调整,也加了两个实用的小功能:
|
||||
|
||||
* **昼夜切换**:对于像我这样喜欢深夜阅读的人来说,刺眼的白底黑字简直是“视力杀手”。加上这个切换按钮,是对眼睛最基本的尊重,你可以点击头像进行切换,也可以在其他页面通过工具栏进行切换昼夜模式,这里听取了一个博友的建议,改了昼夜切换的逻辑,默认第一次获取访客的深色模式状态,后续再根据访客的操作进行切换。
|
||||
|
||||
* **蕉太狼温馨提示**:在首页右下角加了一个小小的“蕉太狼”挂件。它会在早晨问好,深夜催睡。虽然只是简单的几行代码逻辑,但让博客多了一点“人情味”。
|
||||
|
||||
* **丝滑的图片加载**:给文章里的图片加了个“懒加载”动画。以前图片是“刷”的一下出来的,现在是当你滑到它的时候,它会优雅地从下往上浮现出来,那种顺滑的感觉,就像是德芙巧克力一样(夸张了哈),反正我是盯着看了好几遍。
|
||||
|
||||
## 效果展示
|
||||
|
||||
旧样式
|
||||
|
||||

|
||||
|
||||
新样式
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 写在最后
|
||||
|
||||
其实做博客这么久,越来越觉得,**内容始终是核心,而样式只是容器**。
|
||||
|
||||
我们花时间去优化样式,不是无休止的折腾,而是为了让盛在里面的内容能以更优雅、更舒适的姿态呈现给读者。
|
||||
|
||||
这次的调整,算是一次小小的“大扫除”吧。看着焕然一新的小站,心里也觉得踏实了不少。希望你们也会喜欢这些细微的变化。
|
||||
|
After Width: | Height: | Size: 339 KiB |
|
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串起步,一串两串的,佐料都撒不上去。”
|
||||
|
||||
我愣了一下,拿着单子不知所措。
|
||||
|
||||
那一瞬间,尴尬多过了食欲。
|
||||
|
||||
“那……算了。”
|
||||
|
||||
我放下手里的单子,转身走了。
|
||||
|
||||
既然老板看不上,也正好省钱了
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 817 KiB |
|
After Width: | Height: | Size: 630 KiB |
|
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)
|
||||
|
||||
## 演示图
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
写主题还是很累的,就这样,下机了!
|
||||
|
After Width: | Height: | Size: 201 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
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 里使用 `` 语法即可。主题会自动抓取这些图片并生成九宫格预览,同时在详情页里也会做排版优化。
|
||||
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 指教。
|
||||
|
||||

|
||||
|
||||
|
After Width: | Height: | Size: 63 KiB |
|
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项目
|
||||
|
||||

|
||||
|
||||
然后就结束了,在前端填写你绑定好的域名,实际测试下来,速度很不错,体验感比giscus评论体验感好上不少
|
||||
|
||||
KV存储的方式是以键值对的形式存储,也很好备份,只需copy键对应的值即可
|
||||
|
||||

|
||||
|
||||
|
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 即可键出 双拼,比传统全拼可以少敲击很多次,从而减少按键的输入
|
||||
|
||||
输入法我每天都要用,通过双拼方案,我可以大大减少敲击键盘次数,累计起来对打字速度的提升也是巨大的
|
||||
|
||||

|
||||
|
||||
## 双拼的缺点
|
||||
|
||||
因为我使用的双拼输入时间并不长,我目前发现一个缺点就是,全拼两个字母能自动联想的词语,双拼只能死板地当做 声母 + 韵母,最典型的就是 哈哈,全拼的时候,输入法记忆之后,我只需要打 hh,即可敲击哈哈,如果双拼方案则需要依然死板的敲击haha,才可键入 哈哈
|
||||
|
||||
如果你对双拼有兴趣,可以参考以下视频
|
||||
|
||||
https://www.bilibili.com/video/BV1bv4y1P7Hs/?spm_id_from=333.1391.0.0&vd_source=03f609196865dbb1517e87152cab7791
|
||||
|
||||
小鹤双拼官网
|
||||
|
||||
https://flypy.cc/help/#/up
|
||||
|
||||
当然,我的这篇文章,全部使用双拼输入,感兴趣的朋友可以试一试
|
||||
|
After Width: | Height: | Size: 576 KiB |
|
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 额度已经满了,文章发不出去,笑死😂
|
||||
|
||||

|
||||
|
||||
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
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左右的流量,说实话个人用其实完全够用了,刷视频、打游戏、下载东西都绰绰有余。
|
||||
|
||||

|
||||
|
||||
让我苦恼的是,手机长时间开热点,电量流的跟水一样,其次就是手机后台应用开得比较多的话,手机就会异常发热,冬天还好,夏天那就有点夸张了。
|
||||
|
||||
## 5G CPE ##
|
||||
|
||||
既然不打算装宽带,那我的选择就只有5G CPE了。这里先给不太了解的朋友解释一下,什么是5G CPE?
|
||||
|
||||
网上专业解释是这样的:
|
||||
|
||||
> 5G CPE(Customer Premises Equipment)是一种用户终端设备,它可以接收5G信号,并将其转换为Wi-Fi信号或有线网络信号,供多个设备同时使用。
|
||||
|
||||
说白话一点,就是把5G信号变成Wi-Fi,让你的手机、电脑、平板都能上网,相当于一个不用插网线的路由器,就是一个能插卡的随身WiFi。
|
||||
|
||||
## 方案选择 ##
|
||||
|
||||
我在狗东和bilibili搜索了很多CPE,比如比较出名的烽火,口碑蛮不错的,但是价格偏高
|
||||
|
||||
选了一圈,最后还是直接选择了酷安上比较火的方案:**中兴F50 + Cudy TR3000 256MB**方案,也是直接抄作业!
|
||||
|
||||

|
||||
|
||||
为什么选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刷完高级后台可玩性很高,里面内置很多插件,甚至可以在路由器上养虾
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 购买价格 ##
|
||||
|
||||
事实上在购买阶段,我发现现在内存和硬盘涨价都挺厉害的,导致中兴F50 + Cudy TR3000 256MB的价格也有小幅增长,这里我盘算了一下购机价格,给大家一个参考。
|
||||
|
||||
- **中兴F50**:这个是在拼多多上买的全新的,移动大存储版,自带256GB存储,花了399块钱,这个价格我觉得还算合理,毕竟是全新的,带保修的,价格也不算太离谱
|
||||
|
||||
- **Cudy TR3000 256MB**:我是在闲鱼上收的二手,花了180块钱,卖家很好,帮忙刷了immortalwrt,直接就能用,价格也很实在,现在内存价格涨得厉害,256MB的版本二手价也涨了不少,这个价格我觉得很划算了
|
||||
|
||||
- **总计**:399 + 180 = 579块钱
|
||||
|
||||
说实话这个价格我还是能接受的,各位如果想买的话,可以多看看拼多多、京东、淘宝等平台,价格可能会有一些波动,建议多比较一下,找到性价比最高的购买渠道。
|
||||
|
||||
## 使用体验 ##
|
||||
|
||||
到现在为止,这套设备我已经用了一段时间了,说说我的真实使用感受吧。
|
||||
|
||||

|
||||
|
||||
- **首先说说速度** :在我住的地方,广电5G信号还不错,测速的话下行能到300-400Mbps,上行50-80Mbps,这个速度日常使用完全够用了,刷4K视频一点都不卡,下载东西也很快。当然速度这个东西还是要看当地的5G信号覆盖情况,如果信号不好的话速度肯定会慢一些。
|
||||
|
||||
- **再说说稳定性** :这一点我还是挺满意的,连续开机几周了,没出现过断流情况,很稳定,打游戏也不会突然掉线。之前用手机热点的时候,有时候会突然断一下,虽然很快就恢复了,但还是挺烦人的,至少现在没有遇到过。
|
||||
|
||||
- **发热情况** :发热方面也还行,F50摸起来是温热的,不烫手,TR3000的温度更低,几乎感觉不到什么热量,都在可接受范围内。夏天的话可能温度会高一些,但应该也没问题,到时候怼个风扇散热就好了。
|
||||
|
||||
## 尾声 ##
|
||||
|
||||
目前使用了一段时间,中兴F50 + Cudy TR3000这套组合对于租房、临时办公或者懒得拉宽带的朋友来说,确实是一个不错的选择,性价比也挺高的。只要你手里有充足的流量卡,或者能搞到便宜的流量卡,这个方案完全可以替代传统宽带,用起来基本没什么差别。
|
||||
|
||||
如果当地5G信号不太好,或者对网络质量要求特别高,那还是装宽带比较稳妥,毕竟有线网络摆在那儿就是更稳定。不过要真的在琢磨5G上网方案的话,可以考虑参考一下这个组合。而且5G方案还有个巨大的优势——搬家很方便,不用折腾注销宽带、迁移宽带那些麻烦事儿,直接把设备装进包里就走了,主要是他不大,到新住处直接插电就能用。
|
||||
|
||||

|
||||
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
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 的模型配置,无需手动修改配置文件,通过图形界面点击操作,就能轻松切换自定义模型厂商,很方便。
|
||||
|
||||

|
||||
|
||||
## 安装 一键启动 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 即可
|
||||
|
||||

|
||||
|
||||
安装脚本后,只需在任意文件夹空白处右键,选择「Claude Code Here」
|
||||
|
||||
就能一键启动终端并进入 Claude Code 面板,省去了手动切换目录、输入命令的步骤,方便不少。
|
||||
|
||||

|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 418 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 324 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 331 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
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实现在线编辑博客,于是就有了我后台在线编辑器的第一版。
|
||||
|
||||

|
||||
|
||||
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条。
|
||||
|
||||

|
||||
|
||||
点击标题直接进编辑页,不用到处找编辑按钮。hover会浮出预览、编辑、删除三个操作。
|
||||
|
||||

|
||||
|
||||
预览会自动启动Hugo dev server(如果没自己手动执行的话),在新标签页打开`localhost:1313`,也可以点击右上角的眼睛。
|
||||
|
||||

|
||||
|
||||
删除不会真删,进了回收站,保留30天,误删了还能捞回来。
|
||||
|
||||
这个主要是参考了小饿博主的idea。
|
||||
|
||||

|
||||
|
||||
## 撰写体验
|
||||
|
||||
新建文章时,Slug自动生成`YYYYMMDDHHmm`格式的数字时间戳,跟我之前文章的命名风格保持一致。
|
||||
|
||||
文件夹名则是`日期-中文标题`,比如`2026-05-20-我的静态博客动态化方案`,找起来方便。
|
||||
|
||||
编辑器右侧挂了两个面板:上面是快捷插入(H2、H3、代码块、引用、图片等),下面是AI助手。
|
||||
|
||||
AI浪潮怎能不加点AI元素,选中文字可以润色、扩写、缩写、纠错;填了标题能自动生成摘要和标签。
|
||||
|
||||

|
||||
|
||||
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和凭据缓存在本地,下次自动登录。
|
||||
|
||||

|
||||
|
||||
界面是左右分栏:左边文章列表,按MD文件日期倒序排列,最新的在最上面。
|
||||
|
||||
点开一篇文章,右侧按需加载评论,不会像第一版那样全量拉取6000条导致卡死。
|
||||
|
||||
支持在线回复、隐藏、删除评论。
|
||||
|
||||
回复时会显示被回复者的昵称和内容摘要,知道上下文。
|
||||
|
||||
## 订阅源和友链
|
||||
|
||||
这两个模块,我都一定程度实现了动态化,都对接了我自己搭建的API。
|
||||
|
||||
订阅源管理可以增删RSS源,支持批量检测存活状态。
|
||||
|
||||

|
||||
|
||||
友链管理更完整一些:增删改查、隐藏/显示切换、单条或批量存活检测。
|
||||
|
||||

|
||||
|
||||
检测走的是腾讯云SCF代理,因为有些博客在境外,直连可能不通。
|
||||
|
||||
友链列表里每个站点前面有个WiFi图标,绿色表示在线,红色表示失联。
|
||||
|
||||
这样我能及时发现哪些博友的网站打不开了,该隐藏就隐藏,保持友链页面的健康度。
|
||||
|
||||

|
||||
|
||||
## 微信公众号同步
|
||||
|
||||
最近我也开始尝试写公众号。我在自己的接口站新增了微信公众号素材接口,可以把文章同步到公众号草稿箱。
|
||||
|
||||

|
||||
|
||||
流程很简单:后台里选一篇文章,填个封面图URL,点「同步到微信草稿」就行了。
|
||||
|
||||
Markdown会自动转成微信兼容的HTML——标题有深蓝底边线、代码块是Dracula深色主题、引用有蓝色左边框、底部有个居中的「—END—」收尾。
|
||||
|
||||
在公众号后台的草稿箱里预览一下就能直接群发,不用再排版一遍,这样可以保证博客和公众号的同步更新。
|
||||
|
||||
## 图片管理
|
||||
|
||||
单独做了一个图片管理页面,把所有文章文件夹里的图片扫出来,网格预览,能看到每张图的文件大小和所属目录。
|
||||
|
||||
一键复制Markdown引用地址,多余的图片可以直接删。
|
||||
|
||||
以前图片散落在几十个文件夹里,根本不知道哪些在用哪些没用了。现在一目了然。
|
||||
|
||||
同时也可以当做一个回忆相册,哈哈,不是吗?
|
||||
|
||||

|
||||
|
||||
## 开源提示词
|
||||
|
||||
因为本系统是基于自己的主题深度定制的,不适合开源给大家。
|
||||
|
||||
这里把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
|
||||
- "不知道点哪里" → 标题做成可点击,加明显按钮
|
||||
|
||||
```
|
||||
|
||||
希望来访的朋友,可以关注一下我的公众号,感谢!
|
||||
|
||||

|
||||
|
||||
@@ -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 图标变绿就说明整套东西跑通了。
|
||||
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
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。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 第二步:修改 TR3000 的管理 IP
|
||||
|
||||
进入 TR3000 后台,默认地址是 192.168.10.1。输入你的管理账号和密码。
|
||||
|
||||
进入接口设置,删除所有 WAN 口,只保留 LAN 口。然后把网线插到 TR3000 的 LAN 口,连接电脑。
|
||||
|
||||

|
||||
|
||||
点击 LAN 口的常规设置,修改几个参数:
|
||||
|
||||

|
||||
|
||||
把 IPv4 地址改成 192.168.0.2,网关改成 192.168.0.1,DNS 也改成 192.168.0.1。
|
||||
|
||||
保存之后,需要用 192.168.0.2 重新登录 OpenWrt,这样设置才会生效。
|
||||
|
||||
### 第三步:关闭 TR3000 的 DHCP
|
||||
|
||||
进入接口设置,找到 LAN 口的 DHCP 服务器选项卡,勾选「忽略此接口」,然后保存应用。
|
||||
|
||||

|
||||
|
||||
这一步很关键,不关的话 TR3000 还是会给设备乱发 IP。
|
||||
|
||||
### 第四步:删除 WAN 口并桥接所有网口
|
||||
|
||||
删除 WAN:网络 -> 接口,找到 WAN 直接删掉。
|
||||
|
||||

|
||||
|
||||
桥接 LAN:编辑 LAN 接口,进入物理设置,勾选「桥接接口」,然后把 eth0、eth1 这些以太网适配器全部勾上。
|
||||
|
||||

|
||||
|
||||
这样所有网口都变成了交换机的 LAN 口,插哪个都能用。
|
||||
|
||||
### 第五步:接线
|
||||
|
||||
中兴 F50 的 LAN 口用网线接到 Cudy TR3000 的 LAN 口。不要插 WAN 口。
|
||||
|
||||
### 第六步:验证
|
||||
|
||||
重启一下 TR3000,然后用手机连它的 Wi-Fi,看看手机拿到的 IP。
|
||||
|
||||

|
||||
|
||||
我的手机拿到的是 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,网关如图所示。
|
||||
|
||||

|
||||
|
||||
浏览器访问 192.168.0.2 就能进去了。
|
||||
|
||||
## 几个核心知识点
|
||||
|
||||
DHCP 就是负责自动分配 IP 的设备,一个网络里只能有一个,就是主路由。AP 必须关掉自己的 DHCP。
|
||||
|
||||
DHCP 地址池是主路由用来分配 IP 的范围。AP 的静态 IP 必须在这个池子外面,否则会被主路由发给其他设备,造成冲突。
|
||||
|
||||
桥接就是把多个网口合并成一个交换机组,所有口都能互相通信。AP 模式下所有口都应该桥接到 LAN。
|
||||
|
||||
WAN 口在路由模式下用来连上一级网络,AP 模式下用不着,删掉省事。
|
||||
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
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 跑了一遍,别说,效果还真像那么回事
|
||||
|
||||

|
||||
|
||||
不需要太复杂的功能,主要是解决一些机械性的痛点,其实大部分操作交互式的操作反而是不如网页操作的
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
实际上我的服务是跑在本地的,因为这玩意也没必要跑在公网
|
||||
|
||||
一顿框框框写完,擦!电脑还没关!
|
||||
|
||||
我岂不是还得下床关电脑,这也太不优雅了!
|
||||
|
||||
于是,写了个远程关机重启需求
|
||||
|
||||

|
||||
|
||||
这下终于舒服了
|
||||
|
||||
当然,本文也是由 telegram bot 交互式撰写
|
||||
|
||||