← 文章

別讓 Google 找不到你的文章——SSR Blog 的 SEO 踩坑實錄

· 8 分鐘閱讀 · 425 次閱讀
Astro SEO SSR Sitemap JSON-LD
SSR Blog 實戰

別讓 Google 找不到你的文章

SSR Blog 的 SEO 踩坑實錄

用 Astro + PostgreSQL 建 Blog 時,每一個 SEO 決策背後的原因——以及那些差點讓文章消失在搜尋結果裡的坑。

問題

SSR Blog 的 SEO 為什麼跟靜態站不一樣?

1

動態內容不在 Sitemap

SSR 頁面是 runtime 產生的,@astrojs/sitemap 只收錄 build-time 的靜態頁。

文章發了卻不在 sitemap = Google 根本不知道它存在。

2

Tool Schema 不是唯一的隱形成本

跟 MCP token 一樣,SEO 也有「隱形成本」:meta tag 缺失、hreflang 錯誤、canonical 不一致。

67% 多語言站 hreflang 實作有錯誤

3

SSR 的效能雙面刃

SSR 讓內容即時更新,但也讓 TTFB 受 DB 查詢時間影響。

沒有快取策略 = 每次 request 都跑 DB query。

技術棧

SSR Blog 的組成

Astro 5
框架
PostgreSQL
資料庫
Drizzle
ORM 查詢層
Tailwind
樣式
Cloudflare
Tunnel 部署
SEO 決策

我們做了哪些事?

我們補了五層,從動態 sitemap 到 robots.txt,每層對應一個明確消費者。

動態 Sitemap
sitemap-posts.xml 從 DB 即時查詢已發布文章,build 時自動 patch 進 sitemap-index.xml
🔍 Google / Bing 爬蟲
JSON-LD 結構化資料
每篇文章自動生成 BlogPosting schema——標題、作者、發布時間、語言全從 DB 欄位對應
📊 搜尋結果豐富摘要
Canonical URL
SSR 頁面自動設定 canonical,指向自己。避免 http://localhost 和 https://blog.joneshong.com 被判定重複
🔗 去重機制
hreflang 多語言
zh-TW ↔ en 雙向 alternate link,加 x-default fallback。67% 的站做錯這步
🌐 多語言搜尋
robots.txt
允許爬蟲存取所有公開頁面,阻擋 /admin/ 和 /api/。指向兩份 sitemap
🤖 爬蟲控制
效能

Core Web Vitals 優化

0 KB
JS baseline
Astro 預設零 JS
按需
React island hydration
搜尋、圖譜
延遲
字體載入策略
CJK 系統字體 + Latin lazy load
SSR
動態內容
即時更新

Astro 的殺手鐧:預設不送任何 JavaScript。React 元件只在需要互動的地方 hydrate(client:only),其餘全是靜態 HTML。

踩坑紀錄

我們怎麼發現問題的?

不是預先設計好,是踩了 3 個坑才補齊:sitemap 沒人引用、閱讀時間算成 73 分鐘、Nginx subpath 頁面空白。

文章發了 → Google 搜不到

sitemap-posts.xml 沒人引用

@astrojs/sitemap 生成 sitemap-index.xml 時只包含靜態頁。動態的 sitemap-posts.xml 被無視了。修法:build 後自動 patch sitemap-index 加入 posts sitemap。

領悟:自動化工具不一定覆蓋所有 case,SSR 動態頁需要手動補漏。

12 分鐘的文章顯示 73 min read

Rich HTML 的 content.length 包含整個 CSS

content.length / 400 算閱讀時間,但 Rich HTML 格式的 content 有 25000+ 字元的 CSS + HTML tags,得先 strip 掉 tags,算出來的閱讀時間才正確。

領悟:任何跟 content 長度相關的邏輯,都要先確認 content 是什麼格式。

Nginx subpath proxy 下頁面空白

Vite dev server + subpath = 死路

Vite 的 ES module import 用絕對路徑,nginx sub_filter 改不完。解法:dev preview 不用 Vite dev server,改用 built SSR server。

