[CODE]

打造一個掌控自己訂房的民宿網站

為恆春一家民宿打造直客訂房網站的第一塊地基:自助後台透過 GitHub App 寫進版控、GA4 三層主機分流追蹤 CTA 意圖、WebP/AVIF 流水線處理 81MB 照片、路徑感知 CSP——以及為什麼 Google Ads 活動在這個網站上線之前根本不能跑。

1 min read AI 生成
astro ga4 performance security client-work

順序在第一行程式碼之前就決定了。

廣告活動不能先於落地頁。對小型民宿來說,如果付費流量只能打到 OTA 頁面,每一筆轉換仍然照付佣金,OTA 的再行銷像素還順帶吃掉你的廣告受眾資料。直客通路需要一個不把轉換送走的落地地點,廣告才有地方接。

這個網站六月上線,是整套架構的第一塊:網站存在,分析層建立起來量測 CTA 意圖,然後 Google Ads 活動才開。現在鋪的是廣告需要的地基。

有 OTA 連結的直客網站,是更貴的 OTA 轉介頁

這個網站上沒有任何平台連結。沒有合作 logo、沒有「也可以在 OO 上訂」、沒有猶豫的人可以去的備用出口。客戶找過幾個所謂「官網」,見過那種 Best Rate Guaranteed 大標語背後連到 Booking.com 的按鈕。這種設計只是比較有質感的轉介頁,結果一樣:轉換送給 OTA,佣金照算。

OTA 的優勢是三件事疊在一起:多年廣告預算堆出來的搜尋版面、跨百萬則評論建立起來的品牌信任、全職跑轉換優化的行銷團隊。一個單一民宿頁面,靠存在本身動搖不了任何一條。但它能做一件事:給廣告活動一個可以成交的地點。沒有這個,廣告不值得開。

AI 工具讓這套東西對小型業主在經濟上第一次可行。分析架構、圖片流水線、後台系統、廣告架構,以前每一層都要有專職人員。把這些壓進一個案子,讓從來養不起這些職位的民宿,第一次能同時把全套跑起來。

業主能自己操作的後台,才是真正的產品

最重要的功能,房客永遠不會看到。

業主可以自己推優惠——母親節橫幅、新住宿方案——不需要聯絡我。後台用 Google OAuth 做身份驗證,白名單全部放在 Railway 環境變數裡:一個永遠不會被鎖住的主帳號、加上一個可以動態新增管理員的逗號分隔清單,下一次請求就生效,不需要重新部署才能加人。業主送出修改後,後台透過 GitHub App API 把異動 commit 進 repo——commit 來自 app,不是她的帳號——Railway 偵測到推送,1 到 2 分鐘後上線。UI 只是比較好看的前門,骨子裡是一次 git commit。

我不在場的時間,是她在看什麼日期有缺口、什麼連假值得推、有沒有新方案想試。工具讓她停用,那些機會就跟著消失。

三個細節,事後看來比外表重要:

預覽內嵌,不另開分頁。 早期版本在新分頁開預覽。晚上十點用手機確認優惠排版的人不會切分頁——這個行為在現實中不存在。把預覽收進表單,打字和確認是同一個畫面。

整條 banner 只用一個顏色。 最初設計讓每行可以選不同顏色。第一個測試貼文出來像彩虹旗。改成整條只有一個色票:選擇變少,每次的成品都更好。

HTML 和宣傳圖片用 revalidate,不用長快取。 首頁和優惠圖片原本快取一小時。業主做了修改,畫面沒動,以為工具壞了,就不再用了。把這兩類路徑改成 max-age=0, must-revalidate,修改才能如實在 1 到 2 分鐘內反映。_astro 建置資產維持永久快取——內容哈希後檔名會變,不存在快取失效問題。讓人以為壞掉而放棄的工具,比根本不存在的工具更糟。

GA4 追的是「已經決定的人」

gtag.js 接 GA4,三層主機邏輯直接寫在客戶端 bootstrap script 裡。生產主機名稱觸發真實事件。localhost 觸發 GA4 DebugView,在本機確認事件結構不會汙染資料集。其他所有主機名稱——Railway 的開發環境、任何 staging URL——靜默,什麼都不載。沒有伺服器端過濾,邏輯在 bootstrap 裡就處理完了。

追蹤的全是 CTA 事件:電話點擊、加 LINE、線上訂房連結、地圖導航、評論連結。Pageview 是背景雜訊。這類民宿的訂房大多發生在 LINE 對話或電話裡,不在可以追蹤的結帳流程裡。Click proxy——GA4 裡的 CTA 事件紀錄——是目前最接近轉換訊號的東西。有人點了電話,是已經在考慮的人。這些訊號進每週報表,報表顯示哪些 CTA 在哪些 session 裡有觸發、哪些 session 整個結束都沒有任何聯絡動作——這是決定下一輪推什麼優惠的依據。

81MB 不是照片庫,是跳出率

原始照片集 81MB。在睡前查住宿的手機網速下,這個網站不會載完。連被考慮的機會都沒有,人已經關掉了。

我用 sharp 做了 WebP/AVIF 流水線,輸出 480、960、1600 三種寬度,兩種格式,commit 進 repo。在 Railway 上,每次部署的轉檔步驟幾乎是即刻完成——變體已經在 repo 裡,只有新圖片才真的跑編碼。品質參數:WebP 依寬度 70 到 76,AVIF 44 到 50(在這批照片上視覺差異幾乎不可見,但體積約小 30%)。不做銳化——銳化會讓壓縮格式的體積膨脹,WebP 和 AVIF 對高頻細節的編碼效率很差。替代方案是每張變體套一個輕微的「調色」:飽和度 1.1、輕量線性對比。在偏平的照片上加出存在感,又不會放大原始照片的雜訊。

<head>:非同步字型載入、AVIF hero 加 preload、中文字型用系統字體取代 CJK webfont(subset 輕易超過 400KB,在民宿網站上幾乎察覺不到視覺差異)、CSS 全部內聯消除渲染阻塞的樣式表往返。Express 壓縮中介層加上 gzip 和 Brotli。12 秒的初始渲染阻塞消失了。

後台寫進 git,資安就不能是政策聲明

後台透過 GitHub App 把修改 commit 進 repo——這不是帶聯絡表單的靜態型錄,攻擊面是真實的。

我在 security-headers.mjs 裡加了路徑感知的 CSP 和 HSTS,先在 Railway 上開 Report-Only 模式讓違規浮出來,確認清楚再切強制執行。

路徑感知是必要的,因為公開頁面和後台需要的來源清單不一樣。公開頁面需要 Google Fonts 和 GA4:googletagmanager.comfonts.googleapis.comfonts.gstatic.com*.google-analytics.com。後台不需要這些,但需要 blob:data: 給上傳預覽縮圖、需要 frame-src 'self' 給內嵌 <iframe srcdoc> 預覽。一個通用政策要嘛對公開端過於寬鬆,要嘛讓後台壞掉。依請求路徑是否以後台前綴開頭分成兩套允許清單,這個問題就解了。


佣金那欄還在她的報表裡——這是第一個月,旁邊有另一個數字了。