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.jsloader 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:completedqua 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, defaultpopup),data-services(optional, prefill). - inline: tìm container (
data-targetselector 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
messagetừ iframe để auto-resize (inline) + forward event.
5. Framing & CORS (BẮT BUỘC)
- Route
?embed=1phả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: DENYcho trang embed.
- Set
- 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.
- Lưu ý: vì iframe load
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
- Custom domain:
custom-domain.md - Booking flow V2 stepper:
../flows/booking-flow.md