「工程師說已經部署了,可是打開網站,Title 跟 Description 還是舊的。」我們在協助客戶排查 SEO 問題時,這句話出現的頻率高得驚人。多數人的第一反應是去 Google Search Console 催索引,但這次我們實際排查一個 Next.js 加 CloudFront 架構的網站時,發現卡住更新的根本不是 Google,而是網站自己的快取。這篇把整個排查過程攤開來寫:怎麼從 Response Header 看出網站有幾層快取、x-nextjs-cache: HIT 和 s-maxage=31536000 各自代表什麼、ISR 與 TTL 差在哪,以及一套 HTML 短、靜態資源長的快取策略建議。看完之後,下次遇到「改了卻沒生效」,你會知道第一步該查什麼。

1. SEO 改了卻沒生效,先確認公開網址回傳哪一版 HTML

1.1 從資料庫到 Googlebot,中間隔了好幾層

假設 SEO 團隊今天修改了首頁的 Title、Meta Description 與 Canonical,工程師也回報部署完成,但前台看到的依然是舊資料。這時候先不要急著懷疑 Google,因為一個現代網站的資料傳遞流程,通常長這樣:資料庫或 CMS 產出內容,交給 Next.js 這類框架渲染成 HTML,再經過 CDN 分發,最後才到使用者的瀏覽器與 Googlebot 手上。

這條路上的每一層都可能有自己的快取。資料庫前面可能有 API 快取,Next.js 有自己的頁面快取,CDN 有邊緣節點快取,瀏覽器還有本機快取。任何一層還抱著舊版本,末端拿到的就是舊資料。部署成功只代表程式碼進了伺服器,不代表這條鏈路上的每一層都換成了新內容。

更麻煩的是,不同層的快取狀態可能互相矛盾。你在自己的電腦上按了強制重新整理看到新版,客戶那邊看到的卻還是舊版,因為兩邊命中的是不同節點、不同層的快取。這也是為什麼「我這邊看是好的」在快取問題上完全不能當作證據,一定要用工具直接檢查公開網址的原始回應。

1.2 先問「現在拿到的 HTML 是新是舊」,再問 Google 為什麼沒更新

所以排查的第一個問題,不是「Google 為什麼還沒重新抓取」,而是「公開網址現在實際回傳的 HTML,到底是新版還是舊版」。這兩個問題的差別很關鍵:如果公開網址回傳的還是舊 HTML,那 Googlebot 再怎麼勤勞地來抓,抓到的也是舊資料,這時候去 Search Console 按幾次「要求建立索引」都沒有意義;反過來,如果公開網址已經是新版,問題才真的落在 Google 的抓取與索引排程上。

我們的經驗是,把這個問題放在排查流程的第一步,可以省掉大量無謂的等待。很多團隊在快取還沒清掉的狀態下等了好幾天,以為是 Google 動作慢,其實網站從頭到尾就沒把新版交出去過。

2. 用 Response Header 判斷網站被誰快取

2.1 Windows 上請用 curl.exe,不是 curl

確認公開網址回傳內容最直接的方法,就是查看 HTTP Response Header。Windows 使用者這裡有個很容易踩到的坑:在部分 PowerShell 環境裡,直接輸入 curl 會被當成 Invoke-WebRequest 的別名,參數行為和一般認知的 curl 完全不同,輸出格式也不一樣。建議直接指名執行檔:

curl.exe -I "https://example.com/"

如果想用 GET 請求同時查看完整 Response Header(有些伺服器對 HEAD 請求的回應與 GET 不同),可以改用:

curl.exe -sS -D - -o NUL "https://example.com/"

這行的意思是把 Header 傾印到畫面上,回應內容丟棄,這樣看到的 Header 才是實際 GET 頁面時的狀態。

2.2 要看懂的幾個關鍵 Header

拿到 Header 之後,重點看這幾行:

Cache-Control
Age
X-Cache
Via
ETag
Last-Modified
x-nextjs-cache
X-Amz-Cf-Pop

