今天要解的問題
Day15 把 coffee-review 的杯測拆成三個階段,結尾說還沒想好下一步要先動哪一塊。今天先離開 side project,換到裝著這個系列的部落格 kiwi-walk 本身。
手上有一份部落格設計檢視的 handoff,是寫給 Claude Code 的:一份 README 加一份 P0 計劃,列了六個要修的問題,每項都附上檔案位置、diff 和驗證方式。
- 中文文章的閱讀時間永遠顯示「1 分鐘」
- SEO 還是 PaperMod 的樣板字(
Put some good stuff) - 整頁出現水平捲動
- engineer 分類頁的 Medium 帳號打錯
- /projects 的標題被擠斷成「Commut / e」
- /blogs 的分類磁貼,文字是燒在圖片裡的
handoff 明確寫了「只修 bug、一致性和 SEO,不做改版」,字體、色票、版型都等之後的改版再動。
今天做了三件事:
- 照 handoff 修 P0(主線):handoff 寫得很具體,看起來照抄就好,但它是看畫面寫的,有三處跟實際情況不符
- P1 的一致性調整:分類名稱、上一篇/下一篇、導覽列加 Series 等
- 合併後才發現的全黑按鈕:一個從前次改版就存在的 CSS 優先權問題
想法與取捨
磁貼 layout:layouts/blogs/list.html,還是用 frontmatter 指定
handoff 要我新增 layouts/blogs/list.html,並註明它「只套用在 /blogs/ 本身,子分類不受影響」。
規劃時 Claude 就指出這不對:Hugo 的 Type 取的是第一層 section,/blogs/art/、/blogs/others/games/ 的 Type 都是 blogs,全部都會吃到這個模板。寫這篇時我放了一個探針模板實測:
blogs: 1
blogs/art: 1
blogs/others/games: 1
三頁都輸出了探針字串。所以改成新增 layouts/_default/category-tiles.html,由需要的頁面在 frontmatter 寫 layout: category-tiles 指定。代價是之後新增 hub 頁要記得加這行,但範圍明確,不會誤傷分類頁。
/blogs/others:保留舊的圖片表格,還是一起換成磁貼
handoff 要刪掉 custom.css 裡 table:has(img) 那組規則,但 /blogs/others(games、travel 兩張磁貼)也在用同一組規則,刪了那頁就壞了。
Claude 列了兩個選項給我:保留舊 CSS 讓 others 繼續用 markdown 表格,等改版再處理;或是 others 也換成新磁貼。我選了後者。代價是 games、travel 要補進 params.categories(名稱、tagline、配色、logo)。它們還沒有專屬 logo,先借用 others 的,tagline 從原本分類頁的介紹文字改寫。
副作用是 /blogs/others/games/ 和 /travel/ 也會出現分類 hero 橫幅,因為 hero 是用網址最後一段去查 params.categories 的。計劃裡寫明這跟其他分類一致,我同意了。
水平捲動:重現不出來時,修還是不修
handoff 的起手 prompt 寫「先診斷,回報結果後再修改」,還附了一段找出溢出元素的 console 指令。Claude 問我診斷完要不要先停下來,我選了直接修。
結果是重現不出來:Claude 用 sitemap 把 121 頁在 390px 下掃過一遍,每頁比對 scrollWidth 和視窗寬度,本機和正式站都沒有整頁溢出。handoff 懷疑的 pre 和表格,本來就在自己的容器裡捲動;導覽列在手機上也沒有溢出。
所以 handoff 提供的 .post-content table / pre / img 規則一條都沒加,因為 PaperMod 本來就有。只留了兩層保險:
overflow-x: clip只加在html,沒有照 handoff 寫成html, body。root 的 overflow 會套到 viewport,已經涵蓋整頁,body 不需要- 1000px 以下讓選單換行,不再依賴 PaperMod 的
overflow-x: auto捲軸
閱讀時間:5–7 分鐘,還是接受 3 分鐘
handoff 說開 hasCJKLanguage 之後,共織宇宙應該顯示 5–7 分鐘。實際開完,wordCount 從 35 變成 1067,閱讀時間從 1 分鐘變成 3 分鐘。
原因是 Hugo 對中文大約以每分鐘 500 字計算,handoff 的 5–7 分鐘大概是用每分鐘 200 字估的。要到 5–7 得自己改寫閱讀時間的算法,這次沒做,只把實際數字寫進 PR 描述。
上一篇/下一篇的「同分類」怎麼定義
P1 要讓上一篇/下一篇只在同分類內切換。handoff 建議用 .CurrentSection.RegularPages,但這個站有巢狀目錄:art 底下有 exhibition/,others 底下有 games/、travel/。用 CurrentSection 的話,展覽文章只會在 exhibition 裡面切換。
最後的定義是:分類取 content/blogs/<key>/ 的第一層,跟 category chip 判斷分類的方式一致,再用那一層的 RegularPagesRecursive。所以 art/exhibition 算 art,games 和 travel 算 others。系列文章維持原本的系列內由舊到新。
Series 總覽:英文網址配中文名稱
導覽列加了 Series 之後,/series/ 總覽頁顯示的是 ironman-2026 這個代號。我想要的是網址維持英文,畫面顯示中文。
系列頁本身的標題本來就是中文,問題只在總覽頁:PaperMod 的 terms.html 印的是 term 的 .Name,也就是網址代號。Tags 也共用這個模板,直接改會讓 tags 頁一起變,所以另外新增一個 series 專用的 layouts/series/terms.html,改讀系列頁的 title 和 summary。

