Skip to content

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


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.


#Site KeySite NameProduction DomainPath PrefixSynxis Hotel IDSynxis Chain IDLanguages
1savoy-signatureSavoy SignatureTBDTBD25136PT, EN
2savoy-palaceSavoy PalaceTBDTBD799025136PT, EN
3royal-savoyRoyal SavoyTBDTBDTBD25136PT, EN
4saccharumSaccharum HotelTBDTBDTBD25136PT, EN
5the-reserveThe Reserve HotelTBDTBDTBD25136PT, EN
6calheta-beachCalheta Beach HotelTBDTBDTBD25136PT, EN
7gardensGardens HotelTBDTBDTBD25136PT, EN
8hotel-nextHotel NextTBDTBDTBD25136PT, 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.pt separate, others under savoysignature.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.

types/site.ts
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;
}

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 / Strings

Domain and hostname bindings will be configured once the domain strategy is finalized. The examples below show the path-based approach for reference:

Root NodeCultureHostname (example — path-based model)
Savoy Signatureptwww.savoysignature.com/pt
Savoy Signatureenwww.savoysignature.com/en
Savoy Palaceptwww.savoysignature.com/savoypalacehotel/pt
Savoy Palaceenwww.savoysignature.com/savoypalacehotel/en
Hotel Nextptwww.hotelnext.pt/pt
Hotel Nextenwww.hotelnext.pt/en
(similar for all other sites)

Content Root

🏨 Savoy Signature

🏨 Savoy Palace

🏨 Royal Savoy

🏨 Saccharum

🏨 The Reserve

🏨 Calheta Beach

🏨 Gardens

🏨 Hotel Next

📁 Shared Content

Home

Rooms & Suites

Dining

Spa & Wellness

Experiences

Gallery

Contact

Footer Links

Social Media

Legal Pages

Group Promotions


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.ts replaces middleware.ts for network-level request handling. See ADR-007 in 01_General_Architecture.md.

hotelnext.pt

savoysignature.com