Cache-Control 宣告這份回應可以被快取多久、被誰快取;Age 代表這份快取已經存在幾秒,數字越大代表你拿到的內容越舊;X-Cache 與 Via 會透露中間有沒有 CloudFront 這類 CDN 經手;ETag 與 Last-Modified 是內容的版本指紋,前後兩次請求若指紋相同,代表內容真的沒換過;x-nextjs-cache 則是 Next.js 自己的快取狀態。把這幾行組合起來讀,通常就能判斷網站到底是哪一層正在提供快取內容,不需要進主機翻設定檔。

舉例來說,X-Cache 顯示 Hit 而且 Age 高達幾萬秒,代表 CDN 抱著一份很舊的內容在服務;X-Cache 顯示 Miss 但內容還是舊的,就要往源站那一層追。判讀的關鍵是先弄清楚「這份回應是誰生出來的、已經放了多久」,答案幾乎都寫在這幾行 Header 裡。

3. 實際案例:一組 Header 揭露兩層快取

這次排查時,目標網站回傳了以下資訊:

x-nextjs-cache: HIT
Cache-Control: s-maxage=31536000
ETag: "13sw6lkc23w5oky"
X-Cache: Miss from cloudfront
Via: 1.1 xxxxx.cloudfront.net (CloudFront)
X-Amz-Cf-Pop: TPE53-P3

乍看之下,有人可能會覺得「CloudFront 顯示 Miss,所以不是 CDN 的問題」。但事情沒有這麼單純,這組 Header 其實同時揭露了兩層快取的存在,而且真正提供舊內容的,是比較深的那一層。

CloudFront 顯示 Miss 但 Next.js 快取 HIT,兩層快取讓舊版 HTML 繼續被回傳的流程圖

3.1 x-nextjs-cache: HIT 代表 Next.js 正在餵舊頁面

x-nextjs-cache: HIT 的意思是,這次請求到達 Next.js 時,Next.js 並沒有重新產生頁面,而是直接回傳它手上已經存在的快取版本。整個流程簡化來看是這樣:CloudFront 發現自己沒有快取,於是向後端的 Next.js 要資料;Next.js 檢查自己的頁面快取,發現有現成的,就直接把這份舊的靜態內容交出去。

這代表即使把 CloudFront 的快取全部清空,也不保證能拿到最新內容,因為下一層的 Next.js 本身仍在提供舊版本。這是很多人排查 CDN 問題時最容易忽略的一點:網站可能不只有一層快取,清了外層,內層還在。

3.2 CloudFront 顯示 Miss,不代表沒有快取問題

X-Cache: Miss from cloudfront 只能證明一件事:這一次請求,CloudFront 沒有找到現成的快取,所以回源去要了資料。它完全不能證明「整個網站沒有快取問題」。這次的案例就是最好的示範:CloudFront 是 Miss,但它回源拿到的,是 Next.js 快取裡的舊頁面,而且這份舊頁面還帶著 s-maxage=31536000 的指令,等於告訴 CloudFront「接下來這份內容你可以存很久」。舊資料就這樣一層一層地被延續下去。

所以分析快取問題時,不能只看 X-Cache 一行就下結論,要把 CDN 狀態、後端框架的快取狀態、Cache-Control 的內容三者放在一起讀,才能看出完整的因果鏈。

3.3 s-maxage=31536000 換算出來是一年

看到 Cache-Control: s-maxage=31536000,第一件事是把數字換算成人類看得懂的單位:31,536,000 秒除以 60、再除以 60、再除以 24,等於 365 天,也就是整整一年。s-maxage 是專門給共享快取(Shared Cache)看的指令,CDN、反向代理、CloudFront、Cloudflare 這類中間層都歸這個指令管。這行 Header 等於告訴所有中間的快取系統:這份回應可以存放一年。

對帶有版本雜湊的靜態檔案來說,快取一年完全合理,例如 main.a83c9201.js、style.9282ab.css 這類檔名,內容一改檔名就變,HTML 裡引用的是新檔名,舊快取自然失效,不會有拿到過期檔案的風險。但如果這個指令套在首頁 HTML、文章頁 HTML、產品頁 HTML 上,就非常危險,因為 SEO 團隊修改的 Title、Description、H1、Canonical、hreflang、結構化資料、內文與內部連結,幾乎全部都存在 HTML 裡。HTML 被快取多久,SEO 修改就可能被卡多久。

4. ISR、TTL、TLS 三個縮寫別再搞混

4.1 ISR 是頁面的更新策略

