今天要解的問題
想回頭看自己寫了什麼,kiwi-walk 現在只剩 /archives/:依年、依月分組的一長串清單。總共 134 篇、分布在 23 個月裡,光是 2026 年 10 月就有 49 篇,可是清單上完全看不出哪段時間寫得多、哪幾個月整個空掉。
同時間,覺得之前連載 kiwi-daily 的目的有點偏移,本來是想要每天寫,但目前卻像是不斷堆砌著天數而已,因為我寫完文章後,是順著篇數往下,導致我今天寫的文章,實際上日期是寫11月,但其實又發佈在部落格內了。這也不方便我之後想要了解那時候的我在做什麼,所以就想說與 /archives/ 來做個結合。
就聯想到 kiwi-daily 有一個月曆系列頁,看得出哪天有寫、哪天斷掉。但若是連載停掉之後,文章改回實際寫完的日期,似乎月曆可以移動了。
所以這次就把月曆搬到 Archives 的最上面,再加上一條月份的熱度長條。

想法與取捨
一個元件、放在最上面
熱度圖和月曆合成一個,頁面上不加說明
這次把熱度圖和 kiwi-daily 的月曆合成一個元件,放在 Archives 最上面,也不再用 kiwi-daily 月曆「靠左一欄」的版面,桌機直接拉滿欄寬。
月曆不黏在頂端,只放在頁面最上方。點一個日期,頁面捲到那天的文章;要回到月曆,就用回頂端的按鈕。頁面上也不加任何說明文字或圖例。
月份長條
一格一個月,深淺是當月總篇數
長條的第一版是全期間的月份排成一條:格子之間只留約 1px 的細縫,選到的月份用一個框標出來,沒有文章的月份直接不放。現在手機的滑桿和沒有 JS 的頁面還是用這一條,桌機後來改成一次只看一年。
熱度一律用總篇數:每天看當天總共幾篇,每個月看當月總共幾篇,不用平均。但月份不能直接套每天的分級(0/1/2/3 篇以上),因為當月篇數從 1 篇到 30 多篇都有,照每天的分級算,大部分月份都會是最深色,等於沒分。所以月份另外訂一組:
- 1–2 篇
- 3–5 篇
- 6–14 篇
- 15 篇以上
改完的分布是 1 級 9 個月、2 級 10 個月、3 級和 4 級各 2 個月。
手機不放月曆
只留月份,做成左右滑的滑桿
桌機用的是拉滿欄寬、格子裡寫出當天標題的月曆;手機則不放月曆,只留月份。
一開始手機是保留同一條月份長條,後來改成月份滑桿:中間固定一個指示點,月份格子在底下左右滑,放開後停在最近的月份,頁面捲到列表裡那個月。滑桿的樣品原本上方有一大塊跟著熱度變色的色塊,寫月份和篇數,但那塊太強烈了,就拿掉,改成中間格子正上方一個小標籤「2026 年 10 月 · 49 篇」。

