純 CSR 的 SEO 問題:爬蟲為什麼看到空白的 div
純用戶端渲染(CSR)的頁面,伺服器回傳的 HTML 長這樣:
<!DOCTYPE html>
<html lang="zh-Hant-TW">
<head>
<title>My App</title>
<script type="module" src="/assets/index.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>一個空的 <div>。 內容要等 JavaScript 下載、執行、取資料、渲染之後才出現。
這一頁在 索引流程 的第三關(被渲染)就可能卡住。
Google 的兩階段渲染
它會渲染,但分成兩階段
| 階段 | 做什麼 | 時間 |
|---|---|---|
| 1. 抓取 | 下載 HTML,解析裡面的連結與標籤 | 立即 |
| 2. 渲染 | 排進佇列,用瀏覽器引擎執行 JavaScript | 通常幾秒,但沒有保證 |
第一階段看到的就是那個空的 <div>。內容要等第二階段才進得了索引。
舊資料常說第二階段要等好幾天,那是 2018 年的狀況,官方後來已經澄清不再適用。但「沒有保證」這件事沒有變,排隊的頁面一多就會等。
第二階段有三個風險
| 風險 | 後果 |
|---|---|
| 佇列延遲 | 輪到之前新內容都不會被收錄 |
| 程式碼出錯就放棄 | 任何未捕捉的錯誤都可能讓整頁渲染失敗 |
| 資源被封鎖 | robots.txt 擋掉 JS 或 API 就渲染不出來 |
第二個風險特別隱形,在您的瀏覽器上正常,在爬蟲的環境(不同的引擎版本、沒有 cookie、可能沒有某些 API)可能就掛了。
社群爬蟲完全不執行 JavaScript
這是比搜尋引擎更嚴重的問題
Google 至少會排隊渲染,但 Facebook、X、LINE、Slack、Discord 的預覽爬蟲 完全不執行 JavaScript。
它們只讀伺服器回傳的原始 HTML。所以:
- 靠程式碼注入的 Open Graph 標籤 → 無效
- 靠程式碼注入的 Twitter Card → 無效
- 分享出去的連結 → 預覽卡片一片空白
而且這個問題 沒有「等一下就會好」的可能,它們永遠不會執行您的程式碼。
六個連帶問題
內容看不到只是最明顯的一個。純 CSR 還會帶來這些:
| 問題 | 說明 |
|---|---|
| 軟性 404 | 狀態碼永遠是 200,不存在的頁面被判定為 軟性 404 |
| 檔頭標籤無效 | 社群爬蟲讀不到程式碼注入的 meta 標籤 |
| 結構化資料可能漏掉 | JSON-LD 動態注入的風險 |
| 爬蟲走不到連結 | 路由用 <button> 實作時,爬蟲追不到 |
| LCP 變差 | 內容要等程式碼執行完,LCP 自然晚 |
| 分頁與無限滾動失效 | 爬蟲不捲動 ,只看到第一批 |
軟性 404 最容易忽略
純 CSR 的專案幾乎必然有這個問題:
// 找不到路由時顯示 404 元件,但 HTTP 狀態碼仍是 200
{
path: '/:pathMatch(.*)*',
component: NotFoundPage,
}狀態碼在伺服器就決定了,前端路由改不了它。搜尋引擎看到「頁面說找不到、但回傳 200」的矛盾,就標記為軟性 404。
怎麼驗證
第一步:檢視網頁原始碼
不要用開發者工具的元素面板
| 工具 | 顯示什麼 |
|---|---|
| 開發者工具的 Elements | 執行完程式碼之後 的 DOM,永遠看得到內容 |
檢視網頁原始碼(Ctrl + U) | 伺服器實際回傳 的 HTML |
用 Elements 面板永遠驗不出這個問題。
在原始碼裡搜尋這幾樣,找不到就是有問題:
| 搜尋 | 確認什麼 |
|---|---|
| 文章標題的一段文字 | 內容在不在 HTML 裡 |
og:image | 社群標籤在不在 |
application/ld+json | 結構化資料在不在 |
<a href | 導覽連結是不是真的 <a> |
第二步:用命令列確認
# 看伺服器實際回傳的內容
curl -s https://example.com/some-page | head -50
# 確認社群標籤在不在
curl -s https://example.com/some-page | grep -o 'og:[a-z]*'
# 模擬社群爬蟲(它們不執行 JS,所以結果就是這樣)
curl -s -A "facebookexternalhit/1.1" https://example.com/some-page | grep 'og:title'這是最貼近社群爬蟲的測試
curl 不執行 JavaScript,它看到的就是社群爬蟲看到的。抓不到 og:title 就代表分享出去的卡片會是空的。
第三步:網址檢查工具
Google Search Console 的網址檢查會顯示 搜尋引擎渲染後 的 HTML 與截圖:
| 看什麼 | 判斷 |
|---|---|
| 渲染後的 HTML | 內容有沒有出現 |
| 截圖 | 版面對不對 |
| 頁面資源 | 有沒有資源載入失敗 |
| JavaScript 主控台訊息 | 有沒有程式碼錯誤 |
這一步能抓到「本機正常、爬蟲環境出錯」
主控台訊息那一區特別有用,它會顯示爬蟲執行您的程式碼時發生的錯誤。那些錯誤在您的瀏覽器上可能不會出現。
解決方向
按成本從低到高:
| 方案 | 成本 | 適合 |
|---|---|---|
| 預渲染 | 低 | 頁面數不多、內容不常變 |
| 靜態產生(SSG) | 中 | 內容型網站 |
| 伺服器端渲染(SSR) | 高 | 內容需要即時 |
| 增量靜態再生(ISR) | 中高 | 電商、大量頁面 |
各模式的取捨見 渲染模式怎麼選 。
最小的改動:只對需要的頁面做預渲染
不必整站改造
先盤點:哪些頁面真的需要被搜尋引擎收錄?
| 需要 | 不需要 |
|---|---|
| 首頁 | 登入後的操作介面 |
| 產品或內容頁 | 後台管理 |
| 部落格文章 | 使用者的個人設定 |
| 關於、聯絡頁 | 購物車與結帳流程 |
通常只有前面幾頁需要。針對那少數路由做預渲染,其餘維持原狀。
預渲染在 建置時 把指定路由跑成靜態 HTML:
// vite.config.js(概念示意)
export default {
plugins: [
prerender({
routes: ['/', '/about', '/products', '/blog'],
}),
],
};預渲染的兩個限制
- 路由必須事先列舉:動態產生的網址(
/products/:id)無法涵蓋,除非在建置時就知道所有 id。 - 要確認產出真的有內容:有些設定會漏掉,產出的還是空容器加程式碼。檢視原始碼確認。
過渡期的補救
還沒改成 SSR 或 SSG 之前,這幾件事能減輕問題:
| 補救 | 解決什麼 |
|---|---|
| 在 HTML 裡放上基本的 meta 標籤 | 社群卡片至少有標題與描述 |
導覽連結改用 <a href> | 爬蟲走得到 |
| 伺服器對不存在的路徑回傳真正的 404 | 避免軟性 404 |
在 <div id="app"> 裡放骨架內容 | 第一階段至少看到一點文字 |
<!-- 在容器裡放基本內容,程式碼執行後會被取代 -->
<div id="app">
<h1>網站名稱</h1>
<p>網站的一段簡介,讓第一階段抓取至少有內容可讀。</p>
</div>怎麼確認自己的站沒有這個問題
靜態產生的網站在建置時就把每一頁跑成完整的 HTML,所以天生沒有這個問題。建置完直接檢查產出的檔案:
npm run build
# 產出 dist/seo/rendering-csr-problem.html 等靜態檔案檢視原始碼時,這些東西都應該在:
| 項目 | 在原始碼裡 |
|---|---|
| 文章內容 | 是 |
<title> 與 description | 是 |
| Open Graph 標籤 | 是 |
ld+json 結構化資料 | 是 |
導覽與側邊欄的 <a href> | 是 |
這類架構同時也在瀏覽器端接手路由,所以頁面切換仍然是即時的,靜態產生加上用戶端接手,兼得兩者的優點。
驗證方式很簡單:
# 建置後直接檢查產出的 HTML
grep -c 'og:title' dist/seo/rendering-csr-problem.html
grep -o '<title>[^<]*' dist/seo/rendering-csr-problem.html檢查清單
| 項目 | 標準 |
|---|---|
| 檢視原始碼看得到主要內容 | 必備 |
原始碼裡有 <title> 與 description | 必備 |
| 原始碼裡有 Open Graph 標籤 | 必備 |
| 原始碼裡有結構化資料(若有標記) | 必備 |
導覽連結是真的 <a href> | 必備 |
| 不存在的路徑回傳真正的 404 | 必備 |
用 curl 測過社群爬蟲會看到什麼 | 建議 |
| 用網址檢查工具看過渲染結果與主控台訊息 | 建議 |
| 需要被收錄的頁面已預渲染或 SSR | 必備 |
常見問題
Google 會執行 JavaScript,那到底有什麼問題?
會,但那是排在佇列裡的第二階段,輪到之前內容都不算數,而且程式碼出錯就直接放棄。更關鍵的是社群平台的爬蟲完全不執行 JavaScript,分享連結時只會抓到空容器,預覽卡片一片空白。
怎麼確認爬蟲看到什麼?
用瀏覽器的檢視網頁原始碼功能,不要用開發者工具的元素面板,後者顯示的是執行完程式碼的結果,永遠看得到內容。原始碼是伺服器實際回傳的內容,也是社群爬蟲看到的東西。
只有內容看不到的問題嗎?
不只。狀態碼永遠是 200 所以不存在的頁面變成 軟性 404 、檔頭標籤靠程式碼注入所以社群卡片空白、結構化資料同理、路由連結若用按鈕實作爬蟲還走不到,這些都是連帶問題。
最小的改動是什麼?
先盤點哪些頁面真的需要被收錄,通常只有首頁與內容頁,登入後的操作介面不需要。針對那少數幾頁做預渲染,其餘維持原狀,成本遠低於整站改成伺服器端渲染。
用了預渲染還需要注意什麼?
路由必須事先列舉,動態產生的網址無法涵蓋。另外要確認產出的靜態檔案裡真的有內容與檔頭標籤,而不是只有一個空容器加上程式碼,有些設定會漏掉這一步。