脫離 OTA 自立的帳,成立的前提只有一個:你能以訂房為單位證明直客管道在運作。OTA 贏在三個維度同時疊加——壓倒性的廣告預算、多年交易量堆出來的品牌信任、全職運作的行銷團隊。直客管道不需要三個全贏,但它必須能產出一個回饋迴圈:哪個活動換來訂房,哪個只是在燒錢。沒有這個,網站不是直客管道,是成本中心。
我介入這個案子時,廣告活動已經在跑一段時間。後台有點擊數、有 CPC,轉換計數是零。不是因為沒有訪客聯絡業主,是因為追蹤事件從來沒有接好。
量測層
這個站沒有線上結帳流程。訪客不是打電話、加 LINE,就是離開。這個事實從根本改變了量測框架的邏輯。聯絡點擊不是轉換的代理指標,聯絡點擊本身就是轉換。每一個 tel: 連結、每一個 LINE 深層連結、每一個 Email 錨點,都需要同時觸發 GA4 事件和 Google Ads 轉換信標。這個階段最早的幾個 commit 沒有任何視覺成分。
三個頻道,各自對應兩個目的地:GA4 負責分析——電話、LINE、Email、訂房頁點擊、25/50/75% 捲動深度里程碑——Google Ads 負責轉換歸因,只有電話、LINE、Email 三項。訂房頁點擊只進 GA4,不送 Ads 轉換信標。訪問聯絡頁是漏斗步驟,實際聯絡才是轉換。把漏斗步驟算進轉換,轉換率虛高,Google 自動出價跟著走偏。這個區分不是細節,是整個量測框架的骨架。
有一個事件差點不見。手機上點 tel: 連結,作業系統不等任何網路請求就把控制權交給撥號器。gtag() 是同步的入隊操作,把事件推進 dataLayer,但實際信標是非同步發出去的。在 iOS Safari 的某些情境下,點擊到撥號器啟動之間的窗口,窄到信標還沒送出去頁面就被中斷。phone_click 悄悄消失,後台讀零,一個其實有在轉換的活動被砍掉。這是整條漏斗最強的訂房意圖訊號——真的試圖打電話,不是捲動到 50% 或點進聯絡頁——靜悄悄地蒸發掉。修法是主動對電話、LINE、Email 這三類接觸動作明確觸發 Ads 轉換信標,並把「任何未追蹤的 CTA」定義為系統 bug,不是日後再補的技術債。
捲動深度的實作另外挖出一個邊緣案例。這個站在 body 設了 overflow-x: hidden,實際的捲動容器是 body,導致 window.scrollY 永遠回傳 0。只從 window.scrollY 讀取捲動位置,任何里程碑都不會觸發。最終的讀取順序是 window.scrollY → window.pageYOffset → document.documentElement.scrollTop → document.body.scrollTop,取第一個非零值,同時監聽 window 和 document 兩個目標,確保無論事件從哪裡冒泡都能被捕獲。
這類工作五年前對小型民宿根本不可行。追蹤建置、廣告設定、歸因持續優化,每一項都需要獨立人頭。AI 工具在這裡不是取代了誰,是讓一個原本在這個規模根本不存在的服務第一次有了可能。
效能審計
歸因到位之後,進效能審計。這個站是純靜態 Astro 建置,部署在 Railway 上,前面掛了 Cloudflare 作為 CDN 代理層。Mobile Lighthouse 停在 76 分。邊緣快取讓靜態 HTML 取得更快,不會讓頁面本身變好。
我去找第三方套件和 render-blocking 外部資源。問題全都在自己寫的架構裡。
Hero AVIF 對 preload scanner 不透明。 Hero 圖片用的是帶 AVIF 和 WebP <source> 的 <picture> 元素。瀏覽器的 preload scanner——在 DOM 建構完成之前就開始投機預取的引擎——無法解析 <source> 的 srcset 候選項。它看到 <picture> 元素,卻要等 CSS 解析完成、元素 render、source selection 執行之後,才知道要抓哪個 AVIF 尺寸。Lighthouse Mobile Slow 4G 模擬下,hero AVIF 要到載入 4.6 秒後才被發現。修法是在 BaseLayout.astro 的 <head> 裡輸出一個 <link rel="preload" as="image" type="image/avif" imagesrcset="...480w, ...960w, ...1600w, ...2400w" fetchpriority="high">。Preload scanner 在解析 HTML 時立刻找到這個 tag,和其他資源並行抓取正確的尺寸。<picture> 元素後來執行 source selection,resolve 到同一個 URL,直接從 cache 取得。
樣式表在 12 KB 觸發 render-blocking 閾值。 Astro 預設的 build.inlineStylesheets: 'auto' 把 4 KB 以下的樣式表 inline,超過門檻的以外部請求送出。這個站的 CSS bundle 是 12 KB,剛好超過,瀏覽器必須先抓完這個外部 CSS 並解析完,才能繪製任何內容。就算 hero AVIF 已在 cache 等著,Lighthouse 測到的 LCP element render delay 仍有 1,756 ms。在 astro.config.mjs 設 inlineStylesheets: 'always',把所有樣式表都 inline 進每一頁 <head> 的 <style> tag 裡。代價是每頁 HTML 多約 12 KB 的 inline CSS。48 頁靜態站大約多 576 KB dist 大小。gzip 壓縮後 wire 大小差距縮小到每頁約 3–4 KB,換 1.7 秒的 LCP 回收。這個帳算得過去。
Google Fonts 吃掉 250 ms,但這個站 95% 是中文。 Cormorant Garamond 和 Nunito 用了 4 個 <link> tag 載入。就算搭配非阻塞的 preload + font-display: swap,Lighthouse Mobile 仍量到約 250 ms 的 render delay 來自字體 CSS 請求。拉丁 display 字體只在少數幾個標題出現。這個 wire 成本沒有賺回它應有的回報。兩套字體全部移除,改為系統字體棧:display 走 Iowan Old Style → Apple Garamond → Cambria → Georgia → CJK fallback,body 走 -apple-system → BlinkMacSystemFont → Segoe UI → Roboto → CJK fallback。視覺代價是真實的——Cormorant 的斜體質感消失了。這是正確的取捨。
Hero AVIF 本身也超重。 960 寬的變體(手機端在典型顯示密度下拿到的尺寸)在 quality 60 下是 137 KB。重新壓縮到 quality 38,降到 57 KB,縮小 58%。Slow 4G 模擬下,光這一張圖就省下約 400 ms 的傳輸時間。
Cloudflare:為了解決 A 進場,A 失敗了,卻帶來 B
Cloudflare 進場是作為診斷步驟。假設是:Meta 的 Facebook 爬蟲封鎖鎖定的是 Railway 的來源 IP 和 SSL 憑證鏈,不是域名字串本身。把 DNS 切到 Cloudflare,用不同的 IP 和憑證鏈呈現給 Meta 的 pipeline,或許能繞過封鎖。這個假設是錯的。一條 Meta 從未爬過的全新路徑,透過 Cloudflare 轉出去幾分鐘內也拿到 403,確認封鎖是鎖在客戶的域名字串本身,不是 IP 或邊緣節點。真正解決封鎖的方式來自完全不同的方向:透過 Meta Business Manager 用 DNS TXT 記錄做域名驗證,繞過被封鎖的 HTTP 路徑,直接讓 Meta 從 DNS 查驗。
Cloudflare 留在架構裡,是因為設好快取規則之後它對效能做了一件事。設定 /images/ 邊緣 TTL 30 天、/_astro/ 雜湊資源邊緣 + 瀏覽器 TTL 各 1 年之後,hero AVIF 的冷啟動 TTFB 從 2,028 ms 降到 180 ms,改善 11 倍。Railway origin 預設的 cache-control: max-age=14400 讓 Cloudflare 每 4 小時強制 revalidate 一次,使「冷啟動延遲」從罕見事件變成每 4 小時必發的事件。把邊緣 TTL 拉長,Cloudflare 才從通道變成真正的快取層。
Mobile 94,Desktop 98
最終結果:Mobile Performance 94,Desktop 98。從 76 到 94 的每一分,都來自資源發現延遲、render-blocking 等待、傳輸體積的真實降低,不是針對門檻的調整。同一目的地的競對站在量測當下是 Mobile 64。
業主打開 GA4,知道廣告在不在換訂房。量測層不是好看的數字,是讓這個管道有資格存在的條件。