iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Modern Web

前端不寫 Python,照樣 ship 一把網頁無障礙 CLI系列 第 16

Day 16:我的連結沒有一個「了解更多」,問題在另一頭

  • 分享至 

  • xImage
  •  

Day 16 · W3 · 無障礙線 · 難度 ★★★☆☆

本系列由 AI 協作撰寫。 內容、技術判斷、程式碼由 light-design 數位顧問團隊與 Claude 共同產出,最終由作者驗證後 publish。完整協作模式與把關方式見 Day 01

在螢幕報讀軟體有一個「連結清單」模式,把整頁的連結抽出來排成一排單獨唸給你聽。平常在版面上,「了解更多」旁邊有標題、有圖、有一整張卡片撐著語意。抽進清單之後那些全部消失,只剩四個字。

了解更多
了解更多
了解更多
了解更多

使用者要嘛一個一個試,要嘛放棄。WCAG 2.4.4「鏈結目的」管的就是這件事。

我要找的其實是兩類。

第一類是通用模糊詞,「了解更多」是代表。W3C 的失敗樣式清單裡有一條 F84,標題就叫「使用 click here 或 more 這類非特定連結文字」。順帶一個常被搞混的地方:F84 掛的是 2.4.9,AAA。「連結文字單獨可懂」是 AAA 才要求;2.4.4 是 AA,允許靠周圍脈絡補足。

第二類是同名但目的不同。這一類我有明確理由去找,送 AAA 審查時,意見裡點名了我的站兩組:

首頁    三種方案的「查看方案」,需要各自加 title 才分得出目的
方案頁  「預約諮詢」也有相同問題

我們照著改了。所以今天去數之前,我以為自己知道會看到什麼。六個頁面撈下來,117 個 <a href>

<a href> 總數                     117
沒有可讀名稱                        0
名稱是通用模糊詞                     0
名稱直接是網址                       0
同樣文字指向不同網址                  0 組

四項全過。

一句話主軸:工具的價值不是判得準,是它把判斷寫成一句可以被否證的話。

一份「全過」的報告,憑什麼相信

最後那個 0 得先說明,因為它跟審查意見看起來衝突。三個「查看方案」現在指向同一頁,網站結構後來改了,所以這條檢查確實沒東西可報。那個 0 不是問題沒發生過,是條件變了。

其他三項也一樣值得問一句憑什麼。先看那條規則到底做了什麼。

if not have_llm(ctx):          return      # 沒模型直接不跑
if is_standard_pattern(text):  continue    # 跳至主要內容、首頁、登入…
if is_definitely_vague(text):  → 報一則     # 黑名單
if len(text) > 8:              continue    # 太長不送模型
judge_or_caveat(...)                       # 剩下的才問模型

那份黑名單只有 20 個詞,而且是完全相等比對。我實測了一下:

'更多'        命中
'點此'        命中
'了解更多'     沒命中     ← 我這篇本來要抓的東西
'看更多'      沒命中
'Learn more' 沒命中

我以為會抓到一排「了解更多」,就算站上真的有,擋下它的也不會是這份名單。

我原本以為模糊的連結都很短,最長的有 73 字

117 條連結在四道閘門的分流是這樣:

  6   空文字        → 迴圈開頭就跳過
 12   跳至主要內容等 → 標準樣式,直接放行
  0   黑名單命中
 35   超過 8 字      → 不送模型
 64   送進模型

那個 len(text) > 8 的閘門,背後的假設是「模糊等於短」。可是我站上連結文字的長度分佈長這樣:

 1-4   49
 5-8   27
 9-10   0   ← 門檻正好落在真空帶
11-20   1
21-40  21
41+    13   最長 73 字

雙峰。門檻設 8 還是 10 完全一樣,因為中間根本沒有東西。

最長那 73 字,是整張作品卡片包成一個連結。標籤、標題、整段描述,全部變成同一個連結名稱:

系統開發 營造數位管理系統 整合 LINE 通訊的營造業全方位管理平台,
讓工地現場到辦公室的流程全面數位化。