ISR 全名是 Incremental Static Regeneration,可以理解為 Next.js 的「靜態頁面定期重新生成機制」。傳統靜態網站的優勢是快,使用者請求進來,直接拿現成的 HTML 回應,不需要每次都查資料庫、跑程式、重新渲染。但缺點也很明顯:資料改了,已經生成的靜態 HTML 要怎麼更新?ISR 就是在解決這件事。在頁面裡設定:

export const revalidate = 300

概念上代表這份靜態頁面每經過 300 秒,就允許重新驗證並產生新版。所以 ISR 回答的問題是「頁面什麼時候重新生成」,它是一種更新策略,而不是快取存放時間本身。這也解釋了為什麼這次案例裡會出現 x-nextjs-cache: HIT:在重新驗證的時間窗之內,Next.js 就是會理直氣壯地回傳快取頁,這是 ISR 的設計,不是故障。問題只出在重新驗證的週期和外層 CDN 的快取時間沒有協調好,各設各的,結果就是更新被層層延遲。

4.2 TTL 是快取可以活多久

TTL 全名 Time To Live,指的是一份快取資料可以存在多久。TTL 等於 300,就是這份快取可以活 300 秒、也就是 5 分鐘,過期之後就該重新向來源要新資料。值得一提的是,TTL 不是 CDN 專有的名詞,DNS 也用 TTL:DNS TTL 設 300,代表 DNS Resolver 可以把這筆網域紀錄保留 5 分鐘。它是一個通用的「資料存活時間」概念。

把兩者放在一起看就清楚了:ISR 管的是 Next.js 如何重新產生頁面,TTL 管的是一份快取可以保存多久。兩個概念會互相配合,但不是同一件事,排查時要分開檢查各自的設定值。

4.3 TLS 與 SSL 是加密協定,跟快取無關

另一組容易混淆的縮寫是 TTL 與 TLS,兩個只差一個字母,管的事情卻完全不同:

縮寫 全名 負責的事情
TTL Time To Live 快取或 DNS 紀錄的存活時間
TLS Transport Layer Security HTTPS 的傳輸加密協定
SSL Secure Sockets Layer TLS 的前身,名稱沿用至今

大家熟悉的 TLS 1.2、TLS 1.3,處理的是 HTTPS 連線加密;TTL 設 300 處理的是資料可以快取多久。跟工程師溝通時把這三個縮寫用對,可以省掉很多雞同鴨講的來回。

5. SEO 網站的快取策略:HTML 短、靜態資源長

5.1 各類資源的建議快取時間

HTML 快取到底該設多久,沒有適用所有網站的唯一答案,但對需要長期經營 SEO、頻繁調整內容與 Landing Page 的網站來說,我們不建議把 HTML 快取設成數天甚至一年。一個務實的起點是 HTML 快取 300 秒,也就是 5 分鐘:

Cache-Control: public, s-maxage=300

各類資源可以這樣區分:

資源 建議快取策略
首頁 HTML 約 300 秒
SEO Landing Page 約 300 秒
文章頁 HTML 300 至 900 秒
robots.txt 60 至 300 秒
sitemap.xml 300 至 900 秒
帶雜湊的 CSS / JS 可長效快取
圖片 可長效快取

重點不是「所有東西快取越短越好」,而是分層對待:HTML 承載所有 SEO 元素,要短;帶版本雜湊的靜態資源改了就換檔名,可以放心存一年。robots.txt 與 sitemap.xml 也值得單獨拉出來設定,因為它們是搜尋引擎理解網站的入口檔案,改了抓取規則或新增了頁面,若被快取卡住幾天,影響的是整站的抓取行為,而不只是單一頁面。

5.2 搭配 stale-while-revalidate 兼顧速度與新鮮度

如果擔心 300 秒的快取過期瞬間會有大量請求打到源站,可以再加上 stale-while-revalidate:

Cache-Control: public, s-maxage=300, stale-while-revalidate=60

意思是快取過期後的 60 秒內,CDN 可以先把舊版本回給使用者,同時在背景向源站要新版。使用者不用等待重新生成,源站負載平穩,內容也能在幾分鐘內完成更新,等於同時照顧了 CDN 效能、頁面更新速度、源站負載與 SEO 修改的生效效率。對流量大的網站來說,這個指令的價值在於把「快取過期」從一個瞬間的懸崖,變成一段平滑的過渡,源站不會在整點被過期請求打爆,使用者也感覺不到重新生成的等待。

