網域搬家的檢查清單跑完了,DNS 換了、SSL 憑證也裝上,登入後台點開媒體庫,縮圖一張張顯示得清清楚楚,正要鬆一口氣,結果切到前台隨便一篇文章,滿版變成破圖示或灰色方塊,好像從沒上傳過一樣。
這個矛盾其實有跡可循。WordPress 產生這兩種畫面用的是完全不同的機制:後台媒體庫的縮圖是即時運算出來的,用目前設定的網站網址,加上資料庫裡存的相對檔案路徑,臨時組出一條網址;前台文章內文裡的圖片網址,卻是搬家或寫文章當下就寫死存進資料庫欄位的完整字串,不會因為設定裡改了新網址就自動更新。這個機制差異,正是網域搬家後圖片全壞、縮圖卻完全正常這種矛盾現象背後的根本原因,也代表故障其實分成兩種完全不同的類型:一種是曾經對資料庫做過整批文字取代,把 PHP 序列化資料的長度資訊改壞;另一種是資料庫裡的字串內容根本沒被動過,純粹是檔案的實際路徑或網域設定跟資料庫記錄兜不起來。兩種故障的修復方式不能混用,用錯方法很可能把原本還救得回來的資料徹底弄壞。
先從第一種、也是最容易把災情擴大的類型講起,看資料庫裡那段看似只是文字的序列化格式,一旦被純文字取代碰過,就會壞成什麼樣子。
媒體庫縮圖正常、前台圖片全壞的兩種故障類型
WordPress 會在資料庫裡留下兩份跟圖片有關的紀錄,一份用來畫後台的縮圖,一份用來組前台文章看到的網址,兩份紀錄的組成方式完全不一樣。媒體庫的縮圖與附件詳情頁,是每次載入頁面時即時運算出來的,做法是 WordPress 讀取 wp_options 裡目前設定的 siteurl 與 home 網址,再接上資料庫裡存的相對檔案路徑,現場組成一條完整網址。只要這兩個選項改成新網域,縮圖幾乎立刻就能正常顯示,不需要動到任何一篇文章的內容。
文章內文裡的圖片網址就不是這麼一回事。當初把圖片插進文章的那一刻,編輯器會把當時看到的完整網址直接寫進 wp_posts 資料表的 post_content 欄位,存成一段固定不變的文字,這段文字不會因為後台設定改了新網址就自動更新,只認資料庫裡實際存的那串字。縮圖靠即時運算、前台圖片靠寫死的舊紀錄,這正是縮圖顯示正常、前台卻滿版破圖的原因。
這個機制差異,決定了搬家後的圖片故障有兩種完全不同的成因。第一種是曾經對整個資料庫執行過搜尋取代,想一次把舊網域換成新網域,結果連帶把某些欄位裡的 PHP 序列化資料改壞;第二種是資料庫裡的字串內容從頭到尾沒被動過,純粹是文章裡寫死的網址還停留在搬家前的舊網域或舊路徑,沒有任何工具去更新它。兩種故障看起來都是圖片打不開,但一個是資料格式被破壞、另一個是資料內容過期,診斷方向與修復做法完全不同,混著用只會愈修愈亂。