作者名稱:只留在 SEO 裡
P0 把 params.author 填成實際的名字之後,PaperMod 在每篇文章的日期旁邊都顯示「kiwi (Lee Chi-wei)」。這個部落格只有一個作者,我只想讓名字留在 SEO 裡。
查了 PaperMod 的模板:顯示作者的是 post_meta.html,它本來就認 hideAuthor 參數;JSON-LD 則是直接讀 site.Params.author。所以加一行設定就好,<meta name=author> 和 JSON-LD 裡的作者都保留。
實作
分類磁貼
磁貼從子 section 產生,名稱、tagline、logo、配色都讀 params.categories,找不到設定就退回 section 自己的 title:
{{- range .Sections }}
{{- $key := path.Base .RelPermalink }}
{{- $cat := index site.Params.categories $key | default dict }}
{{- $name := $cat.displayName | default .Title }}
{{- $tagline := $cat.tagline | default .Description }}
{{- /* ... logo 網址(沿用 category-hero 的 hasPrefix "http" 防護) */}}
<a class="category-tile" href="{{ .RelPermalink }}"
style="{{ with $cat.accent }}--tile-accent: {{ . }};{{ end }}{{ with $cat.bg }} --tile-bg: {{ . }};{{ end }}{{ with $cat.bgDark }} --tile-bg-dark: {{ . }};{{ end }}">
{{- /* ... logo */}}
<span class="category-tile-name">{{ $name }}</span>
{{- with $tagline }}
<span class="category-tile-tagline">{{ . }}</span>
{{- end }}
</a>
{{- end }}
順序用各分類 _index.md 的 weight,照原本表格的順序排:books 1、art 2、engineer 3……others 7。文字是 HTML,配 --ink-warm 放在淺色底上,可以選取,對比也夠。style 那行為什麼要一個顏色一個 with,見下面「踩到的坑」。