5.3 為什麼不建議直接把 CDN 關掉

遇到快取問題,最直覺的解法可能是「那 HTML 全部不要快取不就好了」,技術上把 TTL 設 0 確實做得到,但通常不是最佳解。這等於為了解決一次更新延遲,放棄了 CDN 帶來的所有效能紅利,對每天被存取數萬次、內容卻只改幾次的頁面來說,是非常不划算的交換。CDN 能減少源站負載、降低 TTFB、提升不同地區使用者的存取速度,也撐起流量尖峰的承載能力,這些對使用者體驗與 Core Web Vitals 都有實際幫助。真正該改善的是快取策略,而不是把快取整個拿掉:HTML 給 300 秒,靜態資源給長效快取,重要更新走主動清除,會比完全不快取合理得多。

6. 比等 TTL 過期更成熟的做法:主動更新快取

6.1 Revalidation 加 Invalidation,改完立刻生效

就算 HTML 快取縮到 300 秒,重要頁面改完還要等 5 分鐘才看得到新版,仍然不夠理想。更成熟的部署流程是把「清快取」納入上線動作:內容修改、部署完成之後,先觸發 Next.js 的重新驗證,再對 CDN 執行清除,讓下一個請求直接拿到新版 HTML。Next.js 可以針對特定路徑重新驗證,概念類似:

revalidatePath('/zh-tw/')

