這個民宿網站的目的只有一個:在訂房行為抵達 OTA 之前把它攔截下來。
OTA 的佣金遠比外界想像的高。透過平台成立的每一筆訂單,民宿實際到手的金額都少一截。直訂不只是節省成本,更是維持服務品質的前提。反制策略很清楚:讓直訂的體驗比平台更好、資訊更完整、服務更個人化。
BBQ 包場和生日驚喜安排,是這個邏輯的結構性延伸。十二人的烤肉食材直送民宿庭院,或者由 A-E 姐提前向恆春在地蛋糕店訂好蛋糕、等客人入住——這兩件事在任何 OTA 的預訂流程裡都不存在。它們本來就需要直接溝通。把頁面建起來,只是把那通本來會打去的電話正式化。
技術背景:Astro 5 SSR-first 架構,公開頁面以 export const prerender = true 靜態化。手動 i18n 路由,讓 admin 路由不被語系處理流程碰到。npm run build 依序跑三件事:圖片轉檔、astro build、audit-orphan-classes.mjs——三者都必須以零退出,CI 才算過。
四份語系檔案(BBQ 和生日各一中一英)幾個小時搭完。讓 build 乾淨花了更久。
那三個 bug,沒有一個是當天寫進去的。
英文在結構上帶著的、中文永遠不會有的東西
第一個 build 失敗讓我對著同一段程式碼讀了好幾遍,找不到問題在哪。兇手是 BBQ 頁 JS frontmatter 裡的一行:
name: 'Ocean's Best (Seafood Only)',
單引號開頭的 JS 字串,遇到字串內容裡的撇號就被截斷。'Ocean' 是完整字串。後面的 s Best (Seafood Only) 是意外的 token。結尾的 ' 再開一個未關閉的字串。錯誤訊息落在整個混亂序列的下游,不在撇號本身的位置。這就是為什麼重讀好幾次都找不到。
中文版從建站開始就一直正常 build。原因很結構性:中文散文裡不存在縮寫,' 這個字元不會出現在字串內容裡。單引號包中文散文是安全的,因為中文本身不帶這個字元。英文完全相反——廠商名稱的所有格、行銷文案裡的 ampersand、從文件貼過來的縮寫,這些字元是英文的結構性組成。只要用單引號包英文散文,每一個帶撇號的字串都是等著引爆的定時炸彈。
中文已經跑了好幾週的 build,英文這是第一個 session。不是測試流程不完整,是兩個字元集之間的結構性差異,第一次被暴露出來。
修法是一個字元:把外層引號換成雙引號。之後的慣例是:frontmatter 裡的英文散文字串一律用雙引號。這個規則不需要任何人記住。雙引號在 JS 裡是主流寫法,單引號反而變成異常——code review 一眼就看得出來。
複製 markup 是維持一致的方法,也是讓它分裂的方法
orphan-class audit 在 astro build 完成後才跑。腳本掃描 dist/ 裡每一個 HTML,抽出 markup 裡用到的 class name,逐一確認在該頁面的 CSS 裡有對應的 rule。有 class 沒 rule,腳本以非零值退出,build 失敗。
新服務頁是從現有元件複製結構來搭的。markup 帶走了,配套的 CSS rules 沒全帶到。四個 class 出現在四份語系檔案——BBQ 和生日各一中一英——卻沒有對應的樣式定義:.bbq-hero-content、.pkg-card--highlight、.pkg-group、.bday-hero-content。
這是維護 N 份語系副本的必然成本。內容各語系不同,layout markup 應該一致,所以你複製 markup。class 和 rule 的對應關係,存在一個地方(stylesheet),但引用分散在 N 個地方(N 份語系檔案)。搭頁面時加了 class 忘了寫 rule;刪了 rule 但三份語系 markup 都還在引用它。
跨所有頁面做視覺 QA 根本做不完。頁面數乘以語系數,注意力遠在全部看完之前就耗盡了。把 audit 放進 npm run build,讓 astro build 跑完之後自動執行,就是把一類沉默的漂移轉成 build 失敗。只有 build 失敗,才能在客人發現之前被抓住。
補上四個 class 花了兩分鐘。有意義的不是那兩分鐘,是這次失敗是大聲的。
裝飾性的 ::after,賭的是 stacking context 不會改變
第三個問題是水印。.protected-image::after 絕對定位在圖片容器正中央,旋轉 45°,文字顏色 rgba(255, 255, 255, 0.03)——近乎透明。
在淺色頁面上,這個透明度讓 pseudo-element 的溢出完全消失在背景裡。BBQ 和生日頁面用的是深色漸層 hero。.protected-image 元素在這些 section 附近時,溢出的水印文字疊在深色背景上,變成一個很淡但確實可見的殘影。
根源在這裡:.protected-image 設了 position: relative; display: inline-block,但沒有 overflow: hidden。水印文字是 font-size: 3rem; letter-spacing: 0.5em; white-space: nowrap。這個尺寸比大多數圖片容器都寬,沒有溢出邊界就跑出去了。這個問題大概已經存在好幾週。淺色頁面上,rgba(255,255,255,0.03) 疊在白色背景是真的看不見。深色 hero 讓它現出原形。
我選擇刪掉 ::after,而不是調整定位數值。調整數值只能修掉這個頁面組合的問題。下一個深色 section 或不同的 containing block,會讓同樣的溢出再次出現。根本原因不變,只是數字不同。
更根本的判斷是:rgba(255,255,255,0.03) 的裝飾性水印,根本不值得讓一個全域 ::after 參與每個頁面的 stacking context 計算。真正該問的問題是:這個元素值不值得存在。答案是不值得。.protected-image 同時加上了 overflow: hidden,確保未來放進去的任何內容都不會跑出邊界。
同一個 session:首頁服務卡接上了 BBQ 和生日頁面的連結,RoomCard 和 Footer 的聯絡 CTA 收緊,一個房型的文案在同一個 commit 裡中英文同步更新。
最後這件事不是小細節。orphan-class audit 在 build 層面作業,看不見跨 commit 累積的漂移。把 zh 和 en 的修改放在同一個 commit,才是真正關閉那個工具看不到的缺口。分開 commit,盲區就產生了。
那三個 bug 不是當天引入的。新的頁面只是移走了讓它們能繼續躲藏的條件。