一位顧問把名片上的乾淨網址——truelink-group.com/johnny——分享給客戶。客戶用手機一打開,畫面卻先閃出一個大大的「404」,隔了一瞬間才跳到正確的顧問落地頁。顧問緊張地問我們:「是不是我的連結壞了?」我們在自己電腦上反覆測試,怎麼開都很順、根本看不到 404。直到我們改用手機、特別是從社群 App 裡點進去,那一閃而過的 404 才現身。這不是連結壞了,而是一個藏在「裸短網址沒有後端路由」與「前端補救轉址放錯位置」之間的可見副作用。這篇文章,是我們在 TrueLink 自家平台上實際遇到、診斷、並修好這個坑的完整第一手紀錄——也順帶把背後「歸因路由」這個看似多餘、其實刻意的設計取捨講清楚。
本文重點
症狀:手機上先閃一下 404 才跳轉
先還原現場。我們的顧問有一個專屬的乾淨推廣網址,形式是裸單段路徑——例如 truelink-group.com/johnny。它印在名片上、寫在報價單裡、貼進私訊裡,目的就是「好記、好念、好打」。客戶拿到這個網址,在手機上輸入或點開它,期待看到的是顧問的個人介紹落地頁。
實際發生的是:網頁打開的瞬間,畫面上先出現一張全頁的「404 找不到頁面」大標題,停留極短的一瞬,接著才自動跳轉到正確的顧問落地頁。整個過程雖然最後會到達正確的目的地,但那「閃一下 404」的瞬間,已經在客戶心裡種下一個問號。
最折磨人的,是它難以在開發環境重現。工程師在自己的桌機上,用 Chrome、Safari 反覆打開那個裸網址,幾乎每次都是「咻」地一下就到落地頁,看不到任何 404。於是這類回報很容易被當成「使用者眼花」或「偶發網路問題」而被擱置——直到越來越多手機使用者,尤其是從 LINE、Facebook 這類 App 內建瀏覽器點進來的人,描述出同一個畫面。
先搞懂:裸 vanity 網址與歸因路由是兩種東西
要理解這個 bug,得先分清楚兩個長得很像、但職責完全不同的網址:
- 裸 vanity 網址(裸單段路徑):例如
/johnny。它的價值是對人友善——短、好記、適合印在名片上或口頭分享。它沒有任何前綴、沒有?ref=之類的參數尾巴,看起來乾乾淨淨。 - 歸因路由:例如
/r/johnny。它是平台正式、由後端負責處理的路徑:當有人造訪它時,後端會以 HTTP 200 回傳一張完整的伺服器端渲染(SSR)顧問名片落地頁。因為內容是後端直接吐出來的 HTML,社群與搜尋的分享預覽(OG 卡片)能正確抓到,連不執行 JavaScript 的爬蟲也看得到內容。
換句話說,/r/johnny 是「給機器與正式分享用的、結構完整的入口」,而 /johnny 只是「給人類好記的乾淨外觀」。理想情況下,我們希望使用者打 /johnny 也能順利抵達顧問落地頁——而這正是問題發生的接縫處。
關鍵設計觀念:對外正式分享(貼到社群、訊息)應該用
/r/johnny,因為它對爬蟲與分享預覽友善;裸/johnny是給名片這種「人會親手輸入」的場景。對沒有執行 JavaScript 的爬蟲而言,裸網址仍然是 404——這是一個刻意的取捨,不是疏漏。
根因:沒有路由就回 404,補救 script 又放在最末端
現在把鏡頭拉到伺服器這一端,看看當客戶打 /johnny 時,到底發生了什麼。
第一件事:裸單段路徑 /johnny 沒有對應的後端路由(rewrite)。我們的主機只為正式的歸因路由設了規則(/r/** 會交給後端的轉址函式處理),但「裸的單段路徑」並不在這份規則裡。於是主機去找一個叫 /johnny 的實體頁面,找不到。
第二件事:找不到頁面,主機就做它該做的事——回傳真正的 404 頁面(HTTP 狀態碼就是 404)。這個 404 頁面是一張設計好的、多語系的「找不到頁面」錯誤頁,本身沒有問題。
第三件事,也是 bug 的真正所在:為了不要讓客戶卡死在 404,我們在這個 404 頁面裡放了一段補救轉址 script。它的邏輯是:「如果使用者打進來的是一個合法格式的單段 slug(像 johnny),那就把他導去正式的歸因路由 /r/johnny。」這個想法本身完全正確——但那段 script 原本被放在頁面 <body> 的最末端。
瀏覽器處理一份 HTML 的順序,是由上而下解析的。當轉址 script 在 <body> 末端時,瀏覽器會先把整個頁面的內容解析、繪製出來(包括那個又大又醒目的「404」標題),之後才執行到底部那段 script,才開始做 location.replace('/r/johnny') 的轉址。於是順序變成了:
- 主機回 404 頁面。
- 瀏覽器解析並繪製出「404」大標題(使用者看到了)。
- 瀏覽器執行到 body 末端的 script,發現是合法 slug,呼叫
location.replace。 - 頁面跳轉到
/r/johnny,正確的落地頁出現。
第 2 步與第 3 步之間的那個空檔,就是使用者眼中「閃一下 404」的真相。它不是壞掉,是一個先繪製、後轉址的時間差被肉眼捕捉到了。
為什麼桌機看不到、手機才明顯
這就解釋了那個最令人困惑的現象:為什麼同一個網址,在工程師的電腦上怎麼測都正常,到了客戶手機上卻會閃?
答案在於那個「空檔」的長度,會隨著裝置與網路條件而伸縮。桌機通常 CPU 較快、網路較穩定,從「繪製 404」到「執行底部 script」之間的時間極短,短到肉眼幾乎無法分辨——畫面看起來就是一步到位。
但手機不同。行動裝置的處理速度與連線品質普遍較弱,尤其是從社群 App 的內建瀏覽器點進來時,環境更受限。那個原本短到看不見的空檔被放大,於是 404 大標題被真真切切地畫了出來,停留一瞬,才被轉址洗掉。這也是為什麼這類 bug 特別愛在「客戶的真實手機」上現形,卻在「開發者的乾淨環境」裡躲得好好的。
這裡有一個放諸四海皆準的教訓:任何「先讓某個過渡狀態被繪製、再用 JavaScript 把它換掉」的設計,在慢一點的裝置上都會露出那個過渡狀態。真正穩健的做法,是讓那個你不想被看到的狀態,根本沒有機會被繪製出來。
修法:把轉址上移 head、先藏畫面再 replace
知道了根因,修法就清晰了。核心只有一句話:讓轉址發生在 404 大標題被繪製出來之前。我們做了兩件事:
1. 把補救轉址 script 上移到 <head> 的最前面
我們把那段轉址邏輯,從 <body> 末端搬到 <head> 的最前端,也就是 CSS 與頁面內容還沒被繪製的時候就先執行。如此一來,瀏覽器在還沒畫出任何 404 視覺之前,就有機會判斷「這是不是一個該轉址的裸 slug」。
2. 命中 slug 時,先把整頁藏起來,再 replace
光是上移還不夠保險——在轉址真正生效的那幾毫秒裡,理論上仍可能閃出一點點內容。所以我們加上一道保險:當 script 判斷這是一個合法的單段 slug 時,先把 documentElement 的 visibility 設成 hidden(在任何繪製之前,先把整頁藏起來),再用 location.replace 導去 /r/johnny。因為頁面被藏住,使用者連那一瞬間的殘影都看不到,達到真正的零閃爍。
修好後的執行順序變成:
- 主機回 404 頁面。
- 瀏覽器一開始解析
<head>,立刻執行轉址 script。 - script 判斷是合法 slug → 先
visibility:hidden藏頁 →location.replace('/r/johnny')。 - 使用者完全沒看到 404,直接抵達落地頁。
還有一個重要的安全網:如果轉址因為任何原因失敗,就照常顯示 404,不會把使用者卡死在一張藏起來的空白頁。換句話說,這層補救只在「能成功幫上忙」時介入,幫不上忙時則優雅退場、回到原本正常的 404 行為。
同樣重要的是,這個修法零路由風險——我們完全沒有動主機的 rewrite 規則,只調整了 404 頁面內那段 script 的位置與行為。改動的爆炸半徑被壓到最小:它只影響「本來就會 404 的請求」,不可能波及任何正常頁面。
迴圈防護:nf=1 為什麼不可省
把裸路徑導去歸因路由,會引出一個必須處理的邊界情況:如果這個 slug 根本不存在呢?
想像一下:客戶打了 /johndoe,但平台上根本沒有叫 johndoe 的顧問。我們的補救 script 看到它是合法格式的單段 slug,照規則把他導去 /r/johndoe。後端的歸因路由一查,查無此人,於是要把使用者彈回 404 頁——而 404 頁裡的補救 script 又看到 johndoe 是合法 slug,又把他導去 /r/johndoe……如果不處理,這就成了兩邊互踢的無限轉址迴圈。
解法是一個簡單而關鍵的迴圈防護旗標:當歸因路由查無 slug、要把使用者彈回 404 時,會在網址尾巴帶上一個 nf=1 參數(nf 可理解為 "not found")。而 404 頁裡的補救 script,在做任何轉址之前,會先檢查網址裡是不是已經有 nf=1:
- 如果有
nf=1→ 代表「我剛剛已經試著轉去歸因路由、但那邊查無資料才彈回來的」→ 不再轉址,直接老老實實顯示 404。 - 如果沒有
nf=1→ 這是第一次造訪,照常嘗試轉去歸因路由。
這一個小小的旗標,把「合法格式但不存在的 slug」這個尷尬情況收得乾乾淨淨:使用者最多被導一次,確認查無資料後就停在一個正常的 404,而不會被困在無止盡的跳轉裡。任何「A 導去 B、B 在某條件下又導回 A」的轉址設計,都必須有一個這樣的單向旗標來打破潛在迴圈——這是寫轉址邏輯時不能省的一課。
為什麼不用 catch-all rewrite 一勞永逸
讀到這裡,很多工程師會冒出一個直覺:「與其在 404 頁裡用前端 script 補救,為什麼不乾脆加一條 catch-all 的 /* rewrite,把所有裸路徑都送進後端,由後端統一判斷要不要轉址?這樣不就根本不會回 404、也不會閃了嗎?」
這個想法很合理,但我們刻意選擇不這麼做,原因是成本與穩定的取捨:
- catch-all 會把每一個打錯的網址都變成一次後端呼叫。一條
/*rewrite 意味著:使用者打/jonny(打錯字)、/abuot(typo)、爬蟲亂試的隨機路徑、舊連結殘骸……全部都會被送進 Cloud Function。本來主機可以用一張靜態 404 頁、零後端成本、瞬間回應的請求,現在每一個都要喚起一次函式運算。流量越大、打錯的網址越多,這筆成本與延遲就越可觀。 - 它削弱了主機原生靜態 404 的速度與穩定。靜態 404 是最快、最不會出錯的回應路徑;把它換成「凡事先進後端」反而增加了一個可能出錯、可能變慢的環節。
- 它放大了爆炸半徑。動 rewrite 規則影響的是「所有路徑的路由行為」;而我們的修法只動「本來就會 404 的那一張頁面」。前者是全域手術,後者是局部貼布——在「能用小範圍修法解決」時,沒有理由去動全域路由。
所以我們的原則是:不動 rewrites,只在「本來就是 404」的請求上做前端補救。乾淨的 catch-all 看似一勞永逸,真實代價卻是把每一個 typo 都升級成一次後端呼叫。把改動限制在最小範圍——只在已經要失敗的請求上補救、不碰正常流量的路由——才是更穩健、更省成本的選擇。
推廣連結、登入、轉換的「看不見的破口」,想要有人幫你把關?
從裸短網址的 404 閃爍,到歸因路由與整體獲客動線的順暢度,這些藏在接縫處的細節往往最傷信任與轉換。TrueLink 可以協助你檢視自家產品的連結、登入與歸因設計,把放血的傷口補起來。
預約數位顧問諮詢 與我們聯絡歸因路由的取捨:誰把這個客戶帶進來的?
講到這裡,值得退一步問:我們為什麼一開始要設計 /r/johnny 這條歸因路由?為什麼不直接讓裸 /johnny 變成正式入口就好?
因為「誰把這個客戶帶進來的」這件事,本身就有商業價值。當一位顧問用他的專屬網址把客戶帶進平台、客戶最後註冊或成交,平台需要知道「這個轉換要歸功給哪位顧問」。/r/johnny 這條路由,本質上就是一個帶著歸因資訊的正式入口:它由後端處理,可以在伺服器端就把「這次造訪來自 johnny」這件事記錄下來、把對的歸因資訊一路帶進後續的註冊流程。
這就帶出一組真實的工程取捨:
- 裸
/johnny:對人最友善(好記、好打、印名片好看),但它沒有後端、不帶結構,對爬蟲是 404,也無法在伺服器端做歸因。 - 歸因路由
/r/johnny:對機器最友善(200 SSR、OG 預覽好、可被爬蟲索引、可在後端記錄歸因),但網址多了/r/前綴,沒那麼「乾淨」。
我們的解法不是二選一,而是讓兩者並存、各司其職:把裸網址留給「人會親手輸入」的名片場景,並用前述的補救轉址把它無痛接到歸因路由;而把 /r/johnny 當成「對外正式分享」的標準連結。這樣顧問既能在名片上印出乾淨好記的 /johnny,平台又不會在客戶轉換時丟失「是誰帶進來的」這個關鍵歸因。這個 404 閃爍的 bug,其實正是這套「人類友善網址」與「機器友善路由」並存設計在接縫處的一道裂縫——修好它,才讓這套取捨真正完整。
first-touch 歸因 cookie 的取捨
歸因還有一個更細的維度:當客戶不是「點進來就立刻註冊」,而是「先看看、過幾天才回來註冊」時,平台要怎麼記住「他最初是被誰帶進來的」?這就牽涉到所謂的 first-touch(首次接觸)歸因,而它通常靠一個 cookie 來實現——在客戶首次經由顧問連結造訪時,就把「來源是 johnny」寫進瀏覽器的 cookie,往後就算他直接打開網站再註冊,也能讀回這份來源、把功勞正確歸給最初帶他來的人。
這個機制很實用,但它帶著一串必須誠實面對的取捨。以下是設計 first-touch 歸因 cookie 時要權衡的幾個面向:
- 首次接觸 vs 最後接觸:如果一位客戶先被顧問 A 帶來、後來又點了顧問 B 的連結才註冊,功勞該算誰的?first-touch(記住第一個)保護的是「最初開發這位客戶的人」;last-touch(記住最後一個)則偏向「臨門一腳的人」。選擇哪一種沒有標準答案,但你必須明確選一種並說清楚規則,否則歸因會變成各說各話的爭議來源。
- cookie 的存活期:歸因 cookie 該記多久?太短,客戶過幾天回來註冊就追不回來源、低估了顧問的貢獻;太長,幾個月前的一次點擊還在影響今天的歸因,可能高估、也可能不符客戶的實際心智。存活期是一個直接影響「歸因公不公平」的旋鈕。
- 隱私與同意:歸因 cookie 是在追蹤「使用者從哪來」,這在隱私法規下屬於要謹慎處理的範疇。它應該服務於誠實的商業歸因,而非變成跨站追蹤;存什麼、存多久、為什麼存,都應該對得起對使用者的承諾。對一個以信任為核心價值的品牌來說,歸因的正當性本身也是信任的一部分。
- 跨裝置與清除 cookie 的現實:使用者可能在手機上點了連結、卻在電腦上才註冊;也可能清掉 cookie、或用無痕模式。任何純靠 cookie 的歸因都會在這些情況下漏接——所以歸因要被設計成「盡力而為、合理估算」,而不是「分毫不差的真理」。把它當成一個有誤差的訊號來用,才不會在邊界情況下做出錯誤的商業決定。
我們把這些取捨攤開講,是因為歸因常被當成「裝一下就好」的小功能,但它一旦牽涉到把功勞(乃至於分潤)歸給某個人,每一個旋鈕——記第一次還是最後一次、記多久、存什麼——都會變成真實的公平性與信任問題。誠實面對歸因的誤差與選擇,比假裝它精準無比更值得信賴。
為什麼一閃而過的 404 仍然值得修
你可能會想:「不過就是閃一下,最後還是會到正確頁面,有必要大費周章嗎?」答案是:有,因為在信任這門生意裡,第一印象很貴。
把場景想清楚:顧問把乾淨網址印在名片上、寫進報價單、貼進私訊,鄭重地分享給一位還在評估要不要信任這家公司的潛在客戶。客戶第一次打開這個連結——也就是他與這個品牌的第一次數位接觸——畫面卻先給他一個又大又醒目的「404」。即使隨後跳轉成功,那一瞬間傳達的潛台詞是:「這個連結怪怪的」「這家公司的東西不太穩」。對一個賣「數位信任」的品牌來說,這種首觸瑕疵尤其諷刺,也尤其傷。
而它最危險的地方,跟許多手機端的轉換問題一樣,是安靜。客戶不會特地回報「我看到你網站閃了一下 404」,他只會默默地在心裡扣一分,甚至直接關掉。你這邊的日誌裡,可能只是少了一筆完成的造訪,沒有任何錯誤訊號告訴你「剛剛有人在第一哩路上對你產生了懷疑」。我們之所以會發現它,正是因為一位願意開口的顧問替他的客戶問了一句——有多少不開口的人,已經默默扣了分?
反過來說,這也意味著:補這個破口,是少數「投入很小、回報直接」的體驗優化。我們沒有重寫路由、沒有動全域 rewrite、沒有承擔任何後端成本,只是把一段 script 換了位置、加了一道藏畫面與一道迴圈防護。對的人,在第一次打開連結時,就看到順暢、乾淨、可信賴的落地頁——順暢的入口,本身就是數位信任最具體的一塊基石。
上線前檢查清單
最後,把整套做法收斂成一份可以直接拿去對照的清單:
- 轉址 script 位置:把裸 slug 的補救轉址 script 放在
<head>的最前面,而不是<body>末端——在 CSS/內容被繪製之前就先執行。 - 先藏再轉:命中合法單段 slug 時,先
documentElement.style.visibility = 'hidden'藏頁,再location.replace導去歸因路由,達到零閃爍。 - 失敗退場:轉址若因任何原因失敗,照常顯示 404,絕不把使用者卡在一張藏起來的空白頁。
- 迴圈防護:歸因路由查無 slug 彈回時帶
nf=1;補救 script 偵測到nf=1就不再轉、直接顯示 404,打破潛在的無限迴圈。 - slug 格式守門:只對「合法格式的單段 slug」轉址;含路徑分隔、含副檔名(如
.html)或多段路徑者,照常顯示 404,別把真正的 404 也誤導走。 - 不動全域路由:用前端補救,不要為了裸 slug 加 catch-all
/*rewrite,避免把每個 typo 都變成後端呼叫。 - 分享用歸因路由:對外正式分享(社群、訊息、OG 預覽)一律用
/r/slug;裸網址留給名片這種人會親手輸入的場景。 - 真機驗證:務必用真實手機、最好從社群 App 內建瀏覽器點進裸網址測試——桌機因為太快,往往重現不出那一閃的 404。
這個問題的本質,從來不是「連結壞了」,而是「一個你不想被看到的過渡狀態,在慢一點的裝置上被畫了出來」。真正的解法不是事後把它洗掉,而是讓它根本沒有機會被繪製。把這個接縫補好,你守住的不只是一個短網址,而是客戶與你品牌第一次相遇時那份還很脆弱的信任。