CloudFront 則可以針對特定 URL 執行 Invalidation,例如只清 /zh-tw/、/zh-tw/product/、/zh-tw/blog/example/ 這幾條路徑,而不是每次都把 /* 全站清空。範圍越精準,清除速度越快,也越不會把其他頁面的快取效益一起犧牲掉。全站清空留給大改版這種真的動到所有頁面的場合,日常內容修改用路徑級的清除就夠了。

理想狀態下,這兩個動作應該自動化:CMS 儲存文章或部署流程跑完時,自動觸發對應路徑的重新驗證與清除,SEO 團隊不需要每次都去拜託工程師手動清快取。把清快取做成流程的一部分,而不是靠人記得,更新才不會三不五時漏掉。

6.2 一套實際可用的 Next.js 加 CloudFront 設定

把上面的原則組起來,一套適合長期經營 SEO 的網站的參考設定是:Next.js 頁面設 revalidate = 300;HTML 的回應標頭設 Cache-Control: public, s-maxage=300, stale-while-revalidate=60;CloudFront 的 Minimum TTL 設 0、Default TTL 設 300、Maximum TTL 設 3600;部署流程加上 Next.js Revalidation 與 CloudFront Invalidation 兩個步驟。CSS、JS、字型、圖片這類帶雜湊的資源,則繼續採用數月甚至一年的長效快取。這樣網站效能與 SEO 更新效率可以同時成立,不必二選一。

7. 把「上線」拆成階段,一路驗證到 Googlebot

7.1 工程師說的上線,跟搜尋引擎拿到新版是兩回事

這次排查最大的收穫,其實不是學會一個 s-maxage 指令,而是確認了一件事:工程師說「已經上線」,不代表搜尋引擎現在拿到的就是新版。完整的 SEO 上線流程應該拆成好幾個階段來驗證:程式碼部署完成、源站內容更新、Next.js 快取更新、CDN 快取更新、公開 URL 回傳新版、Googlebot 能取得新版,最後才是 Google 重新索引。SEO 團隊如果只確認到「部署成功」這一步,離「搜尋引擎看到新版」其實還有好幾層距離,中間任何一層卡住,後面全部不會發生。

我們的建議是把這條鏈寫進團隊的驗收清單:每次重要的 SEO 修改上線,負責的人要能回答「源站是新版嗎、CDN 是新版嗎、公開網址是新版嗎」這三個問題,而不是只回報「已部署」。這份清單同時也是 SEO 團隊與工程團隊的共同語言,出問題時可以立刻定位卡在哪一層,而不是兩邊互相猜測、彼此等待。

7.2 三個步驟驗證 SEO 修改真的上線了

驗證可以照這個順序做。第一步,直接抓公開網址的 HTML:

curl.exe -sS "https://example.com/"

確認 <title>、<meta name="description">、<link rel="canonical"> 是不是最新版本。第二步,查看 Response Header:

curl.exe -sS -D - -o NUL "https://example.com/"

確認 x-nextjs-cache、Cache-Control、X-Cache、Age 的狀態,判斷回應是即時生成還是來自快取。第三步,進 Google Search Console 的網址審查,用「測試實際網址」查看 Google 即時抓到的 HTML。如果連即時測試拿到的都是新版,才能確認網站端的部署與快取問題已經處理完畢;至於搜尋結果頁什麼時候更新,屬於 Google 抓取、索引與排序的下一個階段,那才是需要等待的部分。

8. 常見問題

8.1 已經清了 CloudFront 快取,為什麼頁面還是舊的?

很可能是源站後面還有一層快取。以 Next.js 為例,CloudFront 清空後回源要資料,Next.js 若命中自己的頁面快取(x-nextjs-cache: HIT),交出去的仍是舊 HTML,CloudFront 再把這份舊資料存起來。正確做法是先讓 Next.js 重新驗證頁面,再清 CDN,順序反了就會白清一次。

8.2 s-maxage 和 max-age 有什麼不同?

max-age 對所有快取生效,包含使用者的瀏覽器;s-maxage 只對共享快取生效,例如 CDN 與反向代理,而且優先權高於 max-age。實務上常見的組合是給瀏覽器較短的 max-age、給 CDN 稍長的 s-maxage,讓邊緣節點多扛流量,同時保留對使用者端新鮮度的控制。

8.3 ISR 的 revalidate 設多少比較合適?

取決於內容的更新頻率與重要性。經常調整的首頁與 Landing Page 適合 300 秒左右;更新頻率低的文章頁可以放寬到 900 秒甚至更長。原則是拿「內容多久會改一次」與「改了之後多快需要生效」來權衡,而不是全站套同一個數字。

8.4 直接把 HTML 的 TTL 設成 0,全部不快取可以嗎?

技術上可以,但代價是每個請求都回源,TTFB 變慢、源站負載上升,流量尖峰時風險更大。多數情況下,300 秒的短快取加上部署時主動清除,就能讓更新幾乎即時生效,同時保留 CDN 的效能優勢,比完全關閉快取划算。

8.5 怎麼確認 Googlebot 拿到的已經是新版 HTML?

用 Google Search Console 的網址審查功能,對目標頁面執行「測試實際網址」,再查看測試結果裡的 HTML。這個測試是 Google 當下即時抓取的結果,不受舊索引影響。如果這裡看到的是新版,網站端就沒問題了;如果還是舊版,代表某一層快取仍在提供舊內容,要回頭檢查 Header。

8.6 為什麼我在 PowerShell 用 curl 看到的結果怪怪的?

因為部分 PowerShell 環境把 curl 設為 Invoke-WebRequest 的別名,參數與輸出行為都不同,-I 這類參數甚至會直接報錯。改用 curl.exe 明確指定執行檔,就能得到一般認知的 curl 行為,這是 Windows 環境做網站排查時很常見的小坑。

8.7 網站端都確認是新版了,搜尋結果多久會更新?

這就進入 Google 的抓取與索引排程,沒有保證時間。可以在 Search Console 對重要頁面提出索引要求加速處理,但實際反映到搜尋結果可能需要數天到數週。重點是先把網站端的快取問題排除,讓 Googlebot 每次來都拿得到新版,剩下的時間成本才是純粹等待 Google,而不是自己的網站在扯後腿。

回頭看,這次整個問題的起點只是一行 Cache-Control: s-maxage=31536000,一路追下去卻揭露了 Next.js 快取命中、CDN 快取層、HTML 被宣告可存放一年這一整條鏈。SEO 從來不只是關鍵字、文章與 Sitemap,當網站用上 SSR、SSG、ISR 與 CDN 之後,「Google 到底拿到哪一版 HTML」本身就是 SEO 的基礎工程問題。所以比起問「Google 為什麼還沒更新」,更好的第一個問題永遠是:現在從公開網址拿到的 HTML,真的已經是最新版了嗎?