序列化資料損毀連小工具設定都會一起壞掉
如果搬家後除了圖片壞掉,連跟圖片完全無關的功能也一起出問題,多半不是路徑設定的問題,而是資料庫裡的序列化資料被破壞了。WordPress 有大量設定值,像是側邊欄小工具的內容、佈景主題的客製化選項,都是用 PHP 的 serialize() 格式打包成一長串文字後,整包存進資料庫的單一欄位裡。這種格式對純文字取代特別脆弱,一旦被改壞,輕則某個設定跳回預設值,重則整個頁面白畫面,因為程式讀到一段格式錯誤的字串,呼叫 unserialize() 卻得到 false,後面的程式碼拿到不是預期的陣列或物件,直接整段報錯。
實際會看到的症狀包括:側邊欄的小工具內容整個消失或變成一串亂碼,甚至整個小工具直接從後台清單裡不見;佈景主題原本設定好的顏色、版面配置這類客製化選項無故跳回出廠預設值;部分文章的精選圖片在前台顯示不出來,但用 FTP 或主機的檔案總管確認,原始圖檔其實還老老實實躺在硬碟上,代表壞掉的是資料庫裡記錄的中繼資料,不是檔案本身;如果有開啟 WP_DEBUG,錯誤紀錄檔裡通常會看到跟 unserialize 有關的警告訊息。
這幾個症狀合在一起看,就是判斷眼前這個故障屬於哪一種類型最明顯的訊號。如果壞掉的範圍只限於圖片,大機率是後面要講的路徑問題;但只要牽連到跟圖片八竿子打不著的小工具或佈景主題設定,幾乎可以確定是序列化資料被破壞,得往資料庫的方向查,而不是往路徑設定的方向查。
前台圖片指向舊網域,媒體庫縮圖卻運作正常
跟上一節相反的另一種故障,是資料庫裡的字串資料其實完好無損,後台媒體庫縮圖、附件詳情頁顯示的檔案資訊也完全正確,唯獨文章內文裡插入的圖片,網址仍然完整寫著搬家前的舊網域或舊路徑。用滑鼠對這種壞圖按右鍵查看圖片網址,通常會直接看到舊網域的字樣,如果舊網域還存在,瀏覽器甚至會連過去看到舊站的內容或跳轉頁面;如果舊網域已經停用,就會收到新網域回應的 404 找不到頁面。不管哪一種,問題出在網址本身指錯地方,不是圖檔本身損毀或讀不出中繼資料。
會出現這種故障,通常代表搬家當下根本沒有對資料庫做過任何取代動作,可能只在後台設定裡把網址改成新的,或是單純把 wp-content/uploads 資料夾整包複製到新主機,就以為搬家完成了。資料庫裡那些當初寫死的舊網址字串完全沒被碰過,依舊原封不動地躺在 post_content 裡;wp-content/uploads 資料夾底下依年份、月份分類的子目錄結構,原始檔案通常一張都不會少,只是資料庫記錄的網址跟現在檔案實際的存放位置對不上。
這種情況跟前面提到的格式壞掉是完全不同層次的問題,資料庫裡的字串本身是健康的,可以正常被讀取、正常被 unserialize() 解析,只是內容本身已經過期,還在講一個不存在或已經換過的網域。這也是為什麼修復這種故障通常不需要動用任何序列化工具,把過期的網址換成正確的新網址就能解決。
圖片網址打不開的錯誤訊息透露故障出在資料庫還是磁碟路徑
不用先裝任何工具,單靠瀏覽器內建的功能,就能快速分出眼前這個破圖屬於哪一種故障,診斷可以分成三個步驟依序排查:
- 在前台任一篇壞圖的文章上,對圖片按右鍵選擇複製圖片網址或在新分頁開啟圖片。如果網址裡的網域還停留在搬家前的舊網址,或路徑寫著已經不存在的舊目錄,代表遇到的是路徑對不上的類型;如果網址已經完全是正確的新網域,卻依然打不開,同時還伴隨小工具內容消失或佈景主題設定跑掉,就要懷疑是序列化資料被破壞。
- 打開瀏覽器的開發者工具,切到 Network 分頁重新整理頁面,找到那張圖片的請求,看它回應的 HTTP 狀態碼。狀態碼 404 代表伺服器根本找不到那個路徑的檔案,通常指向路徑問題;如果網址本身沒問題、狀態碼卻不是 200,或是狀態碼 200 卻仍然顯示破圖示,就要往資料庫方向查。
- 對比較熟悉後台操作的人,可以進一步用 phpMyAdmin 或 WP-CLI 直接檢視
wp_postmeta資料表裡_wp_attachment_metadata這個欄位的原始內容,肉眼核對它開頭的序列化格式(例如a:6:{...})裡標示的長度數字,跟後面實際字串的長度是否吻合。吻合就代表這筆資料的格式完好,不吻合就是序列化資料已經壞掉的直接證據。
這三個步驟不需要按順序全部做完,通常第一步看到的網址與症狀組合,就足夠判斷八成該往哪個方向查;第三步屬於進階確認,留給想要百分之百確定的情況再做。
純文字取代改了字串內容,卻沒同步更新長度前綴
要理解為什麼一個看似單純的換網域動作,會把好端端的資料弄壞,得先知道 PHP 的 serialize() 把資料存成什麼樣子。serialize() 的官方定義,是把一個 PHP 值轉換成一段可以儲存在任何地方的位元組流表示法,而不是單純把資料攤平成一句話。以字串為例,一段序列化後的資料長成這樣:
s:32:"https://old-domain.tw/wp-content";Code language: plaintext (plaintext)
這裡的 s 代表這是一個字串型別,32 是雙引號裡文字內容的位元組長度,後面的 "https://old-domain.tw/wp-content" 才是實際內容。unserialize() 讀取這段資料時,不是靠找雙引號來判斷字串邊界,而是直接讀那個寫死的長度數字,讀滿 32 個位元組就認定字串結束,接著解析下一段結構。這個長度數字,是整段序列化資料格式正確與否的關鍵前提。
如果直接用 SQL 的 REPLACE() 函式,或是打開文字編輯器手動搜尋取代,對這段資料動手術,像下面這樣:
UPDATE wp_options
SET option_value = REPLACE(option_value, 'https://old-domain.tw', 'https://new-domain.tw')
WHERE option_name = '某個存了序列化陣列的選項';Code language: SQL (Structured Query Language) (sql)
這種做法只會把字串內容的文字換掉,完全不知道、也不會連動更新前面那個寫死的長度數字 32。換成新網域之後,字串實際的位元組數幾乎必然跟舊字串不一樣長,新舊網域名稱長度不同是常態,幾乎不可能剛好一樣長。這時候那個沒被更新的 32,就跟實際內容的長度對不上,unserialize() 讀到一半發現位元組數兜不起來,直接判定格式錯誤而失敗,整段資料連同裡面所有正確的部分一起讀不出來。
這不是運氣不好或操作失誤才會發生的意外,而是純文字取代這個方法在遇到序列化資料時,結構性地必然發生的結果,只要新舊字串長度不同,格式就一定被破壞。這也正是為什麼 WordPress 官方文件會特別點名警告,對整個資料庫執行搜尋取代來換網址這個動作有風險,表面上看起來只是換個文字,實際上動到的是一種對長度極度敏感的資料格式,而且結果比原本什麼都不做更慘,因為連原本能正常運作的設定都會一起報銷。

