May 28, 2026

摄于 2026-5 franklin falls
本文总计 5.75k 字, 阅读约需要 14 分钟
时光飞逝岁月如梭,用现在这个静态博客已经三年了,竟然还有装修小记可写。不过随着现在 vibe coding democratize(or capitalize?) 写码估计装修也越来越少值得写了吧,刚好前阵子也确立了博客在 22 年前真正的纪念日可以改改回顾时间了,以后这个系列估计也会越来越多向博客回顾倾斜靠拢,这应该是最后一期用这个日子的过去一年博客内容回顾 + 装修小记啦。
内容 #
最受欢迎的博文 #
之前两年的流量回顾一般都是旧文靠着长尾占了前五里的大半,今年竟然全是过去一年的新文章,惊!
- 「为什么不回中国」 - 非常惊讶,很个人也完全没有干货的一篇絮叨,竟然是过去一年浏览量最高的文章……
- 「BILT 2.0 解读」 - 蹭上热度的一篇实用文,应该是为数不多大多数流量来自搜索引擎新读者的有机流量的一篇文章。
- 「生产力陷阱与无限增长幻象」 - 精神状态不佳时候写的较为 emotional 的一篇 rant,当时没啥反响,没想到总浏览量在前几。当然也有可能是因为是 BlogBlog 同樂會投稿所以会接触到一些新的读者渠道。
- 「2025 年终总结」 - 四平八稳中规中矩的一年年终总结,没想到流量还挺多。
- 「我不再做的事」 - 这种命题每人都能写类的文章应该是互链的比较多。
我最喜欢的新博文 #
以往都是挑 5 篇,今年发现写了 65 篇数量较多(其实 24 年多达 95 篇,但毕竟当时不上班,而且可能 recency bias 觉得今年整体质量稍微满意一些)且喜欢的也蛮多的,反正是自己瞎总结,直接提升成 10 篇好了:
- 「异世界般的超多图南极行记」 - 咱也不是说写得有多好,但南极真的太美了,多年之后重燃摄影热情。
- 「有惊无险量大管饱的早秋班夫之旅」 - 班夫之行惊喜和惊险都很多,要不是今年有南极也是妥妥最爱行程了。话说发现近年来每年都有至少一个特别爱的旅行和游记诶,本非旅行博主 feeling 变成输出型爱好了。
- 「博客二十二周年」 - 年纪大了很少回顾早年了,偶尔这么一回顾,真是串起青春的感觉。
- 「一些近期的消费主义试错和退货」 - 继退烧了背包之后,我又开始进军跑鞋界了……目前还挺乐在其中的,至少比背包健康不是?这篇格式蛮喜欢的,没有写成以往很容易写成的裹脚布。
- 「被 Covid 偷走的五年」 - 不回顾的话,时常会感觉 2020 之后的时间凝固了。但细数一下,其实这些年改变还挺多的。
- 「居住空间编年史」 - 由居住空间串起来的一些遥远会回忆,也比较 sentimental。
- 「像素装备设定页之爬山模组」 - 难得重拾一下像素画的形式,说要写成系列当然后来只写了一篇。但照着我之后的消费主义发展架势,下一篇硬可能会变成鞋专场……
- 「10 年 5 份工作 4 次 gap」 - 这篇和「工作的由俭入奢」算是一体两面的职业生涯回顾。
- 「也记录一下古早程序员的一周」 - 既然说到工作了,感觉不当职业博主好多年,难得最近厌班消退,仿照博友写 work log 谁知刚好久记录了近年来效率最高的周之一,还是挺有成就感的。
- 「Absolute Cinema」 - 多次问卷读者都不爱看书影游安利(毕竟这事很个人),也不敢自称 cinephile 写不了什么专题,所以除了月记之外很少有机会安利。这篇是刚好半年之内看到两个让人拍案叫绝 absolute cinema 的场景,趁热给大家按头安利。
内容概括 #
扔给 AI 表示我的博客内容主线是:技术/博客折腾、消费主义与购物复盘、健康与运动、旅行摄影、以及一组持续更新的“关我辟事”生活观察,一边写 Hugo 和代码(感觉我没写那么多折腾呀,为啥 AI 们总觉得我有,还是说我自我认知有偏差……),一边写消费、健康、旅行和职业变化,而且整体风格偏复盘、吐槽、观察与记录。高频标签:
| 标签 | 次数 |
|---|---|
| 关我辟事 | 13 |
| patreon | 12 |
| 消费主义陷阱 | 12 |
| random | 11 |
| review, wellness, tutorial, rant, photography, 问卷, tech | 6 |
装修 #
一个常见的我会取关订阅的独立博客 RSS 的原因就是博主折腾然后改了文章 URL,导致一下给 RSS 里刷出几十上百篇旧文。一两次还算无心之过,三次以上基本上内容再好也会取关了(通常情况下经常来回折腾 URL 的内容也好不到值得被隔三差五刷屏的地步
d)。己所不欲勿施于人,所以我也会额外注意不要无意中污染别人的 RSS。
本站不会来回折腾 URL 所以本无这个需求,但因为搞了时空胶囊页面,初始 backfill 一下会更新十几篇,不想在 RSS 里刷屏,于是有了从 RSS 排除文章的需求。以后每次新的时空胶囊更新还是想放在 RSS 里的,所以也不能单纯用文章 type 排除。研究了一下 hugo 似乎只有全局 RSS 选项没有单篇排除选项,索性干脆把一直在用的模版默认 RSS template override 了之后手动加了一个 exclude_rss param,需要排除某篇文章的时候设置为true即可。
customize RSS 模版的话添加一个 themes/{theme_name}/layouts/_default/rss.xml 即可:
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<title>{{ .Site.Title }}</title>
<link>{{ .Site.BaseURL }}?utm_source=rss</link>
<description>Recent content on {{ .Site.Title }}</description>
{{- $limit := .Site.Config.Services.RSS.Limit }}
{{- $pages := .Site.RegularPages }}
{{- $pages = where $pages ".Params.exclude_rss" "!=" true | first $limit -}}
{{ range $pages }}
<item>
<title>{{ if eq .Type "time-capsule"}}[时空] {{ end }}{{ .Title }}</title>
<link>{{ .Permalink }}?utm_source=rss</link>
<pubDate>{{ .Params.date.Format "Mon, 02 Jan 2006 15:04:05 MST" }} </pubDate>
<guid>{{ .Permalink }}</guid>
{{ with .Params.image }}
<image url="{{ . }}" />
{{ end }}
<description>
{{ .Summary | html }}...
阅读全文 {{ .Permalink }}
</description>
</item>
{{ end }}
</channel>
</rss>
时空胶囊页面 #
先前单独写过一篇博客,不再赘述。不过一开始的启发点是放全景图,后来发现通常拍全景的地方也会写点微型游记,写都写了也放上去。这样一来也没必要限制在全景图了,平时非大型旅游也不会专门写游记博客,但小的本地 hiking 也不乏能写的,这个区域刚好成了完美载体。因为要做这个页面,也顺便引入了 panorama viewer 在博客里,大型游记的形式也可以更丰富了,比如南极这篇里就可以有坐着船在海冰里穿梭的身临其境感,班夫这篇基本每一个主要景点都有全景展现,再比如附近的小瀑布就可以写篇小记:
后面的人不知道为什么要凑那么近突然形成了和路人的合影 XD
长毛象最近嘟文 #
年末休假在家里修博客和各种问题临时薅点羊毛 vibe code,正经东西都修完了就 vibe code 点这种没有 AI 肯定不会写的东西。当时的 AI 前端代码质量没法看又臭又长,但反正自己用的东西又不是上班不需要 maintainability,缝缝补补能用就行,也懒得优化了。效果扔在 now 页面里了:
Loading feed...
AI 代码又臭又长折叠了
{{ $profileUrl := .Get 0 }}
{{ $limit := 5 }}
{{ if .Get 1 }}
{{ $limit = int (.Get 1) }}
{{ end }}
{{/* Convert profile URL to RSS feed URL */}}
{{ $feedUrl := printf "%s.rss" $profileUrl }}
{{/* Generate unique ID for this feed instance */}}
{{ $feedId := printf "mastodon-feed-%d" (now.Unix) }}
<div id="{{ $feedId }}" class="mastodon-feed" data-feed-url="{{ $feedUrl }}" data-profile-url="{{ $profileUrl }}" data-limit="{{ $limit }}">
<div class="mastodon-loading">Loading feed...</div>
</div>
<script>
(function() {
const feedContainer = document.getElementById('{{ $feedId }}');
if (!feedContainer) return;
const feedUrl = feedContainer.dataset.feedUrl;
const profileUrl = feedContainer.dataset.profileUrl;
const limit = parseInt(feedContainer.dataset.limit) || 5;
// Fetch and parse RSS feed
fetch(feedUrl)
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.text();
})
.then(xmlText => {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
const items = xmlDoc.querySelectorAll('item');
if (items.length === 0) {
feedContainer.innerHTML = '<div class="mastodon-error">No items found in feed</div>';
return;
}
let html = '';
const itemsToShow = Math.min(items.length, limit);
for (let i = 0; i < itemsToShow; i++) {
const item = items[i];
const link = item.querySelector('link')?.textContent?.trim() || '';
const pubDate = item.querySelector('pubDate')?.textContent?.trim() ||
item.querySelector('dc\\:date')?.textContent?.trim() || '';
let description = item.querySelector('description')?.textContent?.trim() || '';
if (!description) continue;
// Keep HTML structure - description is already HTML from RSS
// If description has <strong> content, remove everything after <hr>
const tempCheckDiv = document.createElement('div');
tempCheckDiv.innerHTML = description;
const hasStrong = tempCheckDiv.querySelector('strong');
if (hasStrong) {
// Find <hr> tag (handles various formats: <hr>, <hr/>, <hr />, <hr class="...">, etc.)
const hrRegex = /<hr\s*[^>]*>/i;
const hrMatch = description.match(hrRegex);
if (hrMatch) {
const hrIndex = description.indexOf(hrMatch[0]);
if (hrIndex !== -1) {
description = description.substring(0, hrIndex);
}
}
}
// Format date
const formattedDate = formatDate(pubDate);
// Process description: handle NeoDB links, hashtags, etc.
const processed = processDescription(description, profileUrl);
description = processed.description;
const neodbUrl = processed.neodbUrl;
// Build item HTML
html += '<div class="mastodon-item">';
if (neodbUrl) {
// Insert placeholder for NeoDB card
const parts = description.split('|NEODB_CARD_PLACEHOLDER|');
html += '<div class="mastodon-description">';
parts.forEach((part, idx) => {
if (part) html += part;
if (idx < parts.length - 1) {
html += `<div class="mastodon-neodb-placeholder" data-neodb-url="${escapeHtml(neodbUrl)}"></div>`;
}
});
html += '</div>';
} else {
html += '<div class="mastodon-description">' + description + '</div>';
}
html += '<div class="mastodon-meta">';
if (formattedDate) {
html += '<span class="mastodon-date">' + escapeHtml(formattedDate) + '</span>';
}
if (link) {
html += '<a href="' + escapeHtml(link) + '" target="_blank" rel="noopener noreferrer" class="mastodon-link">View Toot</a>';
}
html += '</div>';
html += '</div>';
if (i < itemsToShow - 1) {
html += '<div class="mastodon-separator"></div>';
}
}
feedContainer.innerHTML = html;
// Process NeoDB cards after rendering
processNeoDBCards(feedContainer);
// Process NeoDB placeholders
const placeholders = feedContainer.querySelectorAll('.mastodon-neodb-placeholder');
placeholders.forEach(placeholder => {
const neodbUrl = placeholder.dataset.neodbUrl;
if (!neodbUrl) return;
// Extract dbType from URL - match pattern like neodb.social/book/123 or neodb.social/movie/456
// Remove query parameters and fragments first
const cleanUrl = neodbUrl.split('?')[0].split('#')[0];
const match = cleanUrl.match(/neodb\.social\/(.+)$/);
if (!match || !match[1]) {
placeholder.remove();
return;
}
const dbType = match[1];
const apiUrl = `https://neodb.social/api/${dbType}`;
fetch(apiUrl)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(data => {
const cardHtml = `
<div class="mastodon-neodb">
<div class="db-card">
<div class="db-card-subject">
<div class="db-card-post">
<img loading="lazy" decoding="async" referrerpolicy="no-referrer" src="${escapeHtml(data.cover_image_url || '')}">
</div>
<div class="db-card-content">
<div class="db-card-title">
<a href="${escapeHtml(neodbUrl)}" class="cute" target="_blank" rel="noreferrer">「${escapeHtml(data.title || '')}」</a>
</div>
<div class="db-card-abstract">${escapeHtml(data.brief || '')}</div>
</div>
<div class="db-card-cate">${escapeHtml(data.category || '')}</div>
</div>
</div>
</div>
`;
placeholder.outerHTML = cardHtml;
})
.catch(error => {
placeholder.remove();
});
});
})
.catch(error => {
feedContainer.innerHTML = '<div class="mastodon-error">Error loading feed: ' + escapeHtml(error.message) + '</div>';
});
function formatDate(dateStr) {
if (!dateStr) return '';
try {
const date = new Date(dateStr);
if (isNaN(date.getTime())) return dateStr;
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const month = months[date.getMonth()];
const day = date.getDate();
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${month} ${day}, ${year} ${hours}:${minutes}`;
} catch (e) {
return dateStr;
}
}
function processDescription(desc, profileUrl) {
let neodbUrl = null;
// Use DOM manipulation for better HTML processing
const tempDiv = document.createElement('div');
tempDiv.innerHTML = desc;
// Check for NeoDB links before processing
const neodbLinks = tempDiv.querySelectorAll('a[href*="neodb.social"]');
if (neodbLinks.length > 0) {
neodbUrl = neodbLinks[0].href;
// Replace NeoDB links with placeholder
neodbLinks.forEach(link => {
const placeholder = document.createTextNode('|NEODB_CARD_PLACEHOLDER|');
link.parentNode.replaceChild(placeholder, link);
});
}
// Also check for plain text NeoDB URLs
const textNodes = [];
const walker = document.createTreeWalker(tempDiv, NodeFilter.SHOW_TEXT, null, false);
let node;
while (node = walker.nextNode()) {
if (node.textContent.includes('neodb.social')) {
textNodes.push(node);
}
}
textNodes.forEach(textNode => {
textNode.textContent = textNode.textContent.replace(/https:\/\/neodb\.social\/[^\s<>"]+/g, '|NEODB_CARD_PLACEHOLDER|');
});
// Remove plain text URLs (but keep hashtag links and other anchor tags)
const allLinks = tempDiv.querySelectorAll('a');
const linkUrls = new Set();
allLinks.forEach(link => linkUrls.add(link.href));
// Remove plain text URLs that aren't in anchor tags
const allTextNodes = [];
const textWalker = document.createTreeWalker(tempDiv, NodeFilter.SHOW_TEXT, null, false);
let textNode;
while (textNode = textWalker.nextNode()) {
allTextNodes.push(textNode);
}
allTextNodes.forEach(node => {
node.textContent = node.textContent.replace(/https?:\/\/[^\s<>]+/g, (url) => {
// Only remove if it's not already in a link
return linkUrls.has(url) ? url : '';
});
});
// Remove date/time strings from text nodes
allTextNodes.forEach(node => {
node.textContent = node.textContent.replace(/\w+,\s+\d{1,2}\s+\w{3}\s+\d{4}\s+\d{2}:\d{2}:\d{2}\s+[+-]\d{4}/g, '');
});
// Remove emoji shortcodes from text nodes
allTextNodes.forEach(node => {
node.textContent = node.textContent.replace(/:[a-zA-Z0-9_+-]+:/g, '');
});
// Remove img tags
const imgs = tempDiv.querySelectorAll('img');
imgs.forEach(img => img.remove());
// Fix hashtag links - process existing hashtag links
// Try multiple selectors to catch different formats
const hashtagSelectors = [
'a.mention.hashtag',
'a[class*="mention hashtag"]',
'a[class*="hashtag mention"]',
'a.mention[class*="hashtag"]'
];
hashtagSelectors.forEach(selector => {
const hashtagLinks = tempDiv.querySelectorAll(selector);
hashtagLinks.forEach(link => {
let tagName = '';
// Try to get tag from span or direct text
const span = link.querySelector('span');
if (span) {
tagName = span.textContent.trim();
} else {
// Remove # and any whitespace
tagName = link.textContent.replace(/^#\s*/, '').trim();
}
if (tagName) {
const newLink = document.createElement('a');
newLink.href = `${profileUrl}/tagged/${tagName}`;
newLink.target = '_blank';
newLink.rel = 'noopener noreferrer';
newLink.textContent = `#${tagName}`;
link.parentNode.replaceChild(newLink, link);
}
});
});
// Also handle hashtags that might be in text but not linked
// Only process text nodes that are NOT inside anchor tags
allTextNodes.forEach(node => {
// Skip if this text node is inside an anchor tag
let parent = node.parentNode;
while (parent && parent !== tempDiv) {
if (parent.tagName === 'A') {
return; // Skip this node, it's inside a link
}
parent = parent.parentNode;
}
const text = node.textContent;
const hashtagRegex = /#([\w\u4e00-\u9fff]+)/g; // Include Chinese characters
if (hashtagRegex.test(text)) {
const fragment = document.createDocumentFragment();
let lastIndex = 0;
hashtagRegex.lastIndex = 0;
let match;
while ((match = hashtagRegex.exec(text)) !== null) {
// Add text before hashtag
if (match.index > lastIndex) {
fragment.appendChild(document.createTextNode(text.substring(lastIndex, match.index)));
}
// Create link for hashtag
const tagLink = document.createElement('a');
tagLink.href = `${profileUrl}/tagged/${match[1]}`;
tagLink.target = '_blank';
tagLink.rel = 'noopener noreferrer';
tagLink.textContent = match[0];
fragment.appendChild(tagLink);
lastIndex = match.index + match[0].length;
}
// Add remaining text
if (lastIndex < text.length) {
fragment.appendChild(document.createTextNode(text.substring(lastIndex)));
}
node.parentNode.replaceChild(fragment, node);
}
});
return {
description: tempDiv.innerHTML.trim(),
neodbUrl: neodbUrl
};
}
function processNeoDBCards(container) {
// This function is kept for compatibility but NeoDB is now handled via placeholders
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
})();
</script>
此处还有条当时写的关于此次 vibe coding 的吐槽:
先前本地 vibe 好就发上去了,然后发现远程 403 了,当天晚上 debug 了半天发现是 github pages action 会被 cloudflare bot fight mode 拦截(which 也合理),然后因为这个是免费版的东西无法自定义 ruleset,装插件用 hacky solution 都没解决。
就在我快要放弃准备用手动关闭 bot fight mode 笨办法的时候突然恍然大悟,就 AI 一开始给我写的是 build time fetch,因为我在 vibe 根本没动脑子就照着这个思路走了。但因为是要动态的展示嘟所以本来就不应该 build time fetch 否则读者看不到长毛象更新除非我更新博客。正确做法应该是用 js run time fetch 读者才能看到更新,这样一来也是读者的 organic traffic 根本不会触发 bot mode….让 AI 用 js 重写一遍改成 runtime render,立刻解决了。
简直是 vibe coding 在实际工作中的实际问题缩影了……就产量超级足,输出比谁都多,很多时候硬跑也能跑起来,但选的方案可能完全是错的(祭出下图经典 meme,当然这个 meme 本来是说人写的代码的,现在有了 vibe coding 又更进一步适用了),真要碰上问题可能得花更多时间指对方向,完全 vibe 真的不见得行。也就像前面的嘟说的,个人玩具能做,而且没这个生产力 boost 根本不会做所以也算是立功一件,但真的 robust 能用可能还要一段时间进步。
虽然从去年底到今年初其实 claude code 也进步了不少的(当年是用 cursor 写的),现在生产中也基本没有 tradcoding 传统手作了,99% 都是 AI 写的码了,但时不时还是会遇上这种判断问题,生产力数倍放大的同时偶尔会走错方向。难以想象在 vibe coding 时代刚入行的年轻人们要怎么打好基础……
Neodb 最近标记 #
也是受博友伊丽莎白猫启发半 vibe code 的。毕竟本来就在关我辟事月记里有 neodb 卡片了,做成近期的话只需要把自己的 API token 放上去 pull 自己的 shelf 然后稍作格式调整即可。暂时还在心理 debate 月记里要不要也干脆改改格式放这个自动版算了省得手动复制粘贴,但目前而言月记里还是有条目之外的碎碎念,有时候会有截图,也不光是都标记了的,就暂时还是只放在放在now 页面里了。
AI 代码又臭又长折叠了
<div class="neodb-recent-container">
<div class="debug"></div>
<div class="movies-section" style="display:none;">
<h3>🎬 最近看过</h3>
<div class="movies-grid recent-grid"></div>
</div>
<!-- Books Grid -->
<div class="books-section" style="display:none;">
<h3>📚 最近读过</h3>
<div class="books-grid recent-grid"></div>
</div>
<!-- Games Grid -->
<div class="games-section" style="display:none;">
<h3>🎮 最近玩过</h3>
<div class="games-grid recent-grid"></div>
</div>
<div class="no-data" style="display:none;">
<p class="text-gray-400 text-sm italic">暂时无法获取 NeoDB 数据,请稍后再试。</p>
</div>
</div>
<script>
(async () => {
const container = document.querySelector('.neodb-recent-container');
const token = '{{ .Site.Params.NeodbToken }}';
if (!token) {
container.querySelector('.debug').textContent = '缺少 NEODB_TOKEN';
return;
}
// Fetch function matching your Astro code
async function fetchNeoDB(category) {
try {
const res = await fetch(`https://neodb.social/api/me/shelf/complete?category=${category}&page=1`, {
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/json',
},
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
return json.data ? json.data.slice(0, 4) : [];
} catch (error) {
console.error(`NeoDB ${category} fetch failed:`, error);
return [];
}
}
// Fetch data
const [movies, tvs, books, games] = await Promise.all([
fetchNeoDB('movie'),
fetchNeoDB('tv'),
fetchNeoDB('book'),
fetchNeoDB('game'),
]);
// Movies
const allMoviesTV = [...movies, ...tvs]
.sort((a, b) => new Date(b.created_time) - new Date(a.created_time))
.slice(0, 8); // Top 8 total
// Render sections
if (allMoviesTV.length) {
container.querySelector('.movies-grid').append(
...allMoviesTV.slice(0, 4).map(createCard)
);
container.querySelector('.movies-section').style.display = 'block';
}
// Books
if (books.length) {
const grid = container.querySelector('.books-grid');
books.forEach(item => grid.appendChild(createCard(item)));
container.querySelector('.books-section').style.display = 'block';
}
if (games.length) {
container.querySelector('.games-grid').append(
...games.map(createCard)
);
container.querySelector('.games-section').style.display = 'block';
}
if (!allMoviesTV.length && !books.length && !games.length) {
container.querySelector('.no-data').style.display = 'block';
}
function createCard(record) {
const { item, rating_grade: rating, comment_text: comment, created_time } = record;
const div = document.createElement('div');
div.className = 'db-card';
div.innerHTML = `
<div class="db-card-subject">
<div class="db-card-post">
<img loading="lazy" decoding="async" referrerpolicy="no-referrer"
src="${item.cover_image_url}">
</div>
<div class="db-card-content">
<div class="db-card-title">
<a href="${item.id}" class="cute" target="_blank" rel="noreferrer" title="${item.title}"">
${item.title}
</a>
</div>
<div class="rating">
个人评分:<span class="allstardark">
<span class="allstarlight" style="width:${rating * 10}%"></span>
</span>
</div>
<div class="db-card-abstract">
${comment || item.brief || '暂无评论'}
</div>
</div>
<div class="db-card-cate">${item.category}</div>
</div>
`;
return div;
}
})();
</script>
<style>
/* Your existing global db-card styles assumed to be loaded */
.recent-grid {
display: grid !important;
grid-template-columns: 1fr;
gap: 1rem 1.5rem;
margin: 1rem 0;
}
@media (min-width: 768px) {
.recent-grid { grid-template-columns: repeat(2, 1fr); }
}
.recent-grid .db-card { margin: 0 !important; width: 100%; }
</style>
博客从 github page 迁移到 cloudflare page #
(现在经常会手滑把 cloudflare 打成 claudeflare…)好像初衷是有个最近开始订阅也经常想留言的博主是 giscus 评论系统,又不想博客在互联网上裸奔(虽然我从来都不打草搞吧所以库里确实也都是公开内容啦),github page 要求仓库公开,外加最近 github reliability 巨差,索性动了迁移的心。然后意外发现速度比 github page 快了好几倍吧也算锦上添花了。
迁移本身挺简单的没有太多需要说的,唯一要注意的一点是迁移之后发现 twikoo 评论区的 emoji picker 用不了了,定睛一看之前设置的 emoji config CDN 是 github 的地址,现在转成 private 了自然用不了了。把那个 json file 传了一份自己的 S3 再在 twikoo 控台更新地址之后发现依然用不了,原因是 CORS 没设置。解决方法是在自己的 S3 settings 里加上自己博客 CORS Configurations 的 GET, HEAD,然后给那个文件 purge CDN cache 即可。
系列文章 #

懒得重写一个短代码了,直接加在现成的模版底部了:
{{ if $.Params.series }}
{{ $seriesName := $.Params.series}}
{{ $seriesPages := where site.RegularPages ".Params.series" $seriesName }}
{{ if gt (len $seriesPages) 1 }}
<h3>系列文章</h3>
<ul>
{{ range sort $seriesPages "Date" }}
<li>
<a href="{{ .Permalink }}">{{ if eq .Type "time-capsule"}}[时空] {{ end }}{{ .Title }}</a>
{{range (first 3 (.Params.tags))}}
{{ $tagColor := substr (md5 .) 0 6}}
<div class="tag tag-small" style="--tag-color: #{{$tagColor}}1A" >{{ . }}</div>
{{end }}
</li>
{{ end }}
</ul>
{{ end }}
{{ end }}
音频播放 #
纯粹是因为写 claude 提示音这篇的时候想要一个 single button 的音频播放器,然后惊讶地发现 html 自带的 <audio> tag 竟然不能隐藏控制板,比如:
于是半 vibe 了一个只显示播放键的 audio play button,效果如下:
<audio {{ if .Get 1}}id="{{ .Get 0 }}"{{else}}controls{{end}}>
<source src="{{ .Get 0 }}" type="audio/mpeg">
</audio>
<button class="audio-play-btn" data-audio="{{ .Get 0 }}"
{{ if .Get 1 }}
style="background:#ddd; border:none; width:36px; height:36px; border-radius:50%; cursor:pointer;"
{{ else }}
style="display:none"
{{ end }} >
▶
</button>
博客纪念日 #
前阵子心血来潮追溯了我的博客历史确定了博客纪念日,我的 About 页面先前有一句“写了博客 20 多年“,索性写了个短代码自动更新这个”XX 年“。
{{ $dateStr := .Get 0 | default "2004-03-16" }}
{{ $target := time $dateStr }}
{{ $today := now }}
{{ $years := sub $today.Year $target.Year }}
{{ if or (lt $today.Month $target.Month) (and (eq $today.Month $target.Month) (lt $today.Day $target.Day)) }}
{{ $years = sub $years 1 }}
{{ end }}
{{ $years }}
系列文章
- Hugo 小装修记
tutorial
hugo
code
- Hugo 装修小记之二
tutorial
hugo
code
- 静态博客一年啦!Hugo 装修小记之三
tutorial
hugo
code
- 静态博客两周年 & Hugo 装修小记之四
tutorial
hugo
code
- 静态博客三周年回顾 & Hugo 装修小记之五
tutorial
hugo
code
如果您觉得本文对您有帮助,想支持我的博客创作,或者有特定的内容想要看到,或者想约 coffee chat 等,欢迎:订阅 Patreon 参与博客选题和定制服务在 Kofi 上给我买杯奶茶























