子網域換子目錄的 SEO 遷移實作。301 要永久留著、wrangler 會把不在清單裡的網域連 DNS 一起刪掉,還有搬完之後 Google 一個新網址都沒爬的那三個月。
發布於 2026-09-06・約 6 分鐘
我的站原本是 33 個工具各佔一個子網域:psy.sheeproom.com、travel.sheeproom.com、calc.sheeproom.com。
psy.sheeproom.com
travel.sheeproom.com
calc.sheeproom.com
搬成子目錄:sheeproom.com/psy/、sheeproom.com/travel/、sheeproom.com/calc/。
sheeproom.com/psy/
sheeproom.com/travel/
sheeproom.com/calc/
技術上很順利,一天做完,線上實測全過。SEO 上的代價比我預期的大很多。先講怎麼做,最後講代價。
主流的 SEO 說法是子目錄比子網域好,因為搜尋引擎可能把子網域當成不同的網站看待,權重不共享。這個說法沒有官方的明確背書,Google 官方講法一直是「兩種都可以」。
我搬的實際理由比較樸素:33 個子網域等於 33 個要各自累積信任的起點,而我一個都還沒累積起來。集中成一個至少讓內部連結有意義。
如果你的子網域已經有排名和流量,不要搬。 這篇的前提是「還沒有東西可以失去」。
我在 Threads 上已經發出上百則貼文,連結全部指向舊的子網域。那些連結不能死。
所以 36 個自訂網域一個都不能拆掉,它們現在的工作是接 301:
psy.sheeproom.com/* → 301 → sheeproom.com/psy/*
query 參數要保留。有些工具靠 ?r=cat 這種參數直接進結果頁,轉址時把它吃掉等於那些連結全壞。
?r=cat
「暫時留三個月再拿掉」是很常見的想法,但外部連結不會自己更新。只要還有一個地方連著舊網址,那個 301 就得活著。 我的打算是永久留著。
wrangler.jsonc 裡有一份 routes 清單,列出這個 Worker 綁哪些自訂網域:
wrangler.jsonc
routes
{ "routes": [ { "pattern": "sheeproom.com", "custom_domain": true }, { "pattern": "psy.sheeproom.com", "custom_domain": true }, { "pattern": "travel.sheeproom.com", "custom_domain": true } // ...36 個 ] }
這份清單是宣告式的。 部署的時候,wrangler 會把「Cloudflare 上現有的」跟「清單裡的」做比對,然後把不在清單裡的網域移除——連同它的 DNS 記錄一起刪掉。
我曾經因為整理設定檔時拿掉幾行,造成全站斷線。而且不是 Worker 掛掉那種斷線,是 DNS 解析不到,比較久才會恢復。
所以現在那份清單上面有一行註解:一個都不能拆。 要拆得先確認那個網域真的沒有任何東西連著,而且拆完要重新建 DNS。
這個行為不是 bug,宣告式設定本來就該這樣。危險的地方在於你不會預期一個「部署程式碼」的動作會刪掉 DNS。
const TOOL_HOSTS = new Set(['psy', 'travel', 'calc', /* ... */]) export default { async fetch(request, env, ctx) { const url = new URL(request.url) const [sub, ...rest] = url.hostname.split('.') // 舊子網域 → 新子目錄,永久轉址,保留路徑與 query if (rest.join('.') === 'sheeproom.com' && TOOL_HOSTS.has(sub)) { const target = new URL(`https://sheeproom.com/${sub}${url.pathname}`) target.search = url.search return Response.redirect(target.toString(), 301) } // 其餘照常 return handle(request, env, ctx) }, }
301 不是 302。302 是暫時的,搜尋引擎不會把權重轉過去。
搬家之前,元件裡到處都是硬編的網址。搬完之後有幾個地方還指著舊的,而它們不會報錯——只是使用者點下去多繞一次轉址,或者 canonical 指到舊網址(這個對 SEO 有實害)。
抽成一個函式,之後只改一個地方:
// toolRouting.js export function getToolUrl(toolId, slug) { const base = `https://sheeproom.com/${toolId}/` return slug ? `${base}${slug}` : base }
然後全站禁止在元件裡硬編網址。這條可以用 ESLint 的 no-restricted-syntax 擋,比靠自律可靠。
no-restricted-syntax
我用 Node 直接 import worker.js 打請求測(Workers 的全域物件 HTMLRewriter 和 caches 用替身補上),四種情況都要過:
worker.js
舊子網域 → 301,且 query 參數保留 新路徑 → 200 未知路徑 → 404(不是 200,不是轉回首頁) canonical 與 OG → 指向新網址
線上實測的時候踩到一次假警報:部署後前幾分鐘舊網址可能還回 200,那是邊緣快取殘留。加個 query 參數重測就正常了。
同樣的道理,部署後第一發 curl 拿到的可能是冷啟的通用內容,不能當結論,要再抓一次。
搬完之後我去看 Search Console。新的子目錄網址,100 個裡有 100 個是「Google 目前無法辨識這個網址」,從未爬取。
也就是說,Google 眼中這不是「同一個網站換了網址」,這是 100 個全新的、沒人連過的網址。舊網址累積的那點微薄信任,沒有在搬家的當下轉移過來——301 告訴 Google「東西搬到那裡了」,但 Google 要自己排隊去看,而它排得非常慢。
十天後才有兩個網址第一次被爬到。一個月後是 1 已索引 / 39 已爬未收 / 68 從未爬取。
這段空窗期比我預期長得多。 如果你的站有實質流量,這段期間的損失是真的。
我的情況是搬之前就幾乎沒有 Google 流量,所以損失有限——這也是為什麼我覺得可以搬。如果你有流量,這個決定的期望值完全不同,而且沒有辦法先小規模試(網址結構不能改一半)。
一、36 個網域的清單加了一行「不可拆」的註解,比任何備份都有用。
二、絕對網址集中成一個函式,這件事在搬家之前就該做,搬家只是逼我做而已。
三、驗證用程式跑,不要用眼睛看。 33 個工具乘上四種情況是 132 個檢查點,人工抽驗一定會漏,而漏掉的那個就是三個月後才發現壞掉的那個。
標籤:SEO、Cloudflare Workers、網址遷移、子網域、301