廣告帳戶在最佳化一個跟訂房毫無關係的訊號。
幾組恆春業者各跑各的 Google Ads 帳戶。週報告都有數字:花費、CPC、曝光——全部從 Google 看得到的聯絡動作算出來:訪客點電話號碼、LINE 按鈕、WhatsApp。發生在瀏覽器裡的,Google 看得到。看不到的是接下來的事:另開 app 才打出去的那通電話、三天後才傳的那則 LINE、客人計畫確定後才回來敲定的那一筆。真實成交發生在所有系統之外,在老闆腦袋裡,記在廚房牆上的月曆。
沒有真實訂房資料,廣告系統唯一能拿到的是「在站點擊聯絡按鍵」的 GA4 事件。這些事件不是沒用,但它們不是訂房。Smart Bidding 只收到聯絡動作,就只能學哪種訪客會點按鍵,學不到哪種訪客會住進來。兩個族群不完全重疊,幾週下來,預算會往點擊習慣好的受眾漂移,而不是往真的會訂房的受眾。
訂房追蹤器是為了補上這個缺口而建的。老闆一確認訂房就記一筆,這筆資料同時進兩個地方:一本全家都能看的共用 Google 行事曆,以及正確 Google Ads 帳戶裡的一個成交事件。
路由問題決定了整個架構
多帳戶的需求帶出了第一個硬限制。
傳送離線成交給 Google Ads 最直覺的方式是 uploadClickConversions API——直接送、有歸因。問題是老闆記的是「事後確認」,客人早已離開網站,根本沒有 gclid。這個端點強制要求 gclid、wbraid、gbraid 三者之一,沒有就回 400。這條路走不通。
替代方案:GA4 Measurement Protocol 搭配每間民宿獨立的事件名稱。所有業者共用一個 GA4 資源,但各家 Google Ads 帳戶從這個共用 GA4 資源匯入自己的事件——A 民宿的訂房送 booking_[a]_confirmed,B 民宿送 booking_[b]_confirmed。兩個帳戶連結同一個 GA4,但各自只讀自己的事件,完全不互相污染。GA4 Measurement Protocol 不需要 gclid,事後記錄的限制就這樣繞過去了。
轉換價值不用固定金額。訂房 payload 帶動態價值,根據訂房類型和晚數計算:包棟每晚定價遠高於散客每晚,數字從業者實際定價表回推而來。Smart Bidding 依轉換價值給予不同權重,三晚包棟和一晚散客對演算法應該是比例完全不同的訊號。
後端路由先判斷這筆屬於哪個業者,從 properties 表查出這間民宿對應的事件名稱,計算 晚數 × 單晚均值,再送 GA4 Measurement Protocol 請求。properties 表就是路由表——新增業者只需要加一筆資料,不需要動應用程式碼。
表單的日常使用者,今年七十歲
第二個限制跟技術無關,但一樣沒有妥協空間:其中一家民宿每天負責記訂房的,是七十幾歲的老闆娘。
第一版表單照著標準手機 UI 設計。老闆娘用不了。日期選擇器的觸控區域太小、下拉選單在奇怪的位置、填了不合理的日期範圍不報錯只是靜靜存不進去——每一個問題都會變成電話,在這種規模的案子,接電話的是我。
改版消除了所有輸入歧義的來源。民宿、聯絡管道(LINE、電話、WhatsApp、FB 訊息)、訂房類型,全部改成大按鈕網格,選中的亮橘色,不需要輸入任何文字。日期欄位自己強制邏輯:退房最小值永遠是入住加一天;把入住日改到比退房日還晚,退房欄自動清空,不讓無效狀態靜靜卡在那裡。送出後是整個畫面的成功狀態,不是一個小提示。
這個 app 以 PWA 運行,裝在 iPhone 主畫面。原生 iOS 要付 Apple Developer 年費、等 App Store 審核;PWA 部署到 Vercel,推 GitHub 就自動上線,零額外分發成本,在主畫面上跟原生 app 看起來一樣。
行事曆問題的正確模型
家庭式民宿不是一個人在跑的。先生早上接電話,太太管走進來的客人,在外地工作的女兒負責回訊息軟體。三個人都要看同一份訂房狀態,而且任何一個都可能是某天記那筆的人。
最直覺的設計是讓每個登入帳號各自管自己的行事曆事件。結果是三本局部視角的月曆,沒有一本能完整信賴。
正確的模型是一個業者一本共用行事曆,由主帳號持有,其他家人透過 Google Calendar ACL 拿到寫入權限。這不是 app 層的權限,而是原生 Google 日曆的分享——成員可以直接在 Google 日曆 app 操作,不必每次都開訂房追蹤器。
第一筆訂房時,路由先查業者記錄裡有沒有現成的行事曆 ID。沒有就呼叫 calendarList.list() 找名叫「訂房日誌」的那本,找不到就新建一本,存下 ID,然後對所有非 owner 成員呼叫 acl.insert 發出分享邀請。acl.insert 對已分享的 email 回 409,程式忽略它。但如果每次訂房都重觸發邀請,就算 API 層面無害,收到重複通知的家人會把這個工具靜音。邀請只在行事曆第一次建立時發出。
正式環境踩到的坑:早期把行事曆寫入包在 try-catch 裡,但外層 async function 沒有 await。Vercel serverless 函式在寫入完成前就退出了,訂房進了 Supabase,行事曆沒有任何動靜。老闆看到確認畫面,全家行事曆靜止不動。加一個 await 就解決了。在本機因為程序不在請求之間退出,完全看不出這個問題。
多帳號存取層用 Supabase 的 SECURITY DEFINER SQL function 把任意 auth.uid() 映射到對應的 operator ID。RLS policy 靠這個函數做行層隔離,不需要把映射邏輯暴露給前端。
管道資料是附帶的收穫
老闆記訂房時選聯絡管道。這個欄位跟著 GA4 事件一起送出,存成自訂參數。
幾週下來會有一組管道分解:不是哪個管道帶來最多聯絡動作,而是哪個管道帶來確認訂房。如果 LINE 占了 40% 的聯絡按鍵點擊但只占 20% 的確認訂房,LINE 流量的成交轉換率有問題——在調整廣告投放方向前,這個數字比點擊率更有參考價值。
為什麼這套東西現在才出現
客製化離線成交管線、跨帳戶路由、動態轉換價值計算、共用行事曆基礎建設、為特定使用者設計的 UX——這些以前加在一起是好幾份合約的規模,遠超過幾組家庭式民宿能合理預算的範圍。
它現在存在,是因為 AI 把建構成本壓到這種規模的業者也算得過來的地方。成交那一欄不再是空的。演算法終於不再只學按鍵習慣了。