Hotel Group Websites: Multi-Property Architecture with Next.js
Managing one hotel website is straightforward. Managing thirty? That's where most teams start making decisions they'll regret for years. I've watched hotel groups cobble together separate WordPress installs per property, duct-tape page builders onto monolithic CMS platforms, and burn six-figure budgets on enterprise solutions that still can't handle a new property launch in under three months.
There's a better way. A single Next.js application — properly architected — can serve every property in a hotel group from one codebase, one deployment pipeline, and one content management layer. Each property gets its own branding, its own content, its own domain. The engineering team gets their sanity back.
This article breaks down exactly how to build that system. Not theory — actual architecture patterns we've used on real hotel group projects.
Table of Contents
- Why Hotel Groups Need a Unified Platform
- Architecture Overview: One Codebase, Many Properties
- Multi-Tenancy Patterns in Next.js
- Headless CMS Strategy for Hotel Groups
- Shared Components vs Property-Level Customization
- Booking Engine Integration
- Domain Routing and Property Resolution
- Performance at Scale
- Centralized Management Dashboard
- Deployment and DevOps
- Real-World Cost Comparison
- FAQ

Why Hotel Groups Need a Unified Platform
The typical hotel group website situation looks like this: Property A runs WordPress with a theme from 2019. Property B is on Squarespace because the GM's nephew set it up. Property C has a custom PHP site that nobody wants to touch. The corporate site is on a different platform entirely.
Every property update requires a different workflow. Brand consistency is a pipe dream. SEO strategy is fragmented across dozens of domains with no shared authority. When corporate decides to add a new amenity badge or update the booking widget, someone has to make that change in 15 different places.
The costs compound:
- Maintenance overhead: Each platform needs its own hosting, security patches, plugin updates
- Brand drift: Properties slowly diverge from brand guidelines
- Developer context switching: Your team (or agency) needs expertise across multiple platforms
- Slow property launches: New acquisitions take months to get online
- Analytics fragmentation: No unified view of performance across the portfolio
A centralized multi-property platform solves all of this. One codebase. One deployment. One CMS. Per-property content and branding delivered through configuration, not separate codebases.
Architecture Overview: One Codebase, Many Properties
Here's the high-level architecture that works:
┌─────────────────────────────────────────────┐
│ CDN / Edge Network │
│ (Vercel, Cloudflare, Fastly) │
├─────────────────────────────────────────────┤
│ Next.js Application │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Property │ │ Property │ │ Property │ │
│ │ Resolver │ │ Theming │ │ Content │ │
│ │ Middleware│ │ Engine │ │ Fetcher │ │
│ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────┤
│ API Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Headless │ │ Booking │ │ Media │ │
│ │ CMS │ │ Engine │ │ CDN │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────┘
The Next.js app acts as the rendering layer. Middleware determines which property is being requested (via domain, subdomain, or path). The theming engine applies property-specific styles. The content fetcher pulls property-scoped content from the headless CMS.
Everything downstream — the CMS, booking engine, media storage — gets queried with a property identifier. That identifier is the thread that ties the entire system together.
Multi-Tenancy Patterns in Next.js
There are three primary approaches to multi-tenancy in Next.js. Each has tradeoffs.
Pattern 1: Subdomain-Based Routing
Each property gets a subdomain: grandplaza.hotelgroup.com, seasideresort.hotelgroup.com.
Next.js middleware intercepts the request, extracts the subdomain, and resolves the property config:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { getPropertyByDomain } from '@/lib/properties';
export function middleware(request: NextRequest) {
const hostname = request.headers.get('host') || '';
const subdomain = hostname.split('.')[0];
const property = getPropertyByDomain(subdomain);
if (!property) {
return NextResponse.redirect(new URL('/not-found', request.url));
}
// Inject property context into headers for downstream use
const response = NextResponse.next();
response.headers.set('x-property-id', property.id);
response.headers.set('x-property-slug', property.slug);
return response;
}
Pros: Clean URLs, easy property isolation, good for SEO if properties don't need separate TLDs.
Cons: SSL certificate management for wildcards, less brand independence per property.
Pattern 2: Custom Domain Mapping
Each property has its own domain: grandplazahotel.com, seasideresort.com.
This is what most hotel groups actually want. The middleware logic is similar, but you're matching against a domain lookup table:
const DOMAIN_MAP: Record<string, string> = {
'grandplazahotel.com': 'grand-plaza',
'www.grandplazahotel.com': 'grand-plaza',
'seasideresort.com': 'seaside-resort',
'www.seasideresort.com': 'seaside-resort',
};
Vercel supports custom domains per project natively, and you can map up to 50 domains on their Pro plan ($20/month as of 2025). For larger portfolios, their Enterprise plan removes that limit.
Pros: Full brand independence, existing domain equity preserved.
Cons: DNS management overhead, more complex SSL provisioning.
Pattern 3: Path-Based Routing
All properties under one domain: hotelgroup.com/properties/grand-plaza, hotelgroup.com/properties/seaside-resort.
Pros: Simplest to implement, consolidated domain authority for SEO.
Cons: Less brand identity per property, URL structure feels corporate.
| Pattern | Brand Independence | SEO Flexibility | Implementation Complexity | Best For |
|---|---|---|---|---|
| Subdomain | Medium | Medium | Low | Budget-conscious groups |
| Custom Domain | High | High | Medium | Established brands |
| Path-Based | Low | High (consolidated) | Lowest | New portfolio sites |
Most hotel groups we work with at Social Animal end up choosing custom domain mapping. Properties have brand equity in their domains, and marketing teams want the independence.

