各位老友们好,我是 Chlorine。继续水文,讲讲这几天摸鱼的经历。

准确来说应该是我能记得的经历,因为我记性一向不好,而且这几天的 Git commit 也异常多:

An image to describe post

优化 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_modulesbun.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 样式之类的。静态博主都是装修爱好者,折腾就完了,把园子装修得漂漂亮亮的。

明天我考磕墓三科目三,考完之后我就要着手二次备案了。