在連結清單模式裡,這一整段會被當成一個項目唸完。

那不是 2.4.4 的缺陷,連結目的完全清楚。但它示範了一件事:被閘門擋掉的那 35 條,是我這個站連結的主要長相,而規則從頭到尾沒看過它們一眼。

117 條裡,有 59 條對不上

規則判的字串是 a.get_text(strip=True)。那行程式碼其實是一句宣稱:「使用者聽到的就是這串字。」

宣稱寫下來了,就可以拿去對。我用 Playwright 開 CDP,抓 Chromium 實際算出來的無障礙名稱,跟規則用的那串字逐條比。

cdp = page.context.new_cdp_session(page)
cdp.send("Accessibility.enable")
ax = cdp.send("Accessibility.getFullAXTree")
# 每個節點的 name.value 就是報讀軟體會唸的那一句
117 條
 59  兩邊不一樣          ← 一半
     35  只差一個空白
     24  內容真的不同

連結名稱對照圖:左欄是 a11y-moda 用 get_text(strip=True) 抽出的字串,右欄是 Chromium 從無障礙樹算出的名稱,117 條中有 59 條兩者不同,其中 35 條僅差空白、24 條內容不同,圖中並列三組實例

同一批連結,兩種算法,一半對不上。

這裡要先擋一個誤讀。我把 117 條全部換成瀏覽器算出來的名稱,把那些模糊詞再找一次。

用工具抽的字串找     命中 0
用瀏覽器算的名稱找    命中 0

結論一個字都沒變。 我的站上就是沒有「了解更多」。

變的是另一件事:換算法之前,我沒有任何理由知道那個 0 是對的。
對的答案跟可信的答案,是兩回事。

差一個空白,而規範沒說要不要補

那 35 條長這樣:

瀏覽器   2026/06/13 員工把客戶資料丟給 ChatGPT 算違法嗎?…
我的工具  2026/06/13員工把客戶資料丟給 ChatGPT 算違法嗎?…

日期跟標題在 DOM 裡是兩個並排的區塊元素。瀏覽器組名稱時中間補了一個空白,get_text(strip=True) 沒有。

我去翻了 W3C 的無障礙名稱計算規範,想知道誰對。規範沒有規定。 子節點之間要不要補空白,至今掛在一個未定案的 issue 上,備註寫著工作組「正在考慮」依 CSS display 決定。

所以「照規範實作」不會得到跟瀏覽器一樣的答案。只能實測。我另外用 Playwright 自己那套獨立的名稱計算再跑一次,結果跟 Chromium 一致 —— 兩個各自實作的東西站同一邊,落單的是我。

24 條差在 aria-label:網站做對了,工具看不見

剩下 24 條不是空白問題,是內容不同。

工具看到   預約諮詢          ×12
使用者聽到 預約諮詢 輕量諮詢方案
          預約諮詢 長期合作方案
          預約諮詢 系統開發方案
          預約諮詢

工具看到   (空字串)        ×6
使用者聽到 通過AAA無障礙網頁檢測

前一種是 aria-label 蓋掉了可見文字,18 條。後一種是名稱來自 <img alt>,6 條。

這裡要講清楚一件事。我的站在這 24 條上是做對的。 三個方案卡片的按鈕視覺上都寫「預約諮詢」四個字,但各自掛了不同的 aria-label,報讀軟體聽到的是三種不同的話。標章圖片也有 alt。

看到一排一模一樣項目的,從頭到尾只有我的工具。

而那正是審查意見當初要求的修正。 意見說這兩組同名連結要各自加 title 區分,我們加了 —— 加在 titlearia-label 上。我的規則讀 textContent,所以它看不到那個修正,它看到的還是修之前的樣子。

(那條規則本身當初也寫反了方向,Day 02 講過,這裡不重複。方向早就改對了,讀錯字串是另一件事。)

換句話說,如果我拿這份報告去跟客戶說「你的連結文字重複太多」,我講的是一個已經被修好的問題。