Headless CMS Strategy for Hotel Groups
The CMS choice makes or breaks this architecture. You need a system that supports multi-tenancy at the content level — where editors for Property A can't accidentally modify Property B's content, but corporate admins can manage everything.
CMS Options That Work Well
Sanity is my top pick for hotel groups. Its document-level permissions, custom studio configuration, and GROQ query language make property-scoped content retrieval trivial. You can build a single Sanity Studio with workspace-per-property views. Pricing starts at $99/month for the Team plan (2025 pricing), and it scales well to large content volumes.
Contentful works if you're already in their ecosystem. Their space-level isolation maps well to properties, though it can get expensive — each space on the Premium plan adds cost, and you're looking at $2,500+/month for enterprise-scale hotel group needs.
Strapi (self-hosted) is the budget option. You'll need to build the multi-tenancy layer yourself using custom middleware and role-based access control, but there are no per-seat licensing costs.
We cover the full CMS selection process in our headless CMS development guide.
Content Modeling for Hotels
Here's a content model that works across properties:
// Sanity schema example
export const property = defineType({
name: 'property',
title: 'Property',
type: 'document',
fields: [
defineField({ name: 'name', type: 'string' }),
defineField({ name: 'slug', type: 'slug' }),
defineField({ name: 'domain', type: 'string' }),
defineField({ name: 'brand', type: 'reference', to: [{ type: 'brand' }] }),
defineField({ name: 'location', type: 'geopoint' }),
defineField({ name: 'theme', type: 'propertyTheme' }),
defineField({ name: 'bookingEngineId', type: 'string' }),
],
});
export const room = defineType({
name: 'room',
title: 'Room Type',
type: 'document',
fields: [
defineField({ name: 'property', type: 'reference', to: [{ type: 'property' }] }),
defineField({ name: 'name', type: 'string' }),
defineField({ name: 'description', type: 'blockContent' }),
defineField({ name: 'maxOccupancy', type: 'number' }),
defineField({ name: 'amenities', type: 'array', of: [{ type: 'reference', to: [{ type: 'amenity' }] }] }),
defineField({ name: 'gallery', type: 'array', of: [{ type: 'image' }] }),
],
});
The key pattern: every content document references a property. Queries always filter by property. Editors only see their property's content. Corporate admins see everything.
Shared Components vs Property-Level Customization
This is where the architecture gets interesting. You want 80% of components shared across properties, with 20% allowing per-property customization.
The Theming Layer
Create a theme configuration per property that feeds into your component system:
// types/theme.ts
export interface PropertyTheme {
colors: {
primary: string;
secondary: string;
accent: string;
background: string;
text: string;
};
typography: {
headingFont: string;
bodyFont: string;
};
logo: {
light: string;
dark: string;
};
borderRadius: 'none' | 'sm' | 'md' | 'lg';
heroStyle: 'fullbleed' | 'contained' | 'split';
}
Tailwind CSS v4 (released 2025) makes this significantly easier with its CSS-first configuration and native theme function support. You can set CSS custom properties at the layout level and have them cascade through every component:
// app/layout.tsx
export default async function PropertyLayout({ children }: { children: React.ReactNode }) {
const property = await getCurrentProperty();
const theme = property.theme;
return (
<html
style={{
'--color-primary': theme.colors.primary,
'--color-secondary': theme.colors.secondary,
'--font-heading': theme.typography.headingFont,
'--font-body': theme.typography.bodyFont,
} as React.CSSProperties}
>
<body className="font-body text-text bg-background">
{children}
</body>
</html>
);
}
Component Composition
Shared components accept theme tokens and render differently per property without branching logic:
// components/HeroSection.tsx
export function HeroSection({ property }: { property: Property }) {
const heroConfig = property.theme.heroStyle;
const variants = {
fullbleed: 'h-screen w-full',
contained: 'h-[70vh] max-w-7xl mx-auto rounded-2xl overflow-hidden',
split: 'grid grid-cols-2 h-[80vh]',
};
return (
<section className={variants[heroConfig]}>
{/* Shared hero content structure */}
</section>
);
}
Booking Engine Integration
Hotel websites exist for one reason: to drive bookings. The booking engine integration needs to be rock solid.
Most hotel groups use one of these booking engines: SynXis (Sabre), Pegasus, Bookassist, SiteMinder, or a proprietary central reservation system. The integration pattern is almost always the same: pass a property identifier, date range, and guest count to get availability.
// lib/booking.ts
export async function checkAvailability({
propertyCode,
checkIn,
checkOut,
adults,
children,
}: BookingQuery) {
const response = await fetch(`${BOOKING_ENGINE_URL}/availability`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${BOOKING_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
hotel_code: propertyCode,
arrival: checkIn,
departure: checkOut,
guests: { adults, children },
}),
});
return response.json();
}
For the booking widget itself, you have two options:
- Embedded iframe: The booking engine provides a widget you embed. Least work, least control.
- API-driven custom UI: You build the search and results UI, call the booking API directly, and hand off to the booking engine only for payment. More work, much better UX.
Option 2 is where a Next.js architecture really shines. You can build a gorgeous, fast, on-brand booking experience that feels native to each property. Server Components can pre-fetch availability data. The booking flow stays on your domain, which is better for conversion tracking and SEO.
Domain Routing and Property Resolution
The property resolution flow needs to be fast. Like, really fast. It runs on every single request.
Here's the pattern that works in production:
- Edge middleware resolves domain → property slug (in-memory lookup, sub-millisecond)
- Property config is cached at the edge using Vercel Edge Config or Cloudflare KV
- Full property data (theme, navigation, footer content) is fetched once per build via ISR or at request time with caching
// lib/property-resolver.ts
import { get } from '@vercel/edge-config';
export async function resolveProperty(hostname: string): Promise<PropertyConfig | null> {
// First: check edge config (sub-5ms)
const domainMap = await get<Record<string, string>>('domain-map');
const propertySlug = domainMap?.[hostname];
if (!propertySlug) return null;
// Second: get full property config (cached)
const propertyConfig = await get<PropertyConfig>(`property:${propertySlug}`);
return propertyConfig;
}
Vercel Edge Config is perfect for this — it's a globally distributed key-value store with read latency under 1ms. It costs $0 on Pro plans for up to 512KB of data, which is plenty for a property lookup table.
Performance at Scale
Hotel websites have specific performance characteristics that matter:
- Image-heavy pages: Room galleries, property photos, destination imagery
- Seasonal traffic spikes: Holiday periods, convention season, local events
- Global audience: International travelers browsing from everywhere
- Conversion-critical: Every 100ms of load time costs bookings
Static Generation Strategy
Use Incremental Static Regeneration (ISR) for property pages. Hotel content doesn't change every minute — a 60-second revalidation period is usually fine:
// app/[propertySlug]/page.tsx
export async function generateStaticParams() {
const properties = await getAllProperties();
return properties.map((p) => ({ propertySlug: p.slug }));
}
export const revalidate = 60;
For a 30-property group with ~20 pages per property, you're pre-generating ~600 pages. Next.js handles this without breaking a sweat. Build times stay under 5 minutes.
Image Optimization
Next.js Image component with a remote loader handles per-property image optimization. If you're using Sanity, their image CDN with automatic format conversion and resizing is excellent. Cloudinary is another solid option at $89/month for the Plus plan.
A typical hotel property page should target:
- LCP under 2.5s on 4G connections
- CLS of 0 (no layout shift from loading images)
- Total page weight under 1.5MB on initial load
Centralized Management Dashboard
Beyond the CMS, hotel groups need operational dashboards. This is where you build custom tooling:
- Property overview: Status of each property site (live, staging, maintenance)
- Content freshness: Which properties haven't updated their seasonal content
- Performance monitoring: Core Web Vitals per property
- Analytics rollup: Booking funnel metrics across all properties
We typically build this as a separate Next.js app (often with the App Router's server-side capabilities) that connects to the same data sources. The management dashboard is an internal tool — it doesn't need to be flashy, but it needs to be functional.
Deployment and DevOps
One codebase means one CI/CD pipeline. Here's the deployment flow:
- Code changes: PR → review → merge to main
- Build: Next.js builds all static pages across all properties
- Deploy: Vercel (or similar) deploys to edge network
- DNS: Each property domain points to the deployment
Content changes don't require a deployment. The headless CMS triggers ISR revalidation via webhook:
// app/api/revalidate/route.ts
export async function POST(request: Request) {
const body = await request.json();
const { propertySlug, contentType } = body;
// Revalidate specific paths for the changed property
revalidatePath(`/${propertySlug}`);
if (contentType === 'room') {
revalidatePath(`/${propertySlug}/rooms`);
}
return Response.json({ revalidated: true });
}
Real-World Cost Comparison
Let's compare the actual costs for a 20-property hotel group:
| Cost Category | Separate Sites (WordPress) | Unified Next.js Platform |
|---|---|---|
| Hosting (monthly) | $2,000-4,000 (20 × managed WP) | $150-400 (Vercel Pro/Team) |
| CMS Licensing | $0-600 (plugins per site) | $99-300 (Sanity/Contentful) |
| SSL Certificates | $0-400 (if not using Let's Encrypt) | $0 (auto-provisioned) |
| Maintenance (annual) | $40,000-80,000 (updates, security) | $10,000-20,000 |
| New Property Launch | $5,000-15,000 per site | $500-2,000 (content + config) |
| Annual Total (est.) | $75,000-150,000 | $15,000-35,000 |
The numbers aren't even close. And this doesn't factor in the developer experience improvements — having one codebase means your team actually understands the system. No more "which WordPress version is that property running?"
For hotel groups considering this approach, we've outlined our Next.js development capabilities and you can see our pricing structure for a more detailed estimate.
FAQ
How long does it take to migrate a hotel group to a unified Next.js platform? For a 10-20 property group, expect 3-5 months from kickoff to full launch. The first property takes the longest (8-10 weeks) because you're building the platform. Each subsequent property is primarily content migration and theme configuration, which takes 1-2 weeks each. We typically launch in waves — 3-4 properties at a time.
Can individual properties still have unique pages that other properties don't have? Absolutely. The content model supports property-specific page types. If your resort property needs a "Wedding Venues" section but your business hotel doesn't, that's a content-level decision. The CMS schema supports optional page types, and the Next.js dynamic routing handles rendering any page that exists in the CMS for a given property.
What happens when you acquire a new hotel and need to add it to the platform? This is one of the biggest wins. Adding a new property means: creating a property entry in the CMS, configuring the theme (colors, fonts, logo), adding the domain mapping, and populating content. A competent content team can have a new property live in 1-2 weeks. Compare that to 2-3 months to build a standalone website.
How do you handle multi-language support across properties in different countries? Next.js has built-in i18n routing support. Combined with a headless CMS that supports localized content (Sanity and Contentful both do this well), you can serve each property in its relevant languages. A property in Barcelona might need Spanish, Catalan, English, and French. A property in Miami might only need English and Spanish. Each property's language configuration is independent.
Does this architecture work with Astro instead of Next.js? Yes, and for some hotel groups it's actually the better choice. If your properties are primarily content-driven with minimal interactivity (no complex booking flow, for example), Astro's multi-page architecture can deliver even better performance with less JavaScript. The multi-tenancy patterns are similar — middleware-based property resolution, headless CMS with property scoping, theme tokens per property.
How do you handle SEO when properties are on separate domains but served from one application? Each property domain gets its own sitemap, its own robots.txt, its own structured data (Hotel schema markup), and its own meta tags. From Google's perspective, these are entirely separate websites. The canonical URLs point to each property's own domain. You also get the benefit of centralized schema markup generation — every property automatically gets proper JSON-LD for hotels, rooms, reviews, and local business information.
What about property-specific integrations like local activity booking or spa reservation systems? The component architecture supports property-level integration configuration. Each property config in the CMS can specify which third-party integrations it uses. The rendering layer conditionally includes those integration components. A spa property gets the spa booking widget. A downtown business hotel gets the meeting room configurator. These are loaded as dynamic imports so they don't affect bundle size for properties that don't use them.
Is there a risk of one property's traffic spike affecting other properties? On a platform like Vercel or Cloudflare Pages, not really. These edge platforms are designed for traffic spikes. Static pages are served from CDN cache, so a spike on one property doesn't consume server resources that would affect another. For dynamic routes (like real-time availability checks), you'd want rate limiting per property to prevent one property's viral moment from exhausting your booking engine API quotas. But that's an API-level concern, not a hosting concern.