各位老友们好,我是 Chlorine。继续水文,讲讲这几天摸鱼的经历。
准确来说应该是我能记得的经历,因为我记性一向不好,而且这几天的 Git commit 也异常多:
优化 GitHub 卡片
这几乎是我的每日任务了。由于 GitHub/GitLab/Codeberg 的卡片基本上是一样的,所以说一优化可以优化三次,三倍业绩,三倍快乐,改 bug 也是三倍快乐。
这次的优化很多,包括但是不限于:
- 样式打磨,我的目标就是 Vercel/iOS18 的风格~
- API 逻辑改写,更多地使用 Hugo 模板
- 错误处理完善
- 使用 license 映射来改进显示
- 自动截断过长的描述
- 图标更改,采用 Simple Icons 的 GitLab/Codeberg 图标
- 去掉父元素的
<a>
标签,改良整体样式和点击容错性
此外,我在之前的老朋友 Blowfish 主题那里闲逛的时候发现他们加了 Gitea 短代码,无论谁在写 Hugo shortcode,我小氯都要帮帮场子。直接加上。Gitea 和 Codeberg 的逻辑基本上一样,毕竟 Codeberg 就是基于 Gitea 的。
顺便说一句,我发现我犯了个非常执杖的错误,就是我已经用 resources.GetRemote
获取 API 的数据了,我居然用 JavaScript 又获取了一次。
refactor CSS
闲着没事去主题原型 Hugo landscape 的仓库看了看,发现恐咖冰糖大佬把 CSS 重构了一下。正好我也一直被无法正确导入 CSS 困扰,于是就动手开始 refactor 了。
我原本的文件结构大概是:
.
├── css
│ ├── addon.css
│ ├── algolia(almost of no use)
│ │ ├── algolia_dark.css
│ │ └── algolia_light.css
│ ├── base.css
│ ├── normalize.css
│ ├── tailwind.css
│ └── whisper.css
├── js ...
├── json ...
└── style.css
其中 style.css
是 UnoCSS 自动构建生成的,而我的几乎所有魔改 CSS 都堆在 addon.css
中,看着很难受,维护起来也不方便。
重构代码时我选择了恐咖冰糖大佬使用的 SCSS 方法:在文件夹中创建 SCSS 索引文件,然后在 css.html
中直接用 Hugo 来解构 SCSS 得到 CSS 导入。一顿拆分重组后得到了:
.
├── addon
│ ├── algolia.css
│ ├── fonts.css
│ ├── friend.css
│ ├── index.scss
│ ├── others.css
│ ├── twikoo.css
│ └── whisper.css
├── base
│ ├── base.css
│ ├── index.scss
│ ├── normalize.css
│ └── tailwind.css
├── main.scss
├── test.txt
└── uno.css
此外,对于一些只在特定页面生效的 CSS 文件(例如 whisper.css
只在《碎语》页面生效),我们直接在页面进行导入,避免不必要的开销。
{{ with resources.Get "css/addon/whisper.css" }}
<style>
{{ .Content | safeCSS }}
</style>
{{ end }}
侧边栏优化
在我看来,侧边栏是个相当重要的页面元素。我在侧边栏堆了公告(Announcement)、目录(TOC)、分类(Categories)和标签(Tags)。在移动端,它们会显示在主体卡片的下方。
目录
这个目录改得我几乎红温。我原本是直接采用 Hugo 的 {{ .Page.TableOfContents }}
模板,但是其样式令人一言难尽。而且可能是因为 Swup 的原因,目录是没办法随着文章同步更新的。于是,在深思熟虑后,我决定暂时将其去掉。
《fix bug by removing the feature》
公告
公告组件是我比较看重的东西。我采用的是在 Hugo 配置文件相应选项开启的情况下,直接读取 content
下的 announcement.md
进行展示。其功能比较完善,但是样式看起来并不好看。
我去查了些资料,找到了一个名为 prose 的布局,经过实验看起来还不错。所以就改成了这样:
<div id="announcement-content" class="collapse-wrapper px-4 overflow-hidden">
{{ $announcement := .Site.GetPage "announcement" }}
{{ with $announcement }}
<div class="rounded-xl backdrop-blur-md p-2 mx-4 mb-2 relative md:mx-auto md:max-w-lg">
<div class="prose dark:prose-invert max-w-none text-sm md:text-base">
{{ .Content | markdownify }}
</div>
</div>
{{ end }}
</div>
移动端隐藏组件
我感觉,移动端很少有人会去专门翻最下面的 categories 和 tags,所以我干脆直接把它们 hidden md:block
了。这样最下面就是 profile 和 announcement 了。
图标
从页面的元素选择上,各位老友应该可以看出,我非常喜欢 UnoCSS 的矢量图标,尤其是 IBM 的 Carbon Icons 系列。
所以我打算给侧边栏的组件加图标。这个不算难,一个 flex
布局,再把图标和标题套在一起就可以了。
<div class="font-bold transition text-lg text-neutral-900 dark:text-neutral-100 relative ml-8 mt-4 mb-2 flex items-center
before:content-['']
before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)]
before:absolute before:left-[-16px] before:top-[5.5px]">
<i class="i-carbon-data-categorical mr-2"></i>
Categories
</div>
加个边距看上去好看。
我用的图标分别是:
<i class="i-carbon-border-full mr-2"></i>
<i class="i-carbon-data-categorical mr-2"></i>
<i class="i-carbon-tag-group mr-2"></i>
partial 化
我原本的组件样式大部分是写在 sidebar.html
那里的。但是这样让 sidebar.html
看起来很不整洁,所以我把大部分的代码都移动到了相应的部分代码(partial)中。
{{/* sidebar.html */}}
<div id="sidebar"
class="w-full row-start-3 row-end-4 col-span-2 lg:row-start-2 lg:row-end-3 lg:col-span-1 lg:max-w-[17.5rem] onload-animation">
<div class="flex flex-col w-full gap-4 mb-4">
{{ partial "sidebar/profile.html" . }}
</div>
<div class="flex flex-col w-full gap-4 top-4 sticky">
{{ if .Site.Params.Basic.announcement }}
<widget-layout id="announcement-widget" class="pb-4 card-base">
{{ partial "sidebar/announcement.html" . }}
</widget-layout>
{{ end }}
<widget-layout id="category-widget" class="pb-4 card-base hidden md:block">
{{ partial "sidebar/categories.html" . }}
</widget-layout>
<widget-layout id="tags-widget" class="pb-4 card-base hidden md:block">
{{ partial "sidebar/tags.html" . }}
</widget-layout>
</div>
</div>
可以看到基本上是一个模子刻出来的,形成可复用打法了属于。
折叠展开
此外还写了个折叠展开的代码。但是感觉现在的 tags 和 categories 都不是很长,因此就没真正加上。
Hello,109chan
《hello 宇宙最新力作》
前言
熟悉博客圈的老友应该知道一个著名的工具:TianliGPT。这个工具来自洪哥(张洪 Heo)和 Tianli,是个自动生成博客文章 AI 摘要的工具。对于希望在听故事之前简单了解其脉络的老友来说是个很好的补益。
问题是:这个工具收费。虽然不贵,但是以小氯的节俭程度,还是不太舍得花这个钱的。
那你买域名的时候怎么一点也不节俭啊喂!
于是我就开始想替代方法。很巧,在之前写 Java 大作业的时候,我们有一个 feature request 就是 AI summary(用带清的智谱清言 API 生成新闻摘要)。我当时的思路大概是:
最后实现得非常好,除了学校给的 API SDK 居然不能用,必须 HTTP,害得我和答疑坊的大佬调了半个下午。
于是我想按照这个思路做一个 AI 摘要。不过,我不太熟悉怎么在 Hugo 里面用数据库,而且我的智谱 API 《只有》一千万 token 的额度,我比较担心用完(你确定你能写这么多?)。
于是我又想到了薅 CloudFlare 的羊毛。用 Workers 做 JavaScript 运行时,D1 做数据持久化,Workers AI 做摘要。这个免费额度是绝对够的,唯一的问题是我不会。
穷途末路之下,我想到了我的好伙伴:Quail。
各位老友应该都知道,我的 newsletter 是通过 Quail 实现的,而 Quail 为每位创作者都免费提供 AI 摘要生成功能。这个摘要直接写在 Markdown 源文件的 YAML front matter 里面,是完全静态的,既不怕丢,也不用执行复杂的 API 请求和数据库调用。
那么,就是你了。
设计
由于只有 content/posts
中的页面需要加摘要,所以我们直接改动相应的 single.html
即可。位置我选择在封面图和正文之间。
先用 AI 搓个形:
<div class="my-4 p-4 bg-neutral-100 dark:bg-neutral-800 rounded-lg shadow-md cursor-pointer transition-transform transform hover:scale-105" id="ai-summary-toggle">
<div class="flex items-center justify-between">
<span class="text-neutral-900 dark:text-neutral-100 font-semibold">AI 摘要</span>
<svg class="w-5 h-5 text-neutral-900 dark:text-neutral-100 transition-transform duration-200 transform" id="ai-summary-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</div>
<div class="mt-2 text-neutral-700 dark:text-neutral-300 hidden" id="ai-summary-content">
{{ .Description }}
</div>
</div>
然后我简单修了下,大概就是这样了:
<div class="my-4 p-4 border dark:border-color-neutral-600 border-color-neutral rounded-xl shadow-md cursor-pointer transition-transform transform-gpu md:hover:scale-105 shadow-md md:hover:shadow-lg"
id="ai-summary-toggle">
<div class="flex items-center justify-between pb-3">
<div class="flex items-center">
<span class="i-carbon-ai-generate text-neutral-900 dark:text-neutral-100 mr-4"></span>
<span class="text-lg text-neutral-900 dark:text-neutral-100 font-semibold">AI 摘要</span>
</div>
<div class="flex items-center">
<span
class="px-2 py-1 bg-gradient-to-r from-blue-500 to-purple-500 bg-opacity-20 backdrop-blur-md text-neutral-300 rounded-md text-sm mr-2 ring-1 ring-offset-1 ring-indigo-200 dark:ring-indigo-700">109酱</span>
<svg class="w-5 h-5 text-neutral-900 dark:text-neutral-100 transition-all duration-300 transform hover:scale-110"
id="ai-summary-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
<div class="border-t border-neutral-200 dark:border-neutral-700 my-3"></div>
<div class="mt-3 text-neutral-700 dark:text-neutral-300 hidden" id="ai-summary-content">
{{ .Description }}
</div>
</div>
移动端的点击放大我关掉了,因为看起来不太舒服。高斯模糊的手艺是跟 GPT 学的,本来想用 var(--primary)
(就是我的博客那个一直在变化的主题色),但是不知道为什么没成功。
使用 description
字段可以保证其尽量都是 AI 摘要,summary
字段我比较喜欢用来扯淡。难绷的是 Quail 的标准 AI 摘要键是 summary
,造成我需要手动改一下。已经和作者歌词经理反馈了,作者说会想办法优化一下。
Why 109chan?
还有个关键问题,为什么这个 AI 摘要助手(迫真)要叫 109 酱?
这又要说起现实中园子里的一些故事了。在紫操(紫荆操场)旁边,有一座形状比较特别的建筑,叫紫荆学生公寓综合服务楼,由于其形状,我们一般称其为 C 楼。C 楼的 109 号房间是综合服务台,也叫总台。一般来说,有什么问题不知道去哪里问,就可以去总台。由于 AI 摘要的目的是让老友们更省力地阅读园子里的故事,因此我将其命名为 109 酱(109chan)。如果后续有问答之类的服务,应该也会是 109 酱来承担。
更新依赖
npm 有 package-lock.json
,pnpm 有 pnpm-lock.yaml
,yarn 有 yarn.lock
, Bun 有 bun.lockb
,这充分说明了对于 JavaScript 包,锁定版本有多么重要。我曾经试着直接对 theme/efimero
的依赖进行更新,结果自然是必遭严惩,样式全都乱了。
最近闲着没事去 UnoCSS 的官网的官网看了下,发现已经到 0.62
了,而我的依赖还是 0.60
左右。由于我非常喜欢 UnoCSS,并且确信自己会一直使用,于是我想着更新一下。
直接删除 node_modules
和 bun.lockb
,然后把 package.json
的版本全改成 latest
,直接一手 bun i
更新。
然后构建,不出意料出了很多错误:
Failed to load custom icon "copy" in "carbon": TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module "file:///Users/chlorine/Dev/Hugo/themes/efimero/node_modules/@iconify-json/carbon/icons.json" needs an import attribute of "type: json"
at validateAttributes (node:internal/modules/esm/assert:88:15)
at defaultLoad (node:internal/modules/esm/load:133:3)
at async nextLoad (node:internal/modules/esm/hooks:746:22)
at async nextLoad (node:internal/modules/esm/hooks:746:22)
at async nextLoad (node:internal/modules/esm/hooks:746:22)
at async Hooks.load (node:internal/modules/esm/hooks:383:20)
at async handleMessage (node:internal/modules/esm/worker:199:18) {
code: 'ERR_IMPORT_ATTRIBUTE_MISSING'
}
看来是缺少一个导入类型。在询问 GPT 后,得知改动 uno.config.ts
中的导入部分为动态导入即可:
import { defineConfig } from "unocss";
import { presetUno } from "unocss";
import { presetIcons } from "unocss";
import { presetAttributify, presetTypography } from "unocss";
import presetLegacyCompat from '@unocss/preset-legacy-compat';
import carbonIcons from '@iconify-json/carbon/icons.json';
import mdiIcons from '@iconify-json/mdi/icons.json';
export default defineConfig({
presets: [
presetAttributify(),
presetUno(),
presetTypography(),
presetIcons({
collections: {
carbon: () => carbonIcons,
mdi: () => mdiIcons,
},
scale: 1.2,
warn: true,
}),
presetLegacyCompat({
commaStyleColorFunction: true,
})
],
// ...
});
结语
除此之外还有一堆优化,我们就不展开说了,像什么优化 Algolia Docsearch 样式之类的。静态博主都是装修爱好者,折腾就完了,把园子装修得漂漂亮亮的。
明天我考磕墓三科目三,考完之后我就要着手二次备案了。