/savoypalacehotel/*

/royalsavoyhotel/*

/saccharumhotel/*

/thereservehotel/*

/calhetabeachhotel/*

/gardenshotel/*

/* fallback

Incoming Request

Check hostname

site = hotel-next

Check path prefix

site = savoy-palace

site = royal-savoy

site = saccharum

site = the-reserve

site = calheta-beach

site = gardens

site = savoy-signature

Resolve theme + content

// 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).*)'],
};
helpers/site-resolver.ts
// 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 matching
const 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'];
}
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)

Content TypeScopeSourceExample
Page ContentSite-specificSite root node in UmbracoHomepage, Rooms, Dining
NavigationSite-specificConfigured per root nodeMain menu, breadcrumbs
FooterMixedShared node + site-specific overridesFooter links, social links
Legal PagesSharedShared Content nodePrivacy Policy, Terms
MediaMixedUmbraco Media Library (organized by site)Hotel photos, logos
Labels / StringsSharedDictionary items or Shared ContentButton labels, form labels
SEO DefaultsSite-specificRoot node propertiesDefault meta title, OG image
Booking ConfigSite-specificRoot node propertiesSynxis IDs, Navarino codes
BrowserUmbraco APINext.js (RSC)BrowserUmbraco APINext.js (RSC)GET /content/item/{site-root-id} (site-specific)Site content + navigationGET /content/item/{shared-content-id} (shared)Footer, legal links, labelsMerge site-specific + shared contentApply site themeRendered HTML
RuleDescription
Site overrides SharedIf a site defines a footer link, it takes precedence over shared
Fallback to SharedIf a site doesn’t define a value, fall back to Shared Content
No cross-site referencesA page in Savoy Palace should not reference content in Royal Savoy directly
Media is per-siteMedia folders are organized by site, even if some images are reused

x-site-key header

data-theme attribute

proxy.ts: detect site key

Root Layout: load theme CSS

Render with themed components

Note: Next.js 16 uses proxy.ts instead of middleware.ts. The site key is set by proxy.ts and read in the root layout.

app/layout.tsx
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>
);
}
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
themes/savoy-palace.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


[!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.

DomainRecord TypeValueProxyEnvironment
savoysignature.comA / CNAMECloudflare → Azure App Service☑️PROD
www.savoysignature.comCNAMEsavoysignature.com☑️PROD
hotelnext.ptA / CNAMECloudflare → Azure App Service☑️PROD
www.hotelnext.ptCNAMEhotelnext.pt☑️PROD
savoy.dev-*.wycreative.comCNAMEAzure App Service (DEV)DEV
savoy.stage-*.wycreative.comCNAMEAzure App Service (STAGE)☑️STAGE
qa-*.savoysignature.comCNAMEAzure App Service (QA)☑️QA
FromToType
savoysignature.com (no www)www.savoysignature.com301
hotelnext.pt (no www)www.hotelnext.pt301
HTTP:// any domainHTTPS:// same301
SettingValue
SSL ModeFull (Strict)
Always Use HTTPSOn
Minimum TLS1.2
HTTP/2On
HTTP/3 (QUIC)On
BrotliOn
Auto MinifyHTML, CSS, JS
Browser Cache TTLRespect existing headers

ConcernSolution
Cache key includes hostnameCloudflare caches per hostname — pages for hotelnext.pt are separate from savoysignature.com
Cache key includes pathPath-based sites (e.g., /savoypalacehotel/pt/rooms) have unique cache keys
Purge scopePurge is per-URL, not per-site — publishing in Savoy Palace only purges Palace URLs
Shared content purgeWhen Shared Content changes, purge all affected pages across all sites (dependency graph)

Full cache strategy in 09_Cache_and_Performance.md


// appsettings.json (Umbraco)
{
"Umbraco": {
"CMS": {
"DeliveryApi": {
"Enabled": true,
"PublicAccess": true,
"MemberAuthorization": {
"Enabled": false
}
},
"WebRouting": {
"DisableAlternativeTemplates": true,
"DisableFindContentByIdPath": true
},
"Global": {
"ReservedUrls": "~/.well-known"
}
}
}
}
Document TypePurposeUsed By
siteRootRoot node for each site; stores theme, booking config, SEO defaultsAll sites (1 per site)
homePageHomepage template with hero, featured modulesAll sites
contentPageGeneric content page with module compositionAll sites
roomsListPageRooms listing with filtersHotel sites only
roomDetailPageSingle room detailHotel sites only
diningListPageDining/restaurant listingHotel sites only
diningDetailPageSingle restaurant/dining venue detailHotel sites only
galleryPagePhoto galleryHotel sites only
contactPageContact form + mapAll sites
sharedContentRootRoot of shared content folderShared 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.

SiteSitemap
Savoy Signature (Group)![Savoy Signature Sitemap](/Users/ruirosa/Desktop/Projects/Savoy/PRD Docs/Arquitetura Informação/Savoy Signature.png)
Savoy Palace![Savoy Palace Sitemap](/Users/ruirosa/Desktop/Projects/Savoy/PRD Docs/Arquitetura Informação/Savoy Palave.png)
Royal Savoy![Royal Savoy Sitemap](/Users/ruirosa/Desktop/Projects/Savoy/PRD Docs/Arquitetura Informação/Royal Savoy.png)
Saccharum Hotel![Saccharum Sitemap](/Users/ruirosa/Desktop/Projects/Savoy/PRD Docs/Arquitetura Informação/Saccharum.png)
The Reserve Hotel![The Reserve Sitemap](/Users/ruirosa/Desktop/Projects/Savoy/PRD Docs/Arquitetura Informação/The Reserve.png)
Calheta Beach Hotel![Calheta Beach Sitemap](/Users/ruirosa/Desktop/Projects/Savoy/PRD Docs/Arquitetura Informação/Calheta Beach.png)
Gardens Hotel![Gardens Sitemap](/Users/ruirosa/Desktop/Projects/Savoy/PRD Docs/Arquitetura Informação/Gardens.png)
Hotel Next![Hotel Next Sitemap](/Users/ruirosa/Desktop/Projects/Savoy/PRD Docs/Arquitetura Informação/Next.png)

  • All 8 sites resolve correctly to the right content from a single Next.js application
  • hotelnext.pt serves Hotel Next content, separate from savoysignature.com sites
  • 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