Claude Code 的 /logo skill,是從五次走錯路整理出來的 — SVG 路徑、正規表達式算的邊界框、一個猜錯的漢字
用 AI 代理重新設計這個部落格的 logo 與程式碼字型:壞掉的 SVG 字形、用正規表達式算出的邊界框、猜錯的漢字,以及最後整理出的 /logo skill。

本頁目錄
引言
這次要修的,是一個我不滿意很久的 logo。它是專案建立第一天就自動產生的純文字標記,之後再也沒有人認真想過它該長什麼樣子。
用 AI 代理設計 logo 要跑好幾輪:AI 代理產出候選,我看過之後刪掉一部分並說明理由,然後再跑一輪。我想知道的是,這個迴圈值不值得寫成一個 Claude Code skill。
在這幾輪裡,AI 代理有五次自信滿滿地弄錯了,而我不認為其中任何一次是白費。五次走錯路當中,有四次各自歸納出了一條規則。這些規則現在成了一個 /logo skill 和三支指令碼,下次就能用這個 skill 生成 logo。
最後上線的是一枚刻著桜的朱紅印章,旁邊是用 Fraunces 排的「oharu」;程式碼字型則換成 JetBrains Mono,只用在文章頁。
這個部落格沒有設計,而我一直將就著用
當時的 favicon 是這樣:
rect,加上一個指定 Helvetica,Arial,sans-serif 字型的 <text> 元素,以 favicon 實際使用的三種尺寸並列。那個字母用的是訪客作業系統提供的字型。頁首完全沒有標記,只有純文字的網站標題。
排版也是一樣的情況:
--font-sans: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, …;--font-mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, …;全是系統字型堆疊:網站根本沒有自己的字型。每位訪客看到的,都是自己作業系統內建的字型。這本身是正當的取捨(零位元組、零版面位移),只是我從來沒有認真考慮過它。
第一次走錯:同一個想法的二十四種變化
我先要求廣度:橫跨純圖示、文字商標與組合標記,共二十四個概念。這些概念要算繪成一份對照清單,每個都同時以 128px、32px 與 16px 呈現。
它產出了二十四個。它們全都是同一個 logo。

這是對照清單裡純圖示的那三分之一。光圈、軌道、終端機、圓弧、鳥居、圓角方形、提示符、分割。同一張圖取了八個名字。
每一個都是單線的幾何筆畫:圓、弧、均勻線寬、圓形端點。不論是圖示還是文字商標,骨架都一樣,而這副骨架正是 AI 生成 logo 最常見的預設樣式。我要的是廣度,拿到的卻是換了八個名字的同一個設計。
解決方法是先去看真正的作品。我挑了六個品牌識別,截圖之後逐一仔細看過:
| 參考對象 | 它的做法 | 第一輪的做法 |
|---|---|---|
| Linear、Astro | 實心填色,標記小而低調 | 全是線條,到 16px 就糊成一團 |
| Rauno Freiberg、Josh Comeau | 完全沒有標記。只有文字,加上一個固定使用的顏色 | 以為一定要有標記 |
| Maggie Appleton | 展示用襯線體,因為那是寫作類網站 | 幾何無襯線體,也就是範本的預設值 |
| Anthony Fu | 個人字母組合,帶有手作感 | 純幾何做不出「個人」的感覺 |
第二輪回來的東西確實不一樣了,因為這次它參考了真實的網站:

向 AI 要 N 個選項,它回傳的是同一個分布裡的 N 個樣本。
第二次走錯:從沒被算繪過的字形
手繪的 SVG 字形會有只在算繪後才看得出來的 bug。第一輪的字形一次都沒算繪過,就直接放進了對照清單。最糟的是字母 u:
M9,49 L9,99 A41,41 0 0 0 91,99 L91,140照筆的移動順序來看。左邊字幹從 x-height 往下畫到 y=99。碗狀弧線橫跨到 (91,99)。接著一條線往下畫到基線。弧線結束於 y=99,所以右邊字幹只涵蓋 99 到 140:整個上半段都不見了。算繪出來的 u,右側短了一截。
修法是把右邊字幹畫成獨立的子路徑,維持完整的 x-height 高度:
M9,49 L9,99 A41,41 0 0 0 91,99M91,49 L91,140
另外兩個 bug 也屬於同一類錯誤:
- 一行副標題放在
y=160、縮放為0.26,上伸部就落在160 + 0.26 × (−700) = −22,比位在它上方那個字的基線還高。結果t、h、b、l都刺穿了名字。 - 一個標記畫成 220 單位高,旁邊的字母卻有 700 單位高。它看起來不像 logo,倒像一個多出來的項目符號。
這些問題在瀏覽器裡一眼就能看出來,在路徑資料裡卻完全看不見。
第三次走錯:AI 代理自己假設的一個字
第二輪最有力的概念是判子(hanko),也就是日本的個人印章。這和部落格的本質很接近。朱紅底色、一個字,字以白色鏤空呈現。
名字是「oharu」,所以 AI 代理用了春(haru,春天)。看起來不錯,差一點就照這樣做下去了。
接著它開口問了,因為憑假設把一個字放進別人的品牌識別裡,不是可以默默做的事。我更正了它:「oharu」是桜明。
這個答案改變的是整個設計,不只是一個字符。桜明是兩個字,而兩個字在 16px 的 favicon 裡根本認不出來:在那個尺寸下,十畫的漢字只會變成一團紋理。