滑動方向用一般輪播的方向:月份由舊到新從左排到右,手往左滑看到比較新的月份。
網站本來有一條規則是「手機上什麼都不能左右捲動」,這個滑桿是刻意的例外,屬於設計上的考量。所以滑桿是在一個 overflow: hidden 的框裡用 transform 移動軌道,頁面不會被撐寬。
手機跟桌機的設計邏輯,理論上要不一樣,直接照搬可能會出大事,所以就思考了下手機習慣的邏輯,改了不同的做法。
日格:從底色到 icon
深色格子讀不到字,篇數改看 icon 數
日格的第一版是:底色深淺表示當天篇數(0/1/2/3 篇以上),一個分類放一個 icon,最多 3 個。同一天有好幾篇 engineer,也只會有一個 engineer 的 icon,篇數靠底色表示。
但看了預覽之後,問題很明顯:顏色越深,格子裡的粗體標題和日期就越難讀。所以日格乾脆不上色,有文章的日子和空白的日子同樣底色、同樣邊框。
這樣篇數就要換個方式看。同一天多篇時,我還是想看到多個 icon,而且是疊在一起的,不用分開排。所以改成一篇一個 icon,同分類也重複出現,順序跟浮框一樣是最舊的在前;最多疊 4 個,第 5 篇起在後面接一個「+N」的小圓。像 10/20 那天有 7 篇,就是四個 icon 加「+3」。
一次只看一年
長條只放選中那一年的 12 格
桌機的長條原本放全期間的月份,太擠的時候(格子窄於 8px)會切成「年份按鈕+12 個月」的精簡模式。用了之後,我想要長條改成只顯示一年,左右兩邊換成 « 和 ‹:« 往前一年,‹ 往前一個月。
最後 « ‹ › » 四個都放在長條兩端。長條是選中月份那一年的 12 格,下面寫 1–12,沒文章的月份是不能點的空框。換年時停在同一個月份,那一年那個月沒文章,就取最近的有文章的月份,一樣近就取比較早的。例如 2024 年 10 月按 «,2023 年的 8 月和 12 月一樣近,就停在 8 月。
一年只有 12 格,769px 寬的時候一格也有 40px 以上,永遠夠寬,原本的精簡模式就整個拿掉了。
列表拿掉縮圖
每篇只留分類、標題、日期
列表的第一版每篇有一張 72×54 的小縮圖。但縮圖直接用的是文章封面,最寬 1400px 的 WebP,只靠 loading="lazy" 延後載入,手機捲完整頁要下載好幾 MB。
看過沒有縮圖的版本之後,就決定拿掉了:每篇只留分類標籤、標題、日期,內容往左移。閱讀時間更早就拿掉了,列表只印日期。
實作
篇數在 build 時算好
所有篇數、分級和標題都在 Hugo build 時算好,JS 只負責換月、捲動和滑桿。每天的文章用日期字串分組:
{{- $byDate := dict }}
{{- range $pages.ByDate }}
{{- $d := .Date.Format "2006-01-02" }}
{{- $byDate = merge $byDate (dict $d ((index $byDate $d) | default slice | append .)) }}
{{- end }}
月份用 GroupByDate,順便算出月分級:
{{- range $pages.GroupByDate "2006-01" "asc" }}
{{- $n := len .Pages }}
{{- $level := 1 }}
{{- if ge $n 15 }}{{ $level = 4 }}{{ else if ge $n 6 }}{{ $level = 3 }}{{ else if ge $n 3 }}{{ $level = 2 }}{{ end }}
{{- /* ... */}}
{{- end }}
GroupByDate 只會產生有文章的月份,所以長條自然沒有空月。
長條的每一格是一般連結
<a class="arc-strip-cell" href="#{{ .id }}" data-month="{{ .ym }}" data-level="{{ .level }}"
aria-label="{{ .year }} 年 {{ .month }} 月,{{ .count }} 篇">
href 是列表裡月份標題的 id(#2026-October),所以沒有 JS 時點一格就跳到列表的那個月。有 JS 的桌機會攔下點擊,改成切換月曆的月份,網址的 hash 用 #2026-10,跟列表的 id 不衝突。
2026-10-25 更新:彙整頁的日期統一改成
2026-10-19格式,列表的月份標題變成2026-10,id 也改成#2026-10,跟月曆的 hash 是同一個。上面的#2026-October和「不衝突」是舊版的說法。
桌機一年 12 格的長條是另一組 .arc-years,每年一列,沒文章的月份是 <span class="arc-strip-cell" data-level="0">,只在桌機有 JS 時出現。
日格:按鈕旁邊放浮框
<div class="arc-day has-post{{ if ge $col 5 }} is-right{{ end }}" data-date="{{ $date }}">
<button class="arc-day-btn" type="button" data-date="{{ $date }}"
aria-label="{{ int $start.Month }} 月 {{ $d }} 日,{{ $n }} 篇:{{ (index $ps 0).Title | plainify }}">
<span class="arc-day-num">{{ $d }}</span>
<span class="arc-day-title">{{ (index $ps 0).Title | plainify }}</span>
<span class="arc-day-logos">
{{- range first 4 $logos }}<img src="{{ . }}" alt="" width="20" height="20" loading="lazy">{{ end -}}
{{- if gt (len $logos) 4 }}<span class="arc-day-more" aria-hidden="true">+{{ sub (len $logos) 4 }}</span>{{ end -}}
</span>
</button>
<div class="arc-tip">
<!-- 每篇一個連結:分類 icon + 標題 -->
</div>
</div>
$logos 是當天每篇的分類 icon,同分類也重複。只輸出前 4 個,超過的寫成「+N」;「+N」設 aria-hidden,篇數由按鈕的 aria-label 讀出來。
浮框只在滑鼠移上去,或鍵盤焦點在格子裡時打開:
.arc-day:hover .arc-tip,
.arc-day:has(:focus-visible) .arc-tip { opacity: 1; pointer-events: auto; }
« »:找最近的月份
function yearJump(step) {
var p = parts(current);
var y = years[years.indexOf(p.y) + step];
if (!y) {
return;
}
var best = -1;
var gap = Infinity;
months.forEach(function (ym, k) {
if (ym.indexOf(y + '-') === 0) {
var d = Math.abs(Number(ym.slice(5)) - p.m);
if (d < gap) {
gap = d;
best = k;
}
}
});
go(best);
}
months 由舊到新排,比較用的是 <,所以距離一樣時先遇到的、也就是比較早的月份勝出。
手機滑桿
.arc-strip.is-swiper {
overflow: hidden;
padding: 36px 0 34px; /* the label above, the dot and years below */
}
.arc-strip.is-swiper .arc-strip-track {
flex-wrap: nowrap;
width: max-content;
transition: transform 0.25s ease;
}
軌道用 pointer events 跟著手指改 transform: translateX(),外框 overflow: hidden,長條再加上 touch-action: pan-y,上下滑頁面照常,頁面也不會被撐寬。拖曳時只更新中間的小標籤,放開手指才捲動列表,頁面不會一直跳。
踩到的坑
按鈕裡不能放連結
日格原本打算整格是一個 <button>,浮框放在裡面。但浮框裡的標題是連結,<button> 裡不能再放連結。所以改成外面包一層 <div class="arc-day">,按鈕和浮框是兄弟。
浮框一直開著
浮框一開始用 :focus-within 打開。結果滑鼠點完格子,頁面已經捲到列表了,按鈕還留著 focus,浮框就一直開在那裡。改成 :has(:focus-visible),只有鍵盤的焦點會讓它留著。
拖曳之後多一次 click
拖曳結束後,瀏覽器可能還會送一次 click,觸控時甚至晚一輪才來,等於拖完又被當成點了一下。要把這次 click 吞掉,但不能吞掉之後用鍵盤按 Enter 的 click,鍵盤送的 click detail 是 0:
/* detail 0: Enter on a focused cell, never the drag's own click. */
if (dragged && e.detail !== 0 && dragged.contains(cell)) {
dragged = null;
return;
}
另外,手在滑桿上往上下滑時,瀏覽器會接手去捲頁面,送的是 pointercancel。一開始把它當成放開處理,上下捲動就會讓滑桿跳到某個月;現在 cancel() 只把長條放回按下時的月份,不捲動列表。
連結本身可以拖
長條的格子是連結,而連結預設可以被 HTML5 拖曳,一拖就把 pointer 的拖曳取消掉了,所以要擋 dragstart。setPointerCapture 也要等手指真的移動 4px 才呼叫,不然單純點一下的 click 不會落在連結上:
press.moved = true;
/* Capture only now, so a plain click still lands on its link. */
press.box.setPointerCapture(e.pointerId);
點長條卻跳到列表
在預覽上點桌機的長條,月曆有換月,頁面卻同時捲到下面的列表。原因是 PaperMod 的 footer.js 幫每個 href="#…" 的連結加了平滑捲動,而且比月曆的 handler 先跑。先前驗證時太早讀 scrollY,平滑捲動還沒開始,所以沒抓到。
改成月曆在 capture 階段接手 click:
/* Capture phase: footer.js gives every href="#…" link its own click
handler that smooth-scrolls to the target; strip cells must not reach
it, or the page scrolls to the list as the month changes. */
root.addEventListener('click', function (e) {
// ...
var cell = e.target.closest('.arc-strip-cell[data-month]');
if (cell) {
e.preventDefault();
e.stopPropagation();
// ...
}
}, true);
舊文章沒有 icon
〈喧囂的孤獨〉在月曆上沒有 icon。舊文章的 categories 裡寫的是年份或場館(categories: [2024]),而 category-info.html 拿第一個分類當 key,2024 不是網站的分類,就什麼都沒有。改成取第一個是網站分類的值,找不到才用路徑判斷,8 篇舊文章補回了 icon 和分類標籤。
單一 icon 是橢圓
只有一篇的日子,icon 看起來不是圓的。量起來 <img> 是 16×20,兩個 icon 時每個卻是 20×20。
.arc-day-logos 是寬度跟著內容走的 flex 容器,每個 img 都有 margin-right: -4px 來疊在一起。只有一個 icon 時容器就是 20 − 4 = 16px,flex 預設會縮小子元素,img 就被擠成 16px 寬,border-radius: 50% 畫出來變成橢圓。
修法是 img 加 flex: none,重疊只加在第二個以後。後來改成一篇一個 icon 時,重疊加大到 8px,現在的規則是:
.arc-day-logos img {
flex: none;
width: 20px;
height: 20px;
border-radius: 50%;
box-shadow: 0 0 0 1.5px var(--entry);
}
/* Two type selectors, so this outranks .arc-day-logos img. */
.arc-day-logos img + img,
.arc-day-logos img + .arc-day-more { margin-left: -8px; }
重疊的選擇器壓不過
上面那個註解也是踩出來的。動手改之前畫 mockup 時,重疊先寫成 > * + *,它的權重比 .arc-day-logos img 低,結果 icon 沒有疊起來,769px 寬時還超出格子。所以實作一開始就寫成 img + img、img + .arc-day-more 兩個類型選擇器。
放大格子,年份也跟著放大
手機滑桿中間那格要放大一點,一開始直接在格子上 transform: scale(1.2),結果掛在格子裡的年份標籤也跟著放大。改成把顏色畫在 ::before,只放大 ::before:
.arc-strip.is-swiper .arc-strip-cell.is-on::before { transform: scale(1.2); }
小結
現在 Archives 最上面就看得出哪幾個月寫得多:桌機一次看一年的 12 個月,下面的月曆每天寫第一篇的標題、一篇一個 icon,點一天就捲到那天的文章;手機左右滑月份,放開就捲到那個月。
算是圓滿我的想法,我覺得原本的月曆很漂亮,很有成就感,看到自己不斷地累積,在每個月的每一個日子,真是不錯。