03 — Multi-Site and Domains
PRD Document · Savoy Signature Hotels — Multi-Site Headless Platform
Version: 1.0 · Date: 2026-03-04
Related docs:01_General_Architecture.md,06_Content_Modeling_Umbraco.md,04_Frontend_Architecture.md
1. Purpose
Section titled “1. Purpose”This document defines how 8 websites are managed within a single Umbraco installation and served from a single Next.js application. It covers domain configuration, content tree structure, multi-tenant routing, shared vs. site-specific content, and theming resolution.
2. Site Registry
Section titled “2. Site Registry”2.1 Complete Site Configuration
Section titled “2.1 Complete Site Configuration”| # | Site Key | Site Name | Production Domain | Path Prefix | Synxis Hotel ID | Synxis Chain ID | Languages |
|---|---|---|---|---|---|---|---|
| 1 | savoy-signature | Savoy Signature | TBD | TBD | — | 25136 | PT, EN |
| 2 | savoy-palace | Savoy Palace | TBD | TBD | 7990 | 25136 | PT, EN |
| 3 | royal-savoy | Royal Savoy | TBD | TBD | TBD | 25136 | PT, EN |
| 4 | saccharum | Saccharum Hotel | TBD | TBD | TBD | 25136 | PT, EN |
| 5 | the-reserve | The Reserve Hotel | TBD | TBD | TBD | 25136 | PT, EN |
| 6 | calheta-beach | Calheta Beach Hotel | TBD | TBD | TBD | 25136 | PT, EN |
| 7 | gardens | Gardens Hotel | TBD | TBD | TBD | 25136 | PT, EN |
| 8 | hotel-next | Hotel Next | TBD | TBD | TBD | 25136 | PT, EN |
[!IMPORTANT] Domain strategy is not yet defined. Each site may have:
- Independent domains (e.g.,
savoypalace.com,royalsavoy.com), or- Path-based routing under the main domain (e.g.,
savoysignature.com/savoypalacehotel), or- A combination of both approaches (e.g.,
hotelnext.ptseparate, others undersavoysignature.com)The routing architecture in section 4 supports all three models. The site resolver will be configured once domains are finalized.
Note: The current production URLs (e.g.,
savoysignature.com/savoypalacehotel/pt/) are included for reference but may change.
2.2 Site Identifier Resolution
Section titled “2.2 Site Identifier Resolution”export type SiteKey = | 'savoy-signature' | 'savoy-palace' | 'royal-savoy' | 'saccharum' | 'the-reserve' | 'calheta-beach' | 'gardens' | 'hotel-next';
export interface SiteConfig { key: SiteKey; name: string; domain: string; // Will be set once domain strategy is finalized pathPrefix: string; // e.g., '/savoypalacehotel' or '/' umbracoRootId: string; // GUID of the root node in Umbraco content tree synxisHotelId?: string; synxisChainId: string; defaultLocale: string; supportedLocales: string[]; theme: string; // Maps to CSS theme class / variables file navarinoHotelCode?: string; navarinoApiToken?: string;}3. Umbraco Content Tree Structure
Section titled “3. Umbraco Content Tree Structure”3.1 Tree Overview
Section titled “3.1 Tree Overview”The Umbraco content tree uses root nodes per site, plus a Shared Content node for reusable content:
Content├── 🏨 Savoy Signature [savoy-signature] → www.savoysignature.com│ ├── Home│ ├── About│ ├── Hotels│ ├── News│ └── Contact│├── 🏨 Savoy Palace [savoy-palace] → /savoypalacehotel│ ├── Home│ ├── Rooms & Suites│ ├── Dining│ ├── Spa & Wellness│ ├── Experiences│ ├── Gallery│ └── Contact│├── 🏨 Royal Savoy [royal-savoy] → /royalsavoyhotel│ ├── Home│ ├── Rooms & Suites│ ├── ...│├── 🏨 Saccharum Hotel [saccharum] → /saccharumhotel│ └── ...│├── 🏨 The Reserve Hotel [the-reserve] → /thereservehotel│ └── ...│├── 🏨 Calheta Beach Hotel [calheta-beach] → /calhetabeachhotel│ └── ...│├── 🏨 Gardens Hotel [gardens] → /gardenshotel│ └── ...│├── 🏨 Hotel Next [hotel-next] → www.hotelnext.pt│ └── ...│└── 📁 Shared Content ├── Footer Links ├── Social Media Links ├── Legal Pages (Privacy, Terms, Cookies) ├── Group-wide Promotions └── Common Labels / Strings3.2 Domain Assignment in Umbraco
Section titled “3.2 Domain Assignment in Umbraco”Domain and hostname bindings will be configured once the domain strategy is finalized. The examples below show the path-based approach for reference:
| Root Node | Culture | Hostname (example — path-based model) |
|---|---|---|
| Savoy Signature | pt | www.savoysignature.com/pt |
| Savoy Signature | en | www.savoysignature.com/en |
| Savoy Palace | pt | www.savoysignature.com/savoypalacehotel/pt |
| Savoy Palace | en | www.savoysignature.com/savoypalacehotel/en |
| Hotel Next | pt | www.hotelnext.pt/pt |
| Hotel Next | en | www.hotelnext.pt/en |
| (similar for all other sites) |
3.3 Content Tree Diagram
Section titled “3.3 Content Tree Diagram”4. Multi-Tenant Routing in Next.js
Section titled “4. Multi-Tenant Routing in Next.js”4.1 Routing Strategy
Section titled “4.1 Routing Strategy”The Next.js App Router handles multi-site routing using a combination of domain detection and path prefix matching. The routing strategy is designed to support both independent domains and path-based routing — the site resolver will be configured once the domain strategy is finalized.
Note on Next.js 16:
proxy.tsreplacesmiddleware.tsfor network-level request handling. See ADR-007 in01_General_Architecture.md.
4.2 Proxy Implementation (Next.js 16)
Section titled “4.2 Proxy Implementation (Next.js 16)”// proxy.ts (replaces middleware.ts in Next.js 16)
import { NextRequest, NextResponse } from 'next/server';import { resolveSiteFromRequest } from '@/helpers/site-resolver';
export function proxy(request: NextRequest) { const site = resolveSiteFromRequest(request);
// Inject site context into headers for server components const response = NextResponse.next(); response.headers.set('x-site-key', site.key); response.headers.set('x-site-theme', site.theme); response.headers.set('x-site-root-id', site.umbracoRootId);
return response;}
export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],};4.3 Site Resolver
Section titled “4.3 Site Resolver”// NOTE: Domain values and path prefixes are placeholder examples.// They will be updated once the domain strategy is finalized.
import { NextRequest } from 'next/server';import { SiteConfig, SiteKey } from '@/types/site';
const SITE_CONFIGS: Record<SiteKey, SiteConfig> = { 'savoy-signature': { key: 'savoy-signature', name: 'Savoy Signature', domain: 'TBD', // Will be set once domain strategy is finalized pathPrefix: 'TBD', // Will be set once domain strategy is finalized umbracoRootId: '/* GUID */', synxisChainId: '25136', defaultLocale: 'pt', supportedLocales: ['pt', 'en'], theme: 'savoy-signature', }, 'savoy-palace': { key: 'savoy-palace', name: 'Savoy Palace', domain: 'TBD', pathPrefix: 'TBD', umbracoRootId: '/* GUID */', synxisHotelId: '7990', synxisChainId: '25136', defaultLocale: 'pt', supportedLocales: ['pt', 'en'], theme: 'savoy-palace', navarinoHotelCode: '28854', navarinoApiToken: '/* token */', }, // ... remaining sites};
// Path prefixes sorted by length (longest first) for correct matchingconst PATH_PREFIXES = Object.values(SITE_CONFIGS) .filter(s => s.domain === 'www.savoysignature.com' && s.pathPrefix !== '/') .sort((a, b) => b.pathPrefix.length - a.pathPrefix.length);
export function resolveSiteFromRequest(request: NextRequest): SiteConfig { const hostname = request.headers.get('host') || ''; const pathname = request.nextUrl.pathname;
// 1. Check for separate-domain sites (Hotel Next) if (hostname.includes('hotelnext.pt')) { return SITE_CONFIGS['hotel-next']; }
// 2. Match by path prefix under savoysignature.com for (const site of PATH_PREFIXES) { if (pathname.startsWith(site.pathPrefix)) { return site; } }
// 3. Default: Savoy Signature (group site) return SITE_CONFIGS['savoy-signature'];}4.4 App Router Structure
Section titled “4.4 App Router Structure”apps/web/src/app/├── layout.tsx # Root layout — loads site context + theme├── [locale]/ # Language segment (pt, en)│ ├── layout.tsx # Locale layout — sets lang, dir, hreflang│ ├── page.tsx # Homepage (group site or hotel, based on site context)│ ├── [...slug]/ # Catch-all for CMS-driven pages│ │ └── page.tsx # Resolves content from Umbraco by site + slug│ ├── rooms/ # Hotel-specific routes (only hotel sites)│ │ └── [slug]/page.tsx│ ├── dining/│ │ └── [slug]/page.tsx│ └── contact/│ └── page.tsx├── not-found.tsx # 404 page (themed per site)└── error.tsx # Error boundary (themed per site)5. Shared vs. Site-Specific Content
Section titled “5. Shared vs. Site-Specific Content”5.1 Content Strategy Matrix
Section titled “5.1 Content Strategy Matrix”| Content Type | Scope | Source | Example |
|---|---|---|---|
| Page Content | Site-specific | Site root node in Umbraco | Homepage, Rooms, Dining |
| Navigation | Site-specific | Configured per root node | Main menu, breadcrumbs |
| Footer | Mixed | Shared node + site-specific overrides | Footer links, social links |
| Legal Pages | Shared | Shared Content node | Privacy Policy, Terms |
| Media | Mixed | Umbraco Media Library (organized by site) | Hotel photos, logos |
| Labels / Strings | Shared | Dictionary items or Shared Content | Button labels, form labels |
| SEO Defaults | Site-specific | Root node properties | Default meta title, OG image |
| Booking Config | Site-specific | Root node properties | Synxis IDs, Navarino codes |
5.2 Shared Content Resolution
Section titled “5.2 Shared Content Resolution”5.3 Content Inheritance Rules
Section titled “5.3 Content Inheritance Rules”| Rule | Description |
|---|---|
| Site overrides Shared | If a site defines a footer link, it takes precedence over shared |
| Fallback to Shared | If a site doesn’t define a value, fall back to Shared Content |
| No cross-site references | A page in Savoy Palace should not reference content in Royal Savoy directly |
| Media is per-site | Media folders are organized by site, even if some images are reused |
6. Theme Resolution
Section titled “6. Theme Resolution”6.1 Theme Loading Flow
Section titled “6.1 Theme Loading Flow”6.2 Theme Application
Section titled “6.2 Theme Application”Note: Next.js 16 uses
proxy.tsinstead ofmiddleware.ts. The site key is set byproxy.tsand read in the root layout.
import { headers } from 'next/headers';import { getSiteConfig } from '@/helpers/site-resolver';
export default async function RootLayout({ children }: { children: React.ReactNode }) { const headersList = await headers(); const siteKey = headersList.get('x-site-key') || 'savoy-signature'; const site = getSiteConfig(siteKey);
return ( <html lang={site.defaultLocale} data-theme={site.theme}> <head> {/* Theme-specific CSS variables are loaded via data-theme attribute */} </head> <body className={`site-${site.key}`}> {children} </body> </html> );}6.3 CSS Theme Structure
Section titled “6.3 CSS Theme Structure”themes/├── _base.css # Shared design tokens (spacing, breakpoints, etc.)├── savoy-signature.css # [data-theme="savoy-signature"] overrides├── savoy-palace.css # [data-theme="savoy-palace"] overrides├── royal-savoy.css # etc.├── saccharum.css├── the-reserve.css├── calheta-beach.css├── gardens.css└── hotel-next.css[data-theme="savoy-palace"] { --color-primary: #1a365d; --color-secondary: #c9a96e; --color-accent: #e8d5b7; --color-bg: #faf9f7; --color-text: #1a1a1a; --font-heading: 'Playfair Display', serif; --font-body: 'Inter', sans-serif; /* ... full token set defined in A03_Design_Tokens.md */}Full theming specification in
05_Design_System_and_Theming.md
7. Domain Configuration
Section titled “7. Domain Configuration”7.1 DNS Records
Section titled “7.1 DNS Records”[!NOTE] DNS records below are illustrative. Final domains depend on the domain strategy decision (see section 2.1). Internal DEV/STAGE domains use
wycreative.com.
| Domain | Record Type | Value | Proxy | Environment |
|---|---|---|---|---|
savoysignature.com | A / CNAME | Cloudflare → Azure App Service | ☑️ | PROD |
www.savoysignature.com | CNAME | savoysignature.com | ☑️ | PROD |
hotelnext.pt | A / CNAME | Cloudflare → Azure App Service | ☑️ | PROD |
www.hotelnext.pt | CNAME | hotelnext.pt | ☑️ | PROD |
savoy.dev-*.wycreative.com | CNAME | Azure App Service (DEV) | ❌ | DEV |
savoy.stage-*.wycreative.com | CNAME | Azure App Service (STAGE) | ☑️ | STAGE |
qa-*.savoysignature.com | CNAME | Azure App Service (QA) | ☑️ | QA |
7.2 Redirect Rules
Section titled “7.2 Redirect Rules”| From | To | Type |
|---|---|---|
savoysignature.com (no www) | www.savoysignature.com | 301 |
hotelnext.pt (no www) | www.hotelnext.pt | 301 |
HTTP:// any domain | HTTPS:// same | 301 |
7.3 Cloudflare Zone Configuration
Section titled “7.3 Cloudflare Zone Configuration”| Setting | Value |
|---|---|
| SSL Mode | Full (Strict) |
| Always Use HTTPS | On |
| Minimum TLS | 1.2 |
| HTTP/2 | On |
| HTTP/3 (QUIC) | On |
| Brotli | On |
| Auto Minify | HTML, CSS, JS |
| Browser Cache TTL | Respect existing headers |
8. Cache Considerations for Multi-Site
Section titled “8. Cache Considerations for Multi-Site”| Concern | Solution |
|---|---|
| Cache key includes hostname | Cloudflare caches per hostname — pages for hotelnext.pt are separate from savoysignature.com |
| Cache key includes path | Path-based sites (e.g., /savoypalacehotel/pt/rooms) have unique cache keys |
| Purge scope | Purge is per-URL, not per-site — publishing in Savoy Palace only purges Palace URLs |
| Shared content purge | When Shared Content changes, purge all affected pages across all sites (dependency graph) |
Full cache strategy in
09_Cache_and_Performance.md
9. Multi-Site in Umbraco Configuration
Section titled “9. Multi-Site in Umbraco Configuration”9.1 Umbraco Settings
Section titled “9.1 Umbraco Settings”// appsettings.json (Umbraco){ "Umbraco": { "CMS": { "DeliveryApi": { "Enabled": true, "PublicAccess": true, "MemberAuthorization": { "Enabled": false } }, "WebRouting": { "DisableAlternativeTemplates": true, "DisableFindContentByIdPath": true }, "Global": { "ReservedUrls": "~/.well-known" } } }}9.2 Content Type Structure for Multi-Site
Section titled “9.2 Content Type Structure for Multi-Site”| Document Type | Purpose | Used By |
|---|---|---|
siteRoot | Root node for each site; stores theme, booking config, SEO defaults | All sites (1 per site) |
homePage | Homepage template with hero, featured modules | All sites |
contentPage | Generic content page with module composition | All sites |
roomsListPage | Rooms listing with filters | Hotel sites only |
roomDetailPage | Single room detail | Hotel sites only |
diningListPage | Dining/restaurant listing | Hotel sites only |
diningDetailPage | Single restaurant/dining venue detail | Hotel sites only |
galleryPage | Photo gallery | Hotel sites only |
contactPage | Contact form + map | All sites |
sharedContentRoot | Root of shared content folder | Shared Content only |
Full content modeling in
06_Content_Modeling_Umbraco.md
10. Information Architecture — Site Maps
Section titled “10. Information Architecture — Site Maps”The information architecture for each hotel site has been designed during the UX/UI phase. The sitemaps below represent the planned page structure for each site. These may still evolve during development.
[!NOTE] These sitemaps are sourced from the UX/UI architecture work and serve as the reference for building the Umbraco content tree and Next.js routing structure.
| Site | Sitemap |
|---|---|
| Savoy Signature (Group) |  |
| Savoy Palace |  |
| Royal Savoy |  |
| Saccharum Hotel |  |
| The Reserve Hotel |  |
| Calheta Beach Hotel |  |
| Gardens Hotel |  |
| Hotel Next |  |
11. Acceptance Criteria
Section titled “11. Acceptance Criteria”- All 8 sites resolve correctly to the right content from a single Next.js application
-
hotelnext.ptserves Hotel Next content, separate fromsavoysignature.comsites - Path-prefix routing correctly identifies each hotel site (e.g.,
/savoypalacehotel/*) - Each site loads its own theme (different colors, fonts) via CSS variables
- Shared Content (footer, legal) is accessible from all sites
- Umbraco content tree has 8 root nodes + Shared Content with correct domain/culture bindings
- Publishing content on one site does not affect cache/content of other sites
- Publishing Shared Content triggers purge across all sites that reference it
- 404 pages are themed per site
- Language switching works correctly within each site’s path scope
Next document: 04_Frontend_Architecture.md