而且這個盲點是雙向的。反過來的情形我站上沒有,但規則一樣抓不到:一個 aria-label 寫壞的網站,視覺上寫著具體的字、實際唸出來卻是「button」,只讀 textContent 的檢查看到的是那個好看的可見文字。

送進模型的那串字,已經是錯的

這條規則最後會把字串丟給語言模型判。而丟進去的,就是上面那串抽錯的字。

模型收到     預約諮詢          ×12
使用者聽到   4 種不同的名稱

模型收到 12 份一樣的輸入,給出 12 個一樣的答案。它以為自己在判的那幾個按鈕,使用者聽起來根本不同。

換模型不會改善這件事。加範例、調溫度、換更大的參數量,都不會。失敗發生在模型上游,在那行抽字串的程式碼裡。這是我在別的專案也踩過的同一個坑:輸入錯了,模型再好都只是把錯誤答得更流暢。

工具製造了人不會犯的錯,也留下了人不會留的紀錄

誠實講反面:這 59 條,是工具自己製造出來的錯誤類別。

一個人拿螢幕報讀軟體聽過去,聽到的就是瀏覽器算好的那一句,不可能發生「少一個空白」或「看不到 aria-label」。這種錯只有寫程式抽字串的人會犯。沒有工具,就沒有這 59 條。

但反過來也成立。人聽完 117 個連結之後,手上不會有一份可以逐條比對的紀錄,他的結論是一句印象,而印象沒辦法 diff。

工具像一把刻了刻度的尺。尺本身可能不準,可是因為它有刻度,你可以拿另一把去量它,量出來的偏差是一個數字。目測沒有刻度,準或不準都說不出所以然。

而攤開兩份名單逐條對的時候,還會掉出第三種東西。

我撿到一條真的壞掉的連結。導覽列寫著「服務項目」,href/。我從方案頁點下去,落在首頁最上面,捲動位置 0,而首頁上根本沒有一個叫服務的錨點。

連結文字承諾了一個區塊,目的地是另一個地方。五條 2.4.4 規則沒有一條抓得到 —— 它們全部只判字串,沒有一條會去看目的地實際是什麼。

這條不是規則找到的,是那份紀錄自己掉出來的。 一份能被逐條比對的東西,價值不只在驗證你已經在問的問題。

修法要把抽名稱收斂成一個共用函式:aria-labelledby 優先於 aria-label,再退回內容並且用空白接起來。麻煩的地方在於有一條規則拿連結文字跨頁比對導覽列一致性,改了會動到相等判斷,得另外驗。

工具與人工稽核的能力對照圖:左側為人工稽核,聽到的是瀏覽器算好的名稱因此不會犯抽字串的錯,但結論只是印象無法逐條比對;右側為工具,會製造出人不會犯的錯誤類別但把判斷寫成可重跑可對帳的紀錄,中間標示兩者互補

人不會犯這種錯,但也不會留下能被否證的東西。

今天的重點

  • 「全過」不是結論,是一句待驗證的宣稱。 四項檢查 0 缺失的那份報告,換個算法就掉出 59 條差異。
  • 抽字串的那一行,決定了整條規則的天花板。 模型再強也救不回餵錯的輸入。
  • aria-label 會蓋掉可見文字。 只讀 textContent 的檢查看不到那一層 —— 我的工具看不見審查意見要求、而我們已經做完的那個修正。
  • 規範留白的地方要實測。 無障礙名稱要不要補空白,至今無定論,猜不如量。
  • 工具的好處是可否證,不是準確。 這是它跟人工稽核互補的地方,不是取代的理由。

明天預告

明天 Day 17:連結看完了,接下來輪到深色模式。同一份 CSS,把配色切成深色再量一次對比。同一個顏色,從合格變成不合格。


上一篇
Day 15:我幫大部分的圖決定了它不用被唸出來
系列文
前端不寫 Python,照樣 ship 一把網頁無障礙 CLI16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言