GUID 欄位在任何取代動作裡都不能被覆寫
在教正確做法之前,有一條規則不管用什麼工具、什麼方式做取代,都必須先守住,那就是絕對不能去動 wp_posts 資料表裡的 guid 欄位。GUID 是每一篇文章、每一個附件從建立那一刻起就固定下來、永久不變的識別碼,WordPress 官方文件對這條規則的措辭非常嚴格,明確規定在任何情況下都不可以更改 GUID 欄位的內容。RSS 訂閱閱讀器判斷一篇內容有沒有顯示過,靠的正是這個 GUID 值,不是文章標題或網址。
這條規則看起來跟修圖片沒什麼關係,但因為 GUID 欄位裡經常也包含網域字串,在執行整批取代網址這種大範圍動作時,很容易被一併誤傷。一旦 GUID 被取代動作連帶改掉,所有訂閱這個網站的 RSS 閱讀器,就會把全站所有舊文章都當成從沒出現過的全新內容重新推播一次,對讀者跟訂閱服務都是很糟糕的體驗,而且事後幾乎無法反向修回原本的值。
同一個道理也適用於動手前的準備動作,先把資料庫完整備份一份,並且存放在取代動作影響範圍之外的地方,不能讓這份唯一的備份被同一批操作意外覆蓋掉。理由很直接,序列化資料一旦被純文字取代破壞,前面提到的那個被覆寫掉的長度資訊通常無法逆向修復,備份是唯一能全身而退的保險。動手前先確認備份存在、備份可以還原,遠比事後想辦法補救划算得多。
交給看得懂序列化格式的工具做取代,而不是純文字比對
正式進入正確做法之前,先講清楚一個共同原理,不管接下來要用哪一種工具,核心邏輯都是先把序列化資料還原成真正的 PHP 值,在這個值上做完字串取代,再重新計算長度、重新序列化回資料庫,而不是像 SQL 的 REPLACE() 那樣逐字比對取代。WordPress 官方文件白紙黑字列出三個安全選項,分別對應不同的使用情境與技術門檻。