上一篇/下一篇
{{- $pages := where site.RegularPages "Type" "in" site.Params.mainSections }}
{{- with .File }}
{{- $parts := split .Dir "/" }}
{{- if and (ge (len $parts) 2) (eq (index $parts 0) "blogs") }}
{{- with site.GetPage (printf "/blogs/%s" (index $parts 1)) }}
{{- $pages = .RegularPagesRecursive }}
{{- end }}
{{- end }}
{{- end }}
{{- with (.GetTerms "series") }}
{{- $pages = (index . 0).Pages.ByDate }}
{{- end }}
分類判斷放在系列判斷之前,所以系列文章最後還是會被系列的順序覆蓋。改完之後,Claude 寫了一段 Python 掃過整站 build 的結果:82 篇有上下篇導覽的文章,跨分類的連結是 0,其中 14 篇是系列文章。
分類頁拿掉麵包屑
分類頁的最上方已經有 hero 橫幅寫明是哪個分類,底下再接一行「首頁 » Blogs」顯得多餘。判斷方式跟 hero 一樣,用網址最後一段查設定:
{{- $isCategory := and (eq .Kind "section") (index site.Params.categories (trim (path.Base .RelPermalink) "/")) }}
{{- if not $isCategory }}
{{- partial "breadcrumbs.html" . }}
{{- end }}
tag 頁、archives 和文章頁的麵包屑都保留。
導覽列換行
Series 加進選單之後,390px 下的選單會換成兩行,放大鏡單獨在第二行,而且有兩個問題:第二行第一項有縮排,兩行之間也隔得很開。前者是因為 PaperMod 用 li + li 的左邊距分隔項目,換行後的第一項還帶著這個邊距;後者是因為 .nav 設了 60px 的 line-height,每一行都是 60px 高。
@media (max-width: 1000px) {
#menu {
flex-wrap: wrap;
overflow-x: visible;
row-gap: 4px;
column-gap: var(--gap);
/* .nav's 60px line-height would make each wrapped row 60px tall. */
line-height: 44px;
}
/* The theme spaces items with `li + li` margin, which would
indent the first item of a wrapped row; column-gap does not. */
#menu li + li {
margin-inline-start: 0;
}
}
改用 column-gap 分隔項目,行高壓到 44px。768px 以上選單跟 logo 在同一行,高度還是由 logo 撐到 60px,不受影響。
踩到的坑
截圖是空白的,不代表圖不見了
加了 overflow-x: clip 之後,我讓 Claude 確認文章圖片的點擊放大(image-zoom)有沒有被影響。這支 script 是直接對原本的 <img> 做 transform,custom.css 裡有一段註解警告:只要祖先元素設了 overflow,放大的圖就可能被裁掉。
Claude 前後判斷錯了三次:
- 先點第一張圖,transform 是單位矩陣,以為是那張圖放大倍率太小,所以沒觸發。換一張直式的圖再點,transform 是空的。去 grep 輸出的 HTML 找 script 裡的變數
ZOOMABLE,結果是 0 筆,一度懷疑 script 根本沒載入。其實是minifyOutput把變數名稱壓掉了,改搜 selector 字串project-landing .entry-cover就找到了。 - script 有載入,那為什麼沒反應?原來點第一張圖時其實已經打開了 zoom,點第二張是把它關掉。
- 再點一次,transform 有了(
scale(0.7344)),截圖卻只看到米色的遮罩。把body的 clip 關掉之後,圖就出現了,於是認定「body 設 clip 會把放大的圖藏到遮罩底下」。CSS 改成只設在html,還在註解裡寫下「不要加到 body」。
接著用正式的 CSS 重新載入,截圖又是空白,但這次的 computed style 跟剛才成功那次一模一樣。再截一次,圖就在了:空白只是截圖抓得太早,還沒畫出來。Day15 才被一張截在 transition 中間的截圖騙過,這次又是截圖時機。回頭把 body { overflow-x: clip } 加回去重測,放大一樣正常,第 3 次的結論是錯的。
最後保留「只設 html」,因為這樣就夠了,但把寫錯的註解改掉。之後遇到一張空白截圖,會先再截一次,再下結論。
html/template 把 var(--border-warm) 轉成 ZgotmplZ
磁貼一開始的 style 是這樣寫的,想讓沒設定的分類退回邊框色:
style="--tile-accent: {{ $cat.accent | default "var(--border-warm)" }}; ..."
合併前跑了一次 code review,發現 html/template 對 style 屬性裡的 CSS 值有過濾,帶括號的值會被換成 ZgotmplZ。用一個最小模板實測:
<a style="--a: ZgotmplZ; --b: #B89878;">x</a>
現有分類都有設定,所以畫面上看不出來;但只要新增一個分類、還沒補 hugo.yml,那張磁貼就會沒有底色。修法是 HTML 只輸出有設定的顏色,預設值改寫在 CSS 的 var(--tile-bg, var(--border-warm))。我臨時加了一個沒設定的測試分類驗證:它的 style 是空的,改吃 CSS 的預設值,頁面上也沒有 ZgotmplZ。
「下一頁」是一顆全黑的膠囊
PR 合併之後,我在分類頁的底部看到「下一頁」是一顆全黑、沒有字的按鈕。

