你在瀏覽器打開文章時,需要導覽列、字型與互動元件;AI Agent 抓同一頁時,真正需要的通常是標題、段落、程式碼與連結。Accept Header Markdown 的做法,就是讓同一個網址依請求偏好回傳 HTML 或乾淨 Markdown,而不是維護兩套內容。
這個問題在 2026 年 8 月 26 日登上 Hacker News;截至 8 月 28 日,該討論串顯示 172 points、103 comments。熱度背後真正難的不是「把 HTML 轉成 Markdown」,而是 q-value、406、Vary: Accept 與 CDN cache key 要一起正確。
這篇專為第一次碰 HTTP 協定與網站快取的讀者寫:你會先理解一個白話模型,再完成 WordPress、Cloudflare 原生功能或 Worker 三條路線,最後用六組 curl 驗收。看完不只知道怎麼開功能,也知道怎麼避免「瀏覽器突然收到 Markdown」這種快取事故。
先說結論:同一門牌,兩份餐點
一句話記住:同一網址 + Accept 偏好 = HTML/Markdown 兩種表示。
把網址想成餐廳門牌,Accept 是客人點餐,Content-Type 是廚房實際端出的餐點,Vary: Accept 則是貼給快取看的分流牌。門牌不變,內容的「表示方式」可以不同;文章本身仍只有一份。
- 公開網址直接由 WordPress render:可評估 Roots 外掛,但 v1.7.1 要先加本文的內容保護閘門。
- 網站已在 Cloudflare 且方案符合:先用 Markdown for Agents,最少改動。
- 你已有靜態
.md檔、要自訂嚴格度:再選 Cloudflare Worker。
三條路只選一個主要 Markdown 產生器。若 WordPress 與 Cloudflare 同時各自轉換,除錯時很難判斷 body、header 與 token 計數到底出自哪一層。若 WordPress 只是後台、公開網址由 Next.js 等 headless frontend 回應,裝在 CMS 的外掛不會改變公開網址;協商必須做在 frontend 或 edge。
Accept Header Markdown 是什麼?先把協商說白
RFC 7763 登記了 text/markdown 媒體類型;RFC 9110 則定義 HTTP 內容協商。Agent 可以這樣說:「我最想要 Markdown,也能退回 HTML。」
Accept: text/markdown;q=1, text/html;q=0.5
q 是 0 到 1 的偏好權重;省略時預設為 1,q=0 代表該表示不可接受。伺服器選好後,回應應用 Content-Type 告訴客戶實際格式:
Content-Type: text/markdown; charset=utf-8 Vary: Accept

