specs/storefront-templates.md

店面模板(storefront templates)

一句話:多租戶前台的模板制 —— 內容頁每模板一套版型、交易鏈共構換 token、內容槽 per-tenant 編輯,取代寫死荒野的單一前台。

1. 目標 / 問題

原 (public) 前台是為單一租戶寫死的門面,接第二個客戶時無法複用。目標:租戶從四個基礎模板(首波)選一個,換上自己的品牌內容即可開站;設計品質由模板保證(各模板有獨立的版型、palette 與字體性格),不開放租戶亂調視覺。

範圍切分:

  • 內容頁(首頁、about、tour 列表/詳情、journal、booking 展示層):每模板一套獨立版型。
  • 交易鏈(checkout / member / orders / auth / consent):全租戶同一套結構與表單邏輯,只吃模板供應的 theme token 換視覺。

2. 模板模型

四個基礎模板,code-level registry(typed,非 DB 資料):

key定位備註
outdoor戶外探險(大圖 hero、難度分級、梯次日曆)荒野的新家,第一個落地
resort旅宿度假(房型展示、訂房入口優先)房型區塊吃 lodging 資料;租戶無訂房模組時該區塊隱藏
minimal極簡通用新租戶預設(tenant create --template,缺省 minimal);也是未知 key 的 fallback,永不可下架
voyage經典旅行社(目的地分類、行程表重點展示)

每個模板宣告三樣東西:

  1. 一組 page/section component(獨立版型,共用資料層 loader:tours / journal / settings)。
  2. palette / theme token(含交易鏈要吃的 token)。設計定死,租戶不可改色改字型。
  3. 用哪些內容槽+各自的預設值:slot 目錄(key → 型別)是全域的、住同一個 code registry(見 §3);模板不定義 slot 形狀,只宣告選用哪些 slot 與預設值 —— 租戶不填也有完整門面。

模板與模組正交:模板不得假設任何模組(登山/海外/訂房)啟用,模組未授權時對應區塊靜默隱藏。

3. 資料模型

  • tenant_settings.storefront_template:text,模板 key。 值對不上 registry(打錯、模板改名/下架)時前台 fallback minimal 並記 log —— 門面降級不是整站 500,這裡刻意不 fail-closed(租戶身分解析仍 fail-closed,不受影響)。
  • storefront_contents:窄表,per-tenant 內容槽值(slot_key text PK、value jsonb、updated_at)。只存租戶覆蓋值;預設值住在模板定義(code),讀取時 merge。 slot key 語意:全域命名空間、跨模板共用(hero_title 在任何模板都是同一格值);value 形狀由 slot 型別(短文 / 長文 / 圖片 URL…)定義、不隨模板而異,模板只決定用哪些 slot 與預設值;換模板保留既有值,日後換回仍在。
  • 品牌欄位沿用 tenant_settings 既有 company_name / logo_url / site_tagline / seo_default_description,不另建第二套。
  • 內容頁(about / journal / booking)的 SEO description 兜底序:租戶槽覆蓋 → seo_default_description → 模板槽預設。模板槽預設恆非空,直接吃 merge 後槽值會讓站台級 SEO 預設永遠輪不到 —— 只有租戶真的填過該槽才優先。
  • about 長文沿用既有 content-authoring MDX,不搬進 slots。

4. 圖片選擇器(接既有圖庫)

圖庫與物件儲存已落地,模型的 SSOT 在 content-authoring §5,此處不重複。本功能只疊加:

  • 快速圖片選擇器(image picker):共用 dialog 元件 —— 圖庫網格選圖+現場上傳(沿用既有上傳鏈),選定後把 URL 填回呼叫端欄位。全站圖片欄位一律走 picker:模板內容槽、MDX 編輯器插圖、trip 封面、lodging 封面、logo;URL 手貼保留為 fallback。
  • 權限沿用圖庫既有閘:檢視 image.read、寫入(上傳)image.manage。
  • 刪除被引用中的圖片:允許並接受破圖(不做引用計數;slot 可隨時重選,模板預設值仍能補位)。

刻意不做(等真實需求):裁切、容量配額、孤兒檔清理、引用計數、獨立媒體管理頁(/admin/images 既有頁即管理入口)。

5. UI 與權限

  • /admin/settings 新增「店面(storefront)」tab(「外觀 appearance」已被後台色彩模式占用):選模板(儲存前可預覽)、編輯內容槽(圖片欄位走 picker)。
  • 權限:admin-only tab(settings 既有 isAdmin 二次守門),write action 掛 role.manage(同其他敏感 tab 慣例)。tab 清單與權限矩陣的 SSOT 在 settings §4。
  • 預覽:僅 admin session 生效,走獨立通道(如 draftMode())而非公開路由參數,且不得寫進公開 cache。
  • 切換儲存即生效。現況 (public) 路由 request-dynamic、無快取層;日後若導入 storefront cache,切換模板/改內容槽需連帶失效。
  • tenant create --template <key> 指定初始模板。
  • 位置:留在 apps/web (public) 內,不拆 storefront service(遵守 app-topology Phase 3 不提早拆的拍板)。

6. 刻意不做

主色/字型自訂(模板 palette 定死,ADR 0009)、section 組合式 page builder、媒體庫管理頁、storefront 獨立 service。

7. 交易鏈 theme token

已定案(#75 從 outdoor 反推最小集):模板 palette 直接供應公開站 @theme 的 --color-* 變數(brand/bg/fg/line 三族+hover/alt/soft/muted 變體;狀態語意色 success/warning/danger 跨模板固定、不在集內),由 (public) layout 以 inline style 注入整棵樹 —— 內容頁與交易鏈同一個 seam,交易鏈不換結構只換視覺。具體集合與各模板 值住 code registry(STOREFRONT_THEME_TOKENS,registry 測試守門完整性);注入方式的 決策理由見 ADR 0010。

(slot 目錄與各模板選用清單已定案於 code registry —— code is truth,此處不重複。)