先確認的是:這是不是這次 PR 改壞的?不是。這條規則來自前次改版(7dd2671):
body:not(.dark) a {
color: var(--ink-warm);
}
它的優先權是 (0,1,2),比 PaperMod 的 .pagination a { color: var(--theme) } (0,1,1) 高,所以深色按鈕上的字也被改成深色,對比 1:1。
為了確認還有哪些地方中招,Claude 在瀏覽器裡寫了一段 script,算每個連結的文字色和最近一層不透明背景色的對比。它掃了 11 種頁面,門檻設 3:1,抓到兩個元件:分頁按鈕和 404 頁的「回首頁繼續走」。修完後擴大到 15 種頁面重掃,沒有再找到問題。
修完之後,我又發現專案頁的「網站 →」按鈕也看不清楚。漏掉它有兩個原因:掃的頁面沒有包含各專案頁;而且深色字配綠底的對比高於 3,列表頁 /projects/ 有被掃到,但沒被判定成問題。改用 WCAG AA 對一般文字要求的 4.5:1,把 sitemap 上的頁面加上 404 共 111 頁全部重掃,只剩「網站 →」的 4.41:1。那是原本設計的配色(淺色字配專案綠 #5C7A6B),要達標得動色票,留到改版時處理。
修法有兩個選擇:降低全站規則的優先權,或為實心按鈕加例外。降低優先權會連帶改變 PaperMod 其他已經被這條規則覆蓋的連結顏色,影響範圍太大,所以選了加例外:
body:not(.dark) .pagination a,
body:not(.dark) .pagination a:visited,
body:not(.dark) .kiwi-404 a,
body:not(.dark) .kiwi-404 a:visited,
body:not(.dark) a.project-live-link,
body:not(.dark) a.project-live-link:visited {
color: var(--paper-cream);
}
:visited 也要列,因為全站還有一條 body:not(.dark) a:visited,點過的按鈕會再被蓋成另一種深色。掃對比時,門檻要一開始就用 4.5:1,範圍也要涵蓋每一種模板。
開發中的 Hugo server 沒抓到新資料夾
新增 layouts/series/terms.html 之後,/series/ 頁面完全沒變。用 hugo -d <暫存資料夾> 重新 build 一次,輸出裡已經有新模板的內容,所以是正在跑的 hugo server 沒偵測到新建的 layouts/series/ 資料夾。重開 server 就好了。之後新增 layout 資料夾、畫面卻沒變時,先跑一次全新的 build。
小結
回頭把 handoff 從頭到尾對一次,每一項實際的結果如下。
P0(必做)
| handoff 的項目 | 結果 |
|---|---|
1. 加 hasCJKLanguage,修正「1 分鐘」 | ✅ 共織宇宙 1 → 3 分鐘,wordCount 35 → 1067 |
2. description、keywords、author、images、theme_color 換成真實的值 | ✅ 全部換掉,另外刪了 favicon 和 bing/yandex 驗證的樣板值;og-default.png 還沒做,先用 logo-nobg.png |
3. 找出水平捲動的元素並修掉,加 html, body { overflow-x: clip },1000px 以下選單換行 | ⚠️ 掃了 121 頁都重現不出來;保險只加在 html,選單換行照做 |
4. Medium 帳號 sean22492248 → sean22492249 | ✅ |
5. .project-card-title-row 可換行,h2 加 min-width: 0 | ✅ 390–1024px 都不會斷行 |
6. 新增 layouts/blogs/list.html 產生分類磁貼,清掉 markdown 表格和 table:has(img) | ✅ 改用 layouts/_default/category-tiles.html 加 frontmatter 指定;/blogs/others 也一起換掉 |
P1(可順手做)
| handoff 的項目 | 結果 |
|---|---|
分類名稱統一用 displayName | ✅ art 改成 Art、Game 改成 Games,首頁按鈕拿掉 emoji |
| 上一篇/下一篇只在同分類內切換 | ✅ 改用 content/blogs/<key>/ 第一層的 RegularPagesRecursive,82 篇沒有跨分類 |
| 分類頁的麵包屑移到 hero 上方或移除 | ✅ 移除 |
| 首頁 Latest 加「全部文章 →」 | ✅ |
| About:履歷連結放進第一屏,H1 改成「嗨,我是 kiwi」 | ❌ 決定不做,已經做好的 commit 撤回了 |
| 導覽列加上 Series | ✅ 另外新增 series 專用的總覽模板,網址英文、畫面中文 |
不要做
| handoff 的限制 | 結果 |
|---|---|
| 不換字體、不改色票、不改版型 | ✅ 守住了。「網站 →」的 4.41:1 就是因為不動色票而留著 |
| 不移除 PaperMod submodule | ✅ 沒動 |
驗收
| handoff 的驗收條件 | 結果 |
|---|---|
hugo --minify 成功、沒有 warning | ✅ |
grep 樣板字為 0 筆 | ✅ |
| 共織宇宙閱讀時間 5–7 分鐘 | ❌ 3 分鐘,要改算法才到得了,這次沒做 |
| 390、768、1024px 沒有水平捲軸 | ✅ |
| 900px 時 /projects 標題不斷開 | ✅ |
| /blogs 磁貼文字可選取、Lighthouse 對比通過 | ⚠️ 可以選取;沒跑 Lighthouse,改用自己寫的對比掃描,4.5:1 通過 |
| 每一項修正各自一個 commit | ✅ P0 六項各一個 commit |
handoff 以外多做的有:作者只放在 SEO(hideAuthor)、加了 Series 之後的選單換行修正、磁貼配色的 ZgotmplZ 修正,以及合併後發現的分頁、404、「網站 →」按鈕文字修正。P0 和 P1 在 #19 合併,按鈕修正在 #20 合併。
本文同步發表於 kiwi-walk.com:https://kiwi-walk.com/blogs/engineer/ironman-2026-day16-design-handoff/