q-value 的完整走法
假設客戶送出:
Accept: text/markdown;q=0, text/*;q=0.8, text/html;q=1
text/html有精確規則,品質為 1。text/markdown也有更精確規則,品質為 0;不能拿較寬鬆的text/*;q=0.8蓋過它。- 因此應回 HTML。若 HTML 與 Markdown 都被設為
q=0,伺服器可回406 Not Acceptable;RFC 9110 也允許伺服器忽略偏好、回預設表示。
白話說:先看哪條規則最精確,再看那條規則的 q-value。正式環境不要只用「字串裡有沒有 text/markdown」判斷,否則 q=0、萬用字元與多值 header 都會出錯。
為什麼不能漏掉 Vary 與 CDN cache key?
如果 CDN 只把 URL 當成鑰匙,第一個 Agent 把 Markdown 存進快取,下一個瀏覽器可能就會拿到同一份 Markdown。RFC 9111 規定快取重用含 Vary 的回應時,要比對被點名的請求 header;所以協商端至少要在 HTML 與 Markdown 回應都送出 Vary: Accept。
但 Vary 是分流訊號,不等於每個 CDN 都會自動把所有任意 header 放進 cache key。以 Cloudflare 為例,目前的 Vary 文件要求來源回應先包含 Vary,再透過 Cache Rules Vary 或 Worker 的 cf.vary 指定如何處理。只協商 text/html 與 text/markdown 時,normalize 能避免每種無關的 header 排列都製造新變體;若你還依賴 variant 等媒體參數,Cloudflare 的正規化會移除參數,應改用 passthrough 或重新設計 cache 維度。
cf: {
vary: {
default: { action: "bypass" },
headers: {
accept: {
action: "normalize",
media_types: ["text/html", "text/markdown"]
}
}
}
}
這是 Worker 子請求的 cf.vary 片段;來源仍要回 Vary: Accept。修改 Vary 設定後,Cloudflare 文件也明確指出既有快取不會跟著自動清除,因此上線切換時要為受影響 URL 做一次精準 purge,再重新暖兩個變體。
路線一:Accept Header Markdown 用在 WordPress
如果公開文章直接由 WordPress render,Roots 的開源 Post Content to Markdown 可處理 Accept、.md 後綴與 ?format=markdown。截至 2026 年 8 月 28 日,GitHub 最新 release 為 1.7.1,外掛標示需要 PHP 8.1 以上。
先說安全結論:我們檢查 v1.7.1 原始碼時,沒有看到 post_password_required() 的明確檢查;單篇路徑直接讀取 $post->post_content,Markdown feed 也另有自己的輸出流程。因此密碼保護或非公開內容網站,不應原樣啟用。下面先把單篇範圍鎖成「已公開且沒有文章密碼」,並暫停 Markdown feed。
步驟 1:安裝並只開在測試環境
composer require roots/post-content-to-markdown
沒有 Composer 的網站,可從 GitHub Releases 下載 zip,放到 wp-content/plugins/post-content-to-markdown/ 後啟用。先在 staging 跑完本文六組驗收,再推到正式站。
步驟 2:先加內容保護閘門
把以下內容放進自己的 mu-plugin;它利用外掛公開的 post_allowed filter 鎖住單篇文章,並在更早的 template_redirect 優先序擋掉所有 Markdown feed。這是針對目前 v1.7.1 的 fail-closed 配置,升級外掛後要重新做整合測試。
<?php
add_filter(
'post_content_to_markdown/post_allowed',
function ($allowed, $post) {
return $allowed
&& $post->post_status === 'publish'
&& empty($post->post_password);
},
10,
2
);
add_filter(
'post_content_to_markdown/feed_post_types',
'__return_empty_array'
);
add_action('template_redirect', function () {
if (! is_feed()) {
return;
}
$accept = strtolower($_SERVER['HTTP_ACCEPT'] ?? '');
$is_markdown_feed = get_query_var('feed') === 'markdown'
|| str_contains($accept, 'text/markdown');
if (! $is_markdown_feed) {
return;
}
status_header(406);
header('Content-Type: text/plain; charset=utf-8');
header('Cache-Control: no-store');
header('Vary: Accept', false);
exit("Markdown feeds disabled\n");
}, 0);
這段對 feed 採保守策略:只要 header 提到 Markdown 就擋,包括 q=0。它犧牲少量相容性來避免 feed 走進未套用單篇 post_allowed 的路徑。若業務真的需要 Markdown feed,應另寫逐篇權限檢查與密碼文章整合測試,再移除此擋板。
步驟 3:先驗證三個入口
curl -sS -D - -o /dev/null \ -H 'Accept: text/markdown' https://example.com/post-slug/ curl -sS -D - -o /dev/null \ https://example.com/post-slug.md curl -sS -D - -o /dev/null \ 'https://example.com/post-slug/?format=markdown'
第一個是本文主角;後兩個是容易分享與除錯的顯式入口。外掛目前會為 Markdown 回應送出 Content-Type: text/markdown、Vary: Accept 與 X-Markdown-Source,也會在 HTML 回應宣告 .md alternate。這讓你能從存取紀錄分辨請求是從 header、後綴或 query 進來。
步驟 4:決定 406 策略
外掛預設為 strict:若客戶明確排除 HTML 與 Markdown,就回 406。如果你的舊爬蟲把奇怪的 Accept 當慣例、你想保留 HTML fallback,可以用官方 README 提供的 filter:
add_filter( 'post_content_to_markdown/strict_accept', '__return_false' );
這不是「哪個比較符合規範」的選擇,而是 API 契約:strict 適合早點暴露錯誤,fallback 適合相容性優先。選定後把它寫進測試,別讓環境之間漂移。
步驟 5:分清 WordPress page cache 與 CDN cache
此外掛預設對 Markdown 請求設定 DONOTCACHEPAGE,降低 WordPress 頁面快取交叉送錯格式的風險。這不會替外層 CDN 完成 cache-key 設定;若前面還有 Cloudflare、Fastly 或其他反向代理,仍要逐層驗證。
路線二:在 Cloudflare 直接產生 Markdown
A. 最少改動:Markdown for Agents
Cloudflare 的 Markdown for Agents 目前標示為 Beta,可在邊緣把 HTML 轉成 Markdown。到 Dashboard 的 AI Crawl Control → Markdown for Agents 開啟後,帶有 Accept: text/markdown 的合格請求會收到 Markdown;也能用 Configuration Rule 只套到特定 hostname 或 path。
截至 2026 年 8 月 28 日,官方頁面列出的可用範圍是 Pro、Business、Enterprise 與 SSL for SaaS;輸入是 HTML,來源回應上限 2 MB。轉換回應會包含 x-markdown-tokens 與 x-original-tokens 估計值,並調整 Content-Type、Content-Length、Vary 等 header。這條路適合「來源不想改、先在 edge 做轉換」的網站。
官方文件保證的是「轉換後回應」會讓 Vary 包含 Accept,沒有把完整 q-value tie-break、406 或所有 HTML fallback header 寫成介面契約。因此仍要在自己的 zone 跑本文六組測試,並確認 HTML 與 Markdown 兩邊都具備正確分流訊號;不要把 Cloudflare 文件頁今天的觀察行為當成永久保證。
若來源有內容使用政策,還要檢查 Content-Signal 是否如預期保留。Cloudflare 文件說明:來源未提供時,產生的 Markdown 會帶入其預設的 yes 值;這屬於內容授權訊號,不是格式轉換細節。此外,轉換會移除導覽、頁首、頁尾、script 與 style;若重要歸因、授權文字或產品條件放在這些區域,必須把它們列入內容完整度驗收。
B. 控制力優先:Worker + 靜態 .md 兄弟檔
若你的 build pipeline 能為每頁產生 index.html 與 index.md,Worker 只需負責協商與取檔,不必在每次請求重新轉換。Accept Markdown 的 Worker recipe 可當成靜態資產 binding、406 與 alternate link 的部署骨架;其中 parser 適合示範常見 header,不是完整 RFC parser。若你允許帶引號的參數、variant 或更複雜的 media range,應換成經測試的解析器。核心設定是讓 Worker 先接住請求:
[assets] directory = "./public" binding = "ASSETS" run_worker_first = ["*"]
- 解析
Accept,以精確度與 q-value 算出 HTML/Markdown 偏好。 - 選 Markdown 時,把
/guide/對應到 build 產生的/guide/index.md。 - 回應
text/markdown; charset=utf-8與Vary: Accept。 - 用 Cache Rules Vary 或
cf.vary把兩種表示分開快取。
不要把 parser 縮成一行 accept.includes('text/markdown');那會把 q=0 當成同意,也忽略 HTML 的較高偏好。最少要把本文六組案例、精確拒絕覆蓋 wildcard,以及你允許的參數語法寫成自動測試。想理解 Agent 如何在這類伺服器端流程裡拿工具與資料,可以先讀 AI Agent Harness 是什麼,再接著看 如何實作 Agent Harness。
六組 curl:上線前一次驗完
把 URL 換成同一篇公開文章。每組都同時看 status、Content-Type、Vary 與 body;只看 HTTP 200 不算通過。
URL='https://example.com/post-slug/' # 1. 明確要 Markdown curl -sS -D - -o /tmp/md.body \ -H 'Accept: text/markdown' "$URL" # 2. 明確要 HTML curl -sS -D - -o /tmp/html.body \ -H 'Accept: text/html' "$URL" # 3. HTML 權重較高 curl -sS -D - -o /dev/null \ -H 'Accept: text/markdown;q=0.2, text/html;q=0.9' "$URL" # 4. Markdown 被明確拒絕 curl -sS -D - -o /dev/null \ -H 'Accept: text/markdown;q=0, text/html;q=1' "$URL" # 5. 精確拒絕要蓋過較寬的 wildcard:不得回 Markdown curl -sS -D - -o /dev/null \ -H 'Accept: text/markdown;q=0, text/*;q=1' "$URL" # 6. 兩種表示都被拒絕:依你的契約驗 406 或 fallback curl -sS -D - -o /dev/null \ -H 'Accept: text/markdown;q=0, text/html;q=0' "$URL"
再做最重要的交錯測試:連續送「Markdown → HTML → Markdown」,每次記錄 Age、CF-Cache-Status 或你的 CDN cache header,確認命中後仍各自得到正確格式。單次 MISS 正確,不代表 HIT 也正確。
量測:bytes、token 與引用完整度怎麼看?
我們在 2026 年 8 月 28 日保留了一份可重跑的單頁量測:同一個 Cloudflare 官方 Markdown for Agents 文件 URL,使用 GET 與 curl --compressed,將解壓後 body 寫入檔案再以 wc -c 計算。

- 解壓後 body:HTML 186,385 bytes;Markdown 17,190 bytes,這一頁減少 90.8%。
- Cloudflare header 估計:
x-original-tokens: 46485;x-markdown-tokens: 4289。 - 內容連結抽查:預先選定內文中的 MDN content negotiation、Cloudflare 公告、Content Signals 與 Workers AI 轉換說明,Markdown 保留 4/4。
這組數字只描述該 URL、該日回應與該量測方法,不是跨網站 benchmark。真正要追的是三個分開的指標:
- 傳輸 bytes:用相同 URL、method、壓縮設定與時間窗比較。
- 模型輸入 token:固定 tokenizer 或記錄供應商 header;不要用「字元除以四」估中文。
- 內容完整度:先選標題、程式碼、圖片 alt、JSON-LD 或幾條重要連結,再逐項比對;總連結數下降可能只是導覽列被正確移除。
這和 Claude Code 讀 HTML 還是 Markdown 的檔案格式比較不同:本篇處理的是 HTTP 傳輸層。若你想把「內容有沒有真的更常被引用」也納入驗收,可接著用 GEO AI 引用 A/B Test 的方法做獨立實驗,不要把 payload 變小直接等同於排名提升。
最常踩的 7 個坑
- 只在 Markdown 回應加 Vary:HTML 先被快取時仍可能交叉污染;兩邊都要帶。
- 把 Vary 當成 CDN 設定本身:逐層確認 cache key,而不是只看 origin header。
- 用 includes 解析 Accept:
q=0、萬用字元與較高 HTML 偏好都會判錯。 - 同時開兩個轉換器:先選 WordPress、Cloudflare managed 或 Worker 其中一層當 owner。
- 只測 MISS:一定要交錯測到 CDN HIT,才能抓出變體共用。
- 只數 token:若標題層級、code fence、重要連結或結構化資料遺失,body 再小也不合格。
- 假設每個 Agent 都會送同一種 header:截至 2026 年 8 月,Accept Markdown 專案的觀察矩陣顯示不同客戶端行為不一;把自己網站的 access log 當成採用依據。
FAQ:Accept Header Markdown 常見問題
1. 同一網址回兩種內容,會產生重複內容嗎?
不是兩篇文章。這是同一資源的兩種表示。若另外提供 .md URL,應保留 HTML canonical,並像 Roots 外掛那樣為 Markdown alias 設定適當的 robots 與 alternate 關係。
2. 一定要回 406 嗎?
不一定。RFC 9110 允許伺服器回 406,也允許忽略偏好並回預設表示。重點是把策略寫進契約與測試,不要讓不同環境隨機選擇。
3. 有 Vary: Accept 就完成了嗎?
還差 CDN 驗收。Vary 是必要訊號;你的 CDN 是否把它納入 cache key、如何 normalize,仍要看產品設定並用 HIT 交錯測試證明。
4. WordPress 外掛和 Cloudflare 可以一起開嗎?
技術上可串接,營運上先指定唯一 owner。若 WordPress 已回 Markdown,就讓 Cloudflare只做傳遞與正確快取;若 Cloudflare負責轉換,來源保持 HTML。這樣 header、body 與錯誤都能追到一層。
5. Markdown 一定比較省 token 嗎?
要量。移除導覽、樣式與腳本通常會縮小輸入,但比例取決於頁面、轉換器與 tokenizer。本篇 90.8% 是一頁的 dated snapshot,不是通用係數。
6. 這會直接提升 AI 引用或 SEO 嗎?
不能從格式直接推論。它改善的是內容取得介面;抓取成功率、引用完整度、搜尋表現與商業結果要各自設計觀測或實驗。
7. 為什麼不用獨立的 /article.md?
兩種都能用。同 URL 協商讓 canonical 與內容身分一致;.md 兄弟 URL 比較容易分享、除錯與使用一般 URL cache。Roots 外掛同時提供兩者,Worker 也可用 alternate link 宣告。
8. 怎麼知道真的有 Agent 在用?
看伺服器紀錄,不看想像。記錄協商結果、Accept 正規化後的類別、status、cache status、bytes 與 user agent;若用 Roots 外掛,也可記錄 X-Markdown-Source。先建立基準,再決定是否擴大覆蓋。
給新手的 5 個上線重點
- 先選唯一 Markdown 產生層,不要讓 WordPress 與 edge 搶 owner。
- 用經測試、符合你接受語法範圍的 parser;
q=0必須真的代表拒絕。 - HTML、Markdown 都送
Vary: Accept,並配置 CDN cache key。 - 六組 curl 加上 HIT 交錯測試,一起驗 status、header、body。
- bytes、token、內容完整度分開量;不把單頁結果外推成排名或成本承諾。
如果這是你第一次做 Agent 基礎設施,推薦先補上 Claude 省 token 的系統方法,再比較 Claude Code 與 Codex 的工作流。想把零散技巧整理成完整能力,也可以從 AlphaLab 的 AI 課程繼續學。
接著閱讀
左右滑動查看更多推薦
結語:先讓一個 URL 正確,再擴到全站
回到本文的記憶句:同一網址 + Accept 偏好 = HTML/Markdown 兩種表示。真正把它變成可營運功能的,是後半句:Content-Type 說明端了什麼,Vary 與 cache key 確保別送錯桌。
現在就挑一篇 staging 文章,只開一個 Markdown 產生器,跑完六組 curl 與 HIT 交錯測試。header、body、快取三關都過,再逐步擴到整站;這才是 Agent 友善網站最小、可回滾的起點。