16px 的算繪結果直接給出了答案。直排的桜明是這個名字正確的印章形式,在瀏覽器分頁上卻表現最差。加了外框的版本保住了邊框,兩個字卻都看不見了。所以印章改成了圓形、單字的桜,而選一個字而不是兩個字,是由筆畫數決定的,不是品味。
第四次走錯:用正規表達式算出來的邊界框
到了正式上線的素材,手繪字母換成了從字型烘焙出來的外框:文字商標用 Fraunces,桜用 Noto Serif JP,透過 fontTools 與 HarfBuzz 產生。正確的字距與筆畫粗細對比都直接寫進了路徑資料,所以文字商標是以幾何圖形的形式上線,不需要網頁字型請求,也不會先閃過備用字型。
擺放字符時,需要知道它實際筆畫的邊界。outline.py 把路徑字串裡的每個數字都抓出來,當成交替出現的 x、y 座標對。SVGPathPen 會輸出 H 和 V 簡寫,而這兩個指令只帶一個座標。只要出現一條水平線,後面每個數字的奇偶位置就全部錯開:
路徑 M0,0 H700 L720,50實際的點 (0,0) (700,0) (720,50)regex 配對 (0,0) (700,720) (50, …)好幾份對照清單都只讓人覺得「好像有點偏」,就這樣放了過去。直到一個應該大約 3200 單位寬的組合標記算出來只有 1802,問題才變得無法否認。
修法是別再解析文字,交給函式庫去量:
from fontTools.pens.boundsPen import BoundsPen
bounds = BoundsPen(glyph_set)for info, pos in zip(buf.glyph_infos, buf.glyph_positions): xform = (scale, 0, 0, -scale, (x + pos.x_offset) * scale, 0) glyph_set[name].draw(TransformPen(pen, xform)) glyph_set[name].draw(TransformPen(bounds, xform)) # same transform文字商標實際的邊界是 2456.4 × 758.6。正規表達式先前一直很有把握地回傳別的數字。
它沒有當掉。它產出了看似合理的結果,人看了一眼就核准了,而且核准了兩次。
第五次走錯:每一頁都載入的 preload
程式碼區塊也換上了自己的字型。技術部落格真正被細讀的就是程式碼區塊,而我的程式碼區塊當時還是用 Menlo 顯示,和內文一樣處於「沒有人做過決定」的狀態。
JetBrains Mono Variable,只取 Latin 子集,40KB,自行託管在固定路徑上,這樣就能以 URL 指定 preload。程式碼部分不會另外抓 CJK 字型:程式碼都是 Latin 字元,所以 CJK 字型堆疊維持原樣。
preload 被放進了共用版面。瀏覽器直接說明了為什麼這樣不對:
The resource http://localhost:4321/fonts/jetbrains-mono-latin.woff2 waspreloaded using link preload but not used within a few seconds from thewindow's load event. Please make sure it has an appropriate `as` value andit is preloaded intentionally.首頁、標籤列表頁,以及兩個 CJK 索引頁上都沒有程式碼。每一頁都下載了 40KB,卻完全沒用到。於是改成只有在 ArticleLayout 設定了 preloadMono prop 時才輸出 preload:
| 頁面 | rel="preload" 連結數 |
|---|---|
dist/index.html | 0 |
dist/ja/index.html | 0 |
dist/tags/index.html | 0 |
dist/blog/<article>/index.html | 1 |
一則主控台警告,換來了實際省下的下載量。這是五次走錯路裡,唯一由工具主動回報的一次。其餘的都只能靠算繪、量測或提問才發現。
這幾輪最後變成了 /logo skill
這個 skill 的鐵則,以及每一條的由來:
| 規則 | 由來 |
|---|---|
| 生成任何東西之前,先研究真正的作品 | 來自同一個分布的二十四個概念 |
| 先算繪出來親眼看過,再拿給任何人看 | 只有半截字幹的 u |
| 用 16px 來判斷 | 在瀏覽器分頁裡糊成一團的桜明 |
| 絕不把關於對方的假設寫死在設計裡 | 春 |
| 用真實字型的外框,不要手工畫字形 | 手工搭出來的幾何,以及量測它的那段正規表達式 |
| 顏色靠並排比較決定,不靠理論 | 紅色印章旁的藍色文字商標 |
| 每一道關卡都由使用者來選 | 迴圈本身 |
這個 skill 要求 AI 提出方案、並附上推薦理由,然後停下來:可以提出意見,但決定權在使用者手上。本文的每一輪,都停在一道由人類來選的關卡。正因如此,五次走錯路的代價只是一個下午,而不是一整套建立在 AI 猜測上的品牌識別。
機械性的那一半由三支指令碼負責:
gallery.mjs組出對照清單,並在本機連接埠上提供瀏覽。outline.py透過 fontTools 與 HarfBuzz,把字型外框烘焙成路徑資料。build-brand-lockup.mjs是實際範例,負責產出 favicon 與頁首元件。
概念與對照清單都寫進暫存目錄,絕不寫進 repo:一輪設計產出的大多是被淘汰的方案,而 repo 不是放淘汰品的地方。
上線的成品
一枚刻著桜的朱紅圓印,旁邊是用 Fraunces 排的「oharu」。它是以兩個 SVG 上線的,而不是一個。印章刻意做得比文字商標實際的筆畫高度矮,所以把整個組合標記裁成正方形,也得不到窄螢幕用的版本。
畫面寬度低於 50rem 時只顯示印章,這也順帶解決了一個舊的版面問題:在手機上,文字商標會和搜尋控制項搶空間。
其中一個顏色,是把選項並排比較之後才決定的。網站的強調色是 #1d4ed8,印章是 #d8452a。這兩個顏色在色相上幾乎正對面,藍色文字商標放在紅色印章旁邊,衝突一眼就看得出來。文字商標改用主題的文字色,藍色只留給連結,所以兩者永遠不會相鄰。
| 文字商標路徑資料 | 3569 位元組(座標取整數後,從 5145 位元組減少而來) |
| 印章路徑資料 | 1677 位元組 |
favicon.svg | 1878 位元組 |
| JetBrains Mono,Latin 子集 | 40KB,只用在文章頁 |
/ja/ 文章頁的網頁字型請求 | 1 個(JetBrains Mono),不含 CJK 字型 |
總結
五次走錯路。沒有一次是靠推理程式碼抓到的。
那些千篇一律的概念,要擺在真實的參考對象旁邊,才看得出有多千篇一律。壞掉的字形需要一個瀏覽器。猜錯的漢字需要一次提問。邊界框的 bug 需要一個荒謬到讓人注意的數字。白費的 preload 需要一個主控台。
這五次正是把這件事當成迴圈、而不是單一提示詞來跑的理由。現在產出二十四個精緻的概念只要幾分鐘。也因為這樣,人很容易把數量誤當成廣度,也很容易接受一個建立在猜測上、看起來卻很有把握的成品。
迴圈比單一提示詞多出三件事:算繪、量測,還有提問。設計取決於只有我知道的事實時,AI 代理就該開口問我。
我並不覺得這五次試錯是白費的。因為下次當我設計 logo 時有七條規則和三支指令碼可以遵循,而不是從一個空白的提示詞開始。


