architecture/embed-widget.md

Kiến trúc Embed Widget

BẮT BUỘC đọc trước khi code script nhúng, embed layout, framing/CSP headers, hoặc postMessage giữa iframe ↔ parent.

Cho phép salon nhúng phần booking vào website bất kỳ của họ qua 1 đoạn <script>. Ví dụ:

<script src="https://glamvoo.com/embed.js"
        data-salon="studio-nordic"
        data-mode="popup"></script>

Ưu tiên sau Custom Domain. Tái dùng booking flow hiện có, không xây lại.


1. Phạm vi

Trong phạm vi

  • embed.js loader nhỏ, standalone, host tại platform.
  • 2 mode: inline (chèn iframe vào container) + popup (nút nổi → modal iframe).
  • Trang embed /b/<slug>/book?embed=1 — layout tối giản (bỏ platform chrome).
  • postMessage: auto-resize height + event booking:completed.
  • Framing headers cho phép nhúng (frame-ancestors) + CORS cho API.

Ngoài phạm vi (đến khi có yêu cầu)

  • Widget native (React/Vue component) — chỉ script + iframe.
  • Tuỳ biến theme sâu qua data-attr (MVP: dùng branding của tenant).
  • Analytics/conversion tracking trong widget.
  • Pre-select dịch vụ phức tạp (MVP: optional data-services).

2. Kiến trúc

Website khách (vd nailsalon-oslo.no)
  <script src="glamvoo.com/embed.js" data-salon="..." data-mode="popup">
        │
        ▼ embed.js (≈ vài KB, standalone, versioned)
  ┌──────────────────────────────────────────┐
  │ mode=inline → chèn <iframe> vào container │
  │ mode=popup  → nút nổi "Book now" → modal  │
  └──────────────────┬───────────────────────┘
                     │ iframe src
        glamvoo.com/b/<slug>/book?embed=1
                     │
        ◄── postMessage ──►  { type: 'embed:resize', height }
                             { type: 'booking:completed', bookingId }

3. Trang embed ?embed=1

  • Layout tối giản: bỏ CustomerHeader, TenantTopBar, footer platform; nền có thể trong suốt.
  • Tái dùng nguyên booking flow V2 (stepper) — chỉ đổi shell layout dựa trên query embed=1.
  • Confirmation render trong iframe (không redirect ra ngoài); phát event booking:completed qua postMessage để parent có thể tuỳ xử lý.

4. embed.js loader

  • Bundle standalone (esbuild/rollup target riêng, hoặc Next route serve static), versioned + immutable cache.
  • Đọc data-attr trên thẻ script: data-salon (bắt buộc), data-mode (inline|popup, default popup), data-services (optional, prefill).
  • inline: tìm container (data-target selector hoặc chèn ngay sau script) → tạo <iframe> responsive width 100%.
  • popup: tạo nút nổi (góc phải dưới) → click mở modal overlay chứa iframe + nút đóng.
  • Lắng nghe message từ iframe để auto-resize (inline) + forward event.

5. Framing & CORS (BẮT BUỘC)

  • Route ?embed=1 phải cho phép nhúng cross-origin:
    • Set Content-Security-Policy: frame-ancestors * (hoặc giới hạn theo verified domain) chỉ cho route này.
    • Đảm bảo không gửi X-Frame-Options: DENY cho trang embed.
  • CORS API: audit cấu hình CORS hiện tại (main.ts) — public booking endpoints phải cho origin nhúng gọi được (hoặc iframe gọi same-origin glamvoo.com → CORS không phải vấn đề vì iframe content là glamvoo.com).
    • Lưu ý: vì iframe load glamvoo.com/..., các API call bên trong iframe là same-origin → CORS không chặn. CORS chỉ thành vấn đề nếu sau này expose API call trực tiếp từ trang khách.

6. postMessage contract

Hướng type payload
iframe → parent embed:resize { height } (auto-resize inline)
iframe → parent embed:ready {} (đã mount)
iframe → parent booking:completed { bookingId }
parent → iframe embed:close {} (popup)

Validate event.origin === platform origin ở cả 2 phía.


7. Phases

Phase Nội dung
EW1 Embed layout: ?embed=1 shell tối giản tái dùng booking flow
EW2 Framing/CSP headers + CORS audit
EW3 embed.js loader (inline + popup mode)
EW4 postMessage: resize + booking:completed
EW5 Docs + demo page + versioning embed.js

Optional: giới hạn nhúng theo plan/verified domain (allow-list origin).


8. Liên quan