用 WP-CLI 的 search-replace 指令跑一次取代
如果主機提供 SSH 指令列權限,wp search-replace 就是官方文件列出的首選做法。官方文件的說明是,這個指令能智慧處理 PHP 序列化資料,而且不會更動主鍵值;遇到序列化欄位會自動先解析、完成取代,再正確地重新計算長度並序列化寫回去,同時保證不會動到資料表的主鍵。
實際操作的核心指令如下所示。
wp search-replace 'https://old-domain.tw' 'https://new-domain.tw' --skip-columns=guid --dry-runCode language: Bash (bash)
這行指令裡有兩個一定要用的旗標。--dry-run 會完整跑一次整個搜尋取代流程並顯示報告,但不會真的把改動寫進資料庫,先看報告確認異動的資料表與筆數合理,再拿掉這個旗標真正執行。--skip-columns=guid 呼應前面的鐵則,在指令層級直接把 guid 欄位排除在取代範圍之外,不用擔心手滑誤傷。如果遇到比較特殊或巢狀很深的序列化結構,官方文件也提供 --precise 這個旗標,強制全面改用 PHP、而不是預設較快的 SQL 查詢處理每一個欄位,速度較慢但對複雜結構的處理更可靠,可以在確認 --dry-run 報告怪怪的時候加上去再跑一次。
跑完 --dry-run 看報告時,重點是確認異動的資料表數量與筆數,跟站上實際的文章、附件數量大致對得上。數字明顯偏少,可能代表某些資料表被排除掉,取代並不完整;數字明顯偏多,則要留意是不是連不該動的欄位也被納入了。確認沒問題後,把 --dry-run 拿掉,重新跑一次同樣的指令才會真正寫入。
沒有指令列權限時,用外掛在後台完成同樣的取代
多數共享主機的使用者沒有 SSH 權限,這種情況下官方推薦的路線,是在後台安裝一款懂得處理序列化格式的搜尋取代外掛,像 Better Search Replace,直接在 WordPress 後台的工具選單底下完成同一件事。外掛的處理原理跟 WP-CLI 完全相同,自動解析序列化資料再重新計算長度,而不是逐字比對,差別只在操作介面換成了圖形化的後台頁面,不需要碰指令列。
操作順序也是同一套邏輯,先勾選模擬執行跑一次,看報告列出哪些資料表、多少筆資料會被異動,確認數字合理之後,再取消勾選這個選項正式執行取代。Better Search Replace 的官方外掛頁面本身也坦白提醒,輸入錯誤的搜尋或取代字串同樣可能弄壞資料庫,操作前務必先備份,這句提醒呼應了前面的鐵則,並不會因為換成外掛工具就可以省略備份這一步。
進階場景才用的第三方腳本,用完要立刻刪除
官方文件列出的第三個選項,是 Interconnect/it 開發的 Search and Replace for WordPress Databases 腳本,定位是給熟悉資料庫操作的進階使用者用的備案,通常在前兩種方式都碰壁的情境下才會用到,例如巨型多站網路架構,或是主機環境限制連外掛都裝不上去。
官方文件對這個選項的措辭也特別謹慎,明確註記只有在對資料庫管理有把握的情況下才使用這個選項。這個腳本本質上是一個能直接讀寫資料庫任意內容的獨立程式,用完之後必須立刻從伺服器上刪除,留著不刪等於在正式站上開了一個能直接操控資料庫的後門,是明顯的安全風險,不是修完圖片就可以放著不管的小事。
序列化資料壞掉後,最務實的做法是重新正確取代一次
如果已經先用純文字取代的方式把資料庫弄壞了,現在該怎麼補救,先要建立一個觀念,壞掉的序列化字串通常沒有辦法逆向修回原本正確的內容。原因在於,那個被覆寫掉的長度資訊,連同原本正確的網址字串,已經在錯誤的取代動作發生的當下同時被抹除,並不是重新換算一下就能救回來的問題,而是原始資料本身已經不在了。
最務實的做法,是回到網域搬家之前、還沒動過資料庫的那一份最新備份,重新用前面介紹的正確工具,WP-CLI 或搜尋取代外掛,完整跑一次取代。因為備份裡的資料還沒被錯誤的方法碰過,只要換成懂序列化格式的工具重跑一次,理論上就能得到完全正確的結果,不需要在壞掉的資料上做任何加工。
真的手上完全沒有可用備份,才需要退而求其次,依項目的性質分別搶救。文章附件的中繼資料屬於可以救回的部分,因為原始圖檔本身通常還完整躺在硬碟上,只是資料庫裡記錄的中繼資料格式壞了,這時候可以透過 WordPress 內建或外掛提供的重新產生縮圖功能,讓系統重新掃描既有檔案、重新產生一份格式乾淨的 _wp_attachment_metadata。至於小工具內容跟佈景主題的客製化設定,因為這類資料是純設定值、沒有原始檔案可以回頭推算,多半只能認命手動重新設定一次,沒有捷徑可以省略這道手續。
路徑對不上的破圖,檢查網址設定與資料夾結構就能解決
處理路徑對不上這種故障時,資料庫的字串本身沒有壞,問題出在網址設定跟磁碟上實際的檔案位置對不起來,排查可以照這個順序進行:
- 回到後台設定底下的一般頁面,確認 WordPress 位址與網站位址這兩個欄位是不是都已經改成新網域。官方文件對這兩個欄位的定義很清楚,網站位址是希望訪客在瀏覽器裡輸入、用來造訪網站的網址,WordPress 位址則是 WordPress 核心程式檔案實際存放的位置。多數情況下這兩個值會一樣,但只要其中一個沒改對,就足以讓路徑跑掉。
- 打開
wp-config.php,檢查裡面有沒有手動定義過WP_CONTENT_URL、WP_CONTENT_DIR、UPLOADS這三個常數。如果搬家前為了自訂wp-content資料夾的位置而手動寫了這幾個常數,換到新主機、新網域之後卻沒有同步改成新的路徑,WordPress 認定的路徑就會跟磁碟上實際的位置對不上。官方文件明確規定,UPLOADS這個常數的值一律要是相對於ABSPATH的相對路徑,不能寫成絕對路徑,如果當初圖方便寫成絕對路徑,換一台主機、目錄結構一變,這個常數就一定會失效。 - 確認
wp-content/uploads資料夾底下依年份、月份分類的子目錄結構,是不是整批完整搬過去了,以及路徑的大小寫、斜線寫法是否跟資料庫記錄的一致。多數 Linux 主機的檔案路徑是區分大小寫的,如果搬家過程中資料夾名稱不小心大小寫寫錯,或漏搬了某個年份的子目錄,對應的圖片一樣會打不開。
因為這種故障資料庫字串本身完好無損,通常只要把上面三個設定都對齊、再清一次快取,問題就能立刻恢復,不需要動用前面介紹的那些序列化取代工具。
取代跑完後用三個檢查點確認網站已經修復完成
不管是跑完 WP-CLI、外掛,還是單純改完網址設定,首頁看起來沒有破圖並不代表真的修好了,還需要具體核對三個地方:
- 如果用了 WP-CLI 或外掛的 dry-run、報告功能,回頭核對報告裡列出的異動資料表與筆數,是不是跟站上實際的文章數、附件數量大致對得上。明顯偏少,代表可能有某個資料表被排除在取代範圍之外,異動並不完整。
- 隨機挑幾篇有小工具、自訂欄位或 SEO 外掛設定的頁面,確認這些跟圖片完全無關的功能是否恢復正常運作。這一步同時也是反向驗證,如果這些功能都運作正常,反過來就能證明序列化資料在這次取代過程裡沒有被波及。
- 用瀏覽器開發者工具的 Network 分頁,實際檢查前台圖片的 HTTP 狀態碼。狀態碼 200 代表伺服器真的把檔案抓回來了,問題已經解決;狀態碼 404 代表路徑還是不對,得回頭檢查網址設定與資料夾結構;如果狀態碼是 200,畫面卻依舊顯示破圖示,就要往檔案本身是否損毀的方向查,而不是繼續在網址設定上打轉。
網域搬家後圖片全壞這件事,拆開來看其實沒有想像中神秘,關鍵只在於分清楚眼前這個故障,是資料格式本身被寫壞了,還是資料內容單純過期沒更新。前者要靠懂序列化結構的工具重新處理,後者只要把網址跟路徑設定對齊就好,兩條路線一旦搞混,很容易把原本還救得回來的資料徹底弄壞。下一次要搬網域,把備份做在動手之前,把取代工具換成懂序列化格式的那一種,圖片這一關多半能一次到位,不用等出事才回頭查。