領悟:Astro islands 的 component-url 屬性也需要被 sub_filter 改寫。

工具

貼你自己的 head 進來查

上面那份清單有六項,其中三項不用爬站就能驗——貼上你任一篇文章的 <head> 原始碼即可。canonical、hreflang、JSON-LD 這三項全部在瀏覽器本地解析,不會送出任何東西。剩下三項(sitemap 收錄、robots.txt、TTFB)必須實際對你的網域發請求,瀏覽器的跨站限制做不到,那三項留給上面那段 prompt 交給你的 AI 去爬。

查的是「你貼進來的那段字串」,不是你的線上頁面——如果你貼的是瀏覽器 DevTools 裡的 Elements 面板(那是 JS 跑完後的 DOM),結果可能跟爬蟲看到的不一樣。要跟爬蟲看到的一致,請用「檢視網頁原始碼」(view-source)那份。

帶走這段

複製給你的 AI Agent

你以為文章發了,Google 卻不知道它存在——SSR blog 的頁面是 runtime 才生成的,@astrojs/sitemap 只收 build 時的靜態頁,動態文章根本沒被寫進 sitemap。缺 meta tag 頂多排名差,沒被收錄是直接從搜尋結果消失。

把下面這段貼給你的 AI,讓它別憑常識答,直接去爬你的站、對照本文清單,抓出你實際漏了哪幾項:

我在看一篇 SSR blog 的 SEO 踩坑實錄 (https://blog.joneshong.com/zh/blog/hello-world), 作者用 Astro + PostgreSQL 建站,列了 5 個 SEO 決策和 3 個 SSR 專屬的坑。 請幫我查我的站有沒有踩同樣的坑,別憑一般 SEO 常識回答,直接驗我的實際輸出: 我的站:[填網址],屬 SSR / SSG / 純靜態(不確定就先幫我判斷)。 請逐項實地查(照本文踩坑的順序): 1. 抓我的 sitemap,確認「最新發布的那幾篇動態頁」真的在裡面—— SSR 最常見的坑就是動態頁沒被收錄,等於 Google 不知道它存在 2. 看任一篇文章的 head 標籤:canonical 有沒有指向自己、會不會殘留 localhost 或預覽網域 3. hreflang 有沒有雙向 alternate + x-default(本文實測 67% 多語言站錯在這步) 4. 抓我的 robots.txt:有擋掉 /admin/、/api/ 這類非公開路徑嗎?有指到 sitemap 嗎 5. 每篇有沒有 JSON-LD BlogPosting,欄位是從真實資料生成、還是寫死的 6. 如果我是 SSR:TTFB 有沒有被 DB query 拖慢、有沒有快取層 回我:哪幾項我真的漏了或做錯(附你查到的證據),按「會不會讓文章從搜尋消失」排優先序。 我若是純靜態站,第 1、6 點不適用,直接說不適用,別硬套。
參考資料

延伸閱讀

這五篇各自對應一個決策,從隱形成本類比到 67% hreflang 錯誤率數據都有。

文章 為什麼重要
Apideck: Your MCP Server Is Eating Your Context Window 啟發了「隱形成本」的類比
SEO for Astro: How to Make the Fastest Framework Also the Smartest Astro SEO 實作的完整指南
JSON-LD Blog Post Example BlogPosting schema 的參考實作
Multilingual SEO and Hreflang Guide 67% 錯誤率數據的來源
How to Improve Core Web Vitals SSR 效能優化的具體方法
結論

SEO 是工程,不是玄學

每一個決策都有原因,每一個坑都有教訓。
SSR Blog 的 SEO 不是設完 meta tag 就結束,是持續迭代的工程。

// 決策鏈

問題 1: 動態文章不在 sitemap
  → 手動補 sitemap-posts.xml

問題 2: SSR 無快取
  → 靜態頁 prerender + 動態頁 SSR

問題 3: 多語言 SEO 易出錯
  → hreflang 雙向 + x-default

驗證: Google Search Console 手動提交 + 監控

// 歸納
Astro SSR + 自動化 SEO pipeline
不漏收錄、不重複、不延遲
✦ 帶走這段