今天要解的問題
拿 2026Q3 SEO 健檢那篇去跑 PageSpeed Insights(手機版),最佳做法和 SEO 都是 100,無障礙卻卡在 95,就差那 5 分。扣分的是兩個稽核項目:
- Background and foreground colors do not have a sufficient contrast ratio(文字對比不足)
- Elements with
role="none"orrole="presentation"have conflicts(role 衝突)

這兩項雖然都不會讓網站壞掉。我網站的配色是米色底配暖色的墨色,而這在灰褐色的次要文字(日期、頁尾、月曆上的數字)會非常淡。在保留我的配色下,只有稍微調整一點,讓我的分數可以過關。
技術背景
對比度怎麼算
WCAG 的對比度是兩個顏色「相對亮度」的比值:(L1 + 0.05) / (L2 + 0.05),L1 是較亮的那個,範圍從 1:1 到 21:1(WCAG 2.1 contrast ratio 定義)。AA 等級要求一般大小的文字至少 4.5:1,大字(18pt 以上,或 14pt 粗體)至少 3:1(SC 1.4.3)。
這裡要注意的是,對比度是一組文字色和底色的配對,不是一個顏色本身的屬性。同一個灰褐色,放在白一點的底色上會過,放在黃一點的底色上就不過。我寫了一支小 Python 照這個公式算,後面所有的比值都是它算出來的,跟 Lighthouse 報的數字一致(例如 #7A6A4F 配 #F2EBDB,Lighthouse 報 4.41,自己算 4.42,差在四捨五入)。
role=“presentation” 跟 alt=""
<img alt=""> 是「這張圖是裝飾,螢幕閱讀器請跳過」的寫法,瀏覽器會把它當成 role="presentation"。但如果同一個元素又帶了 aria-label 這種全域 ARIA 屬性,瀏覽器要忽略 presentation 角色、把它當成一般的圖片(WAI-ARIA 1.2:Presentational Roles Conflict Resolution),於是兩個訊號互相打架。Lighthouse 對這個元素的說明是:
nav.nav > div.logo > a > img
Fix all of the following:
Element does not have global ARIA attribute
意思是:要嘛別當裝飾,要嘛拿掉全域 ARIA 屬性。
想法與取捨
先在本機重跑,拿到確切的元素
但 PageSpeed 那次只留下稽核項目的名稱,看不到是哪些元素,而且只測了一頁。所以我先在本機 build 整站,用 Lighthouse CLI 對四個頁面各跑一次:首頁、/blogs/、每日筆記的月曆系列頁,和那篇 SEO 健檢文章。
npx -y [email protected] "http://localhost:8176/series/kiwi-daily/" \
--only-categories=accessibility --form-factor=mobile \
--chrome-flags="--headless=new" --output=json --output-path=series.json --quiet
結果四頁分別是 95、95、95、96,失敗的元素整理起來只有三種:
| 元素 | 前景 背景 | 對比 |
|---|---|---|
| 次要文字 | #7A6A4F#F2EBDB | 4.41 |
| 程式碼註解 | #6E738D#1C1D21 | 3.6 |
| header logo | role 衝突 | — |
第一列的「次要文字」包括頁尾、首頁的「Latest」、月曆的日期與副標、文章底下上一篇/下一篇的標題。看起來元素很多,其實是同一組顏色配對。
改 token,不改元素
修對比有兩條路:在那幾個元素上各自換成比較深的顏色,或直接把次要文字的顏色 token 調深。失敗的五種元素用的都是同一個 token --ink-soft-warm,底色也都是同一個 #F2EBDB,若一個一個改就要動五個 selector,改 token 則一次全部蓋到。加上這個網站的顏色本來就規定只從 custom.css 開頭的 token 來,所以就改 token。
但 token 一改,整站的次要文字都會跟著變,包括原本就過的地方(文章頁的日期、卡片上的 meta)。所以新顏色要越接近原色越好,小到看起來還是同一個色系。
挑哪個灰褐色
從原色 #7A6A4F 往深的方向、保持同樣色相,算了幾個候選:
| 候選 | 列表頁底色 #F2EBDB | 邊框色 #E8E2D5 |
|---|---|---|
#7A6A4F(原色) | 4.42 | 4.07 |
#77674C | 4.62 | 4.25 |
#756549 | 4.76 | 4.38 |
#736348 | 4.90 | 4.51 |
#716146 | 5.05 | 4.65 |
只看失敗的那個底色,#77674C 就過了。但這次的問題就是出在一個沒想到的底色上(見後面「踩到的坑」),所以我多看了網站裡最暗的淺色:邊框色 #E8E2D5,若哪天次要文字放到這個顏色的區塊上也要能過。#736348 是第一個在邊框色上也過 4.5 的,就選它。它在文章頁底色 #FAF7F0 上是 5.44、卡片的 #FFFDF8 上是 5.73。
程式碼註解
程式碼區塊的配色是 PaperMod 內建的 Chroma 樣式,註解色 #6E738D 在深色底色 #1C1D21 上只有 3.6。主題是 git submodule,所以在自己的 custom.css 覆寫。候選算了三個:#8B90A8 5.33、#9399B2 5.97、#A5ADCB 7.57。目標是只把註解拉過門檻、其他語法色不動,所以選三個裡最接近原色、還保留藍灰色調的 #8B90A8。
深色模式
這個網站設成 defaultTheme: light、也關掉了切換按鈕,訪客其實看不到深色模式。但 CSS 裡兩套 token 都還在,所以我還是測了:在頁面上手動加 body.dark,用 axe-core 跑同樣的規則。新改的兩個顏色在深色下都沒問題(深色的次要文字是另一個 token,在各深色底色上 5.03 到 7.23;程式碼區塊在兩種模式都是同一個深色底色,所以註解色共用)。
深色模式下其實還有別的對比問題,例如文章底下的標籤是 #B8A77F 配 #F2EBDB,只有 1.99。這些改之前就存在,原因出在 token 那段 selector 裡的 .post-single:not(.dark):文章區塊本身沒有 .dark class,所以在深色頁面裡又被套回淺色 token。要修就等於讓整套深色模式能用,是另一件事,這次先不動。
實作
次要文字 token
改的只有淺色那一組裡的兩行。--secondary 是 PaperMod 自己用的名字(頁尾、列表的 meta 讀的是它),--ink-soft-warm 是我們自己 partial 用的名字,兩個要一起改:
:root,
.list:not(.dark),
.post-single:not(.dark),
body:not(.dark) {
/* PaperMod-named variables → warm cream tokens */
--theme: #FAF7F0; /* paper-cream */
/* ... */
--secondary: #736348; /* ink-soft-warm */
/* ... */
--ink-soft-warm: #736348;
/* ... */
--code-inline-bg-warm: #F2EBDB;
--code-block-bg: #1C1D21;
/* Code blocks are dark in both modes, so dark mode reuses this. */
--code-comment: #8B90A8;
}
--code-comment 是新的 token,只在淺色那組定義一次。深色那組(body.dark)沒有重新宣告它,CSS 變數會從 :root 繼承下去,所以深色模式拿到的是同一個值。
覆寫 Chroma 的註解色
Chroma 把不同種類的註解拆成好幾個 class,主題的樣式表裡用 #6e738d 的有九個:一般註解、多行註解、單行註解、前處理指令、docstring、heredoc 等等。全部指到新 token:
/* Chroma's comment / docstring colour (#6e738d) is 3.6:1 on the code
block; lift it to AA without changing the rest of the palette. */
.chroma .c, .chroma .ch, .chroma .cm, .chroma .c1, .chroma .cs,
.chroma .cp, .chroma .cpf, .chroma .sd, .chroma .sh {
color: var(--code-comment);
}
主題的 CSS 和我們的 custom.css 會被串成同一個檔,custom.css 排在後面,同樣的 selector 權重,後面的贏,所以不用加 !important。

logo 的 aria-label
header 的 logo 是一張圖加上網站名稱文字,一起包在連到首頁的 <a> 裡。圖本身是裝飾,連結的名稱來自後面的文字「kiwi-walk」,所以 alt="" 是對的,多出來的是 aria-label="logo":
<a href="{{ "" | absLangURL }}" accesskey="h" title="{{ $label_text }} (Alt + H)">
{{- /* ... */}}
<img src="{{ $img.Permalink }}" alt=""
width="{{- site.Params.label.iconHeight | default "30" -}}" height="{{- site.Params.label.iconHeight | default "30" -}}">
{{- /* ... */}}
{{- $label_text -}}
</a>
拿掉之後,螢幕閱讀器讀這個連結時還是「kiwi-walk」,只是不會多讀一個「logo」。這個 header 是從 PaperMod 複製出來改的,aria-label="logo" 是原本主題就有的寫法,兩個分支(處理過的圖片、外部網址的圖片)各一處,兩處都拿掉。
踩到的坑
對比明明是過的
我原本以為 token 本身就沒問題:--ink-soft-warm 配網站的底色 --paper-cream(#FAF7F0)是 4.90,深色那組 7.23,兩個都過。照這樣算根本沒有對比問題,所以只能猜是疊在別的底色上的文字,要重跑才知道是哪些。
結果 Lighthouse 報的底色是 #F2EBDB,不是 #FAF7F0。這個顏色在 token 裡是「行內程式碼的底色」,照理不會出現在頁尾後面。打開首頁直接看 computed style:
body background: rgb(242, 235, 219) color: rgb(42, 37, 32)
footer.footer > span background: transparent color: rgb(122, 106, 79)
才發現 body 的底色真的是 #F2EBDB。往 PaperMod 的原始碼找,在 theme-vars.css:
.list {
background: var(--code-bg);
}
列表頁(首頁、/blogs/、系列頁)的 body 帶 .list class,PaperMod 把它的底色換成 --code-bg。我們的 token 把 --code-bg 指到 #F2EBDB,這導致列表頁的底色其實一直是它,比文章頁的 #FAF7F0 深一點、黃一點。文章頁上被點名的是「上一篇/下一篇」的標題,Lighthouse 量到的底色也是 #F2EBDB。
所以「次要文字配網站底色」這組是過的,失敗的是「次要文字配列表頁底色」,而後者沒有寫在任何一個 token 的名字裡。這也是上面挑顏色時多看一個邊框色的原因。
小結
四個頁面的 Lighthouse 無障礙分數都從 95、96 升到 100,在 preview 上重跑也一樣。改動只有三處:
- 次要文字的 token 從
#7A6A4F調深到#736348 - 新增一個程式碼註解色 token 並覆寫 Chroma 的九個註解 class
- 拿掉 logo 上多餘的
aria-label。

肉眼要特別拉出來看,但若不仔細看,根本很難注意到。灰褐色還是那個灰褐色,只是剛好跨過 4.5:1。