11 — SEO and Metadata
PRD Document · Savoy Signature Hotels — Multi-Site Headless Platform
Version: 1.0 · Date: 2026-03-04
Related docs:06_Content_Modeling_Umbraco.md,10_MultiLanguage_and_i18n.md
1. Purpose
Section titled “1. Purpose”This document defines the SEO strategy and metadata implementation for the headless platform. It covers Next.js metadata generation, Open Graph (social sharing), dynamic sitemaps, robots.txt, canonical URLs, and structured data (Schema.org).
2. Metadata Architecture
Section titled “2. Metadata Architecture”Metadata is composed of base site configuration (defined in the siteRoot node) and page-specific overrides (defined in the seoComposition and openGraphComposition on each page).
2.1 Metadata Resolution Hierarchy
Section titled “2.1 Metadata Resolution Hierarchy”Next.js will construct the final <head> metadata in the following order of precedence:
- Page-specific override (e.g.,
metaTitleonroomDetailPage) - Dynamic generation (e.g., “Room Name — Hotel Name”)
- Site default (e.g.,
defaultMetaTitleonsiteRoot) - Hardcoded fallback (application level)
2.2 Next.js Metadata API Implementation
Section titled “2.2 Next.js Metadata API Implementation”The platform uses the Next.js App Router Metadata API.
import { Metadata } from 'next';
export async function generateMetadata({ params }): Promise<Metadata> { const { locale, slug } = await params; const site = getSiteConfig();
// Fetch page content const page = await client.getContentByPath(site.key, locale, slug.join('/')); if (!page) return {};
return { title: page.seo?.metaTitle || `${page.name} | ${site.name}`, description: page.seo?.metaDescription || site.defaultMetaDescription, keywords: page.seo?.metaKeywords,
// Canonical URL (self-referencing by default) alternates: { canonical: page.seo?.canonicalUrl || `https://${site.domain}/${locale}/${slug.join('/')}`, languages: generateHreflangMap(page.cultures, site.domain), },
// Robots directives robots: { index: !page.seo?.noIndex, follow: !page.seo?.noFollow, },
// Open Graph openGraph: { title: page.seo?.ogTitle || page.seo?.metaTitle || page.name, description: page.seo?.ogDescription || page.seo?.metaDescription, url: `https://${site.domain}/${locale}/${slug.join('/')}`, siteName: site.name, images: [ { url: page.seo?.ogImage?.url || site.defaultOgImage.url, width: 1200, height: 630, }, ], type: page.seo?.ogType || 'website', },
// Twitter Card twitter: { card: page.seo?.twitterCardType || 'summary_large_image', }, };}3. SEO Compositions in Umbraco
Section titled “3. SEO Compositions in Umbraco”As defined in 06_Content_Modeling_Umbraco.md, the following compositions control SEO per page.
3.1 seoComposition
Section titled “3.1 seoComposition”| Field | Behavior if empty |
|---|---|
metaTitle | Fallback to: `{Page Name} |
metaDescription | Fallback to siteRoot.defaultMetaDescription |
noIndex | Default: false (Indexed) |
noFollow | Default: false (Followed) |
metaKeywords | For reference only (not used by modern search engines) |
canonicalUrl | Fallback to current relative URL mapped to absolute domain |
3.2 openGraphComposition
Section titled “3.2 openGraphComposition”| Field | Behavior if empty |
|---|---|
ogTitle | Fallback to metaTitle → Page Name |
ogDescription | Fallback to metaDescription |
ogImage | Fallback to siteRoot.defaultOgImage |
ogType | Default: website (can be article for news) |
twitterCardType | Default: summary_large_image |
4. Robots.txt
Section titled “4. Robots.txt”The robots.txt file is generated dynamically per site, allowing granular control for each hotel domain.
4.1 Implementation
Section titled “4.1 Implementation”import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots { const site = getSiteConfig(); // Resolves based on host/headers
// Base rules const rules = [ { userAgent: '*', allow: '/', disallow: ['/api/', '/_next/', '/preview/'], } ];
// If STAGE/DEV environment, block all crawling if (process.env.APP_ENV !== 'production') { return { rules: [{ userAgent: '*', disallow: '/' }], }; }
return { rules, sitemap: `https://${site.domain}/sitemap.xml`, };}5. Sitemaps (XML)
Section titled “5. Sitemaps (XML)”Sitemaps are generated dynamically per site, indexing all published pt and en pages.
5.1 Generation Strategy
Section titled “5.1 Generation Strategy”- Dynamic: Generated via Next.js
sitemap.tscalling a custom Umbraco endpoint. - Multi-language: Includes
<xhtml:link rel="alternate">tags for hreflang mapping within the sitemap. - Caching: Sitemap is cached at the Cloudflare edge to prevent heavy DB hits.
- Exclusions: Pages marked
noIndex: trueare excluded entirely.
5.2 Sitemap Endpoint Response
Section titled “5.2 Sitemap Endpoint Response”The custom Umbraco endpoint (/api/sitemap/{siteKey}) returns a simplified node tree:
[ { "url": "/pt/alojamento", "lastModified": "2026-03-04T10:00:00Z", "priority": 0.8, "variants": [ { "locale": "pt", "path": "/pt/alojamento" }, { "locale": "en", "path": "/en/accommodation" } ] }]5.3 Next.js Implementation
Section titled “5.3 Next.js Implementation”import { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> { const site = getSiteConfig(); const nodes = await fetchSitemapData(site.key); // from Umbraco custom API
return nodes.map((node) => ({ url: `https://${site.domain}${node.url}`, lastModified: new Date(node.lastModified), changeFrequency: 'weekly', priority: node.priority || 0.5, alternates: { languages: node.variants.reduce((acc, v) => ({ ...acc, [v.locale]: `https://${site.domain}${v.path}` }), {}), }, }));}6. Structured Data (Schema.org)
Section titled “6. Structured Data (Schema.org)”JSON-LD structured data is injected into specific page types to generate rich snippets in Google Search results.
6.1 Hotel Schema (siteRoot / homePage)
Section titled “6.1 Hotel Schema (siteRoot / homePage)”Injected on the homepage of every hotel site.
{ "@context": "https://schema.org", "@type": "Hotel", "name": "Savoy Palace", "description": "Luxury 5-star hotel in Madeira", "image": "https://savoysignature.com/media/.../palace.jpg", "url": "https://savoysignature.com/savoypalacehotel", "telephone": "+351 291 213 000", "address": { "@type": "PostalAddress", "streetAddress": "Avenida do Infante 25", "addressLocality": "Funchal", "postalCode": "9004-542", "addressCountry": "PT" }, "starRating": { "@type": "Rating", "ratingValue": "5" }}6.2 Breadcrumb Schema (All inner pages)
Section titled “6.2 Breadcrumb Schema (All inner pages)”{ "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://savoysignature.com/savoypalacehotel/pt" }, { "@type": "ListItem", "position": 2, "name": "Alojamento", "item": "https://savoysignature.com/savoypalacehotel/pt/alojamento" } ]}6.3 Other Supported Schemas
Section titled “6.3 Other Supported Schemas”| Page Type | Schema Type | Key Properties Mapped |
|---|---|---|
roomDetailPage | HotelRoom | Name, Image, Max Guests, Bed Type, Amenities |
diningDetailPage | Restaurant | Name, Image, Cuisine, Opening Hours, Menu URL |
newsDetailPage | Article | Headline, Image, Date Published, Author |
faqPage | FAQPage | List of Question and Answer |
6.4 Schema Override via CMS (schemaComposition)
Section titled “6.4 Schema Override via CMS (schemaComposition)”While the platform generates schema automatically based on page type, editors have the ability to override or extend schema fields manually per page.
A dedicated schemaComposition provides the following fields:
schemaOverride(Boolean): If true, suppresses automatic generation.customJsonLd(Code Editor / JSON): Allows the editor to paste custom JSON-LD.- Key properties like “Hotel Telephone”, “Max Guests”, or “Address” mapped to automatic schemas must also be editable fields in Umbraco to ensure structured data is always accurate without requiring code deployments.
6.5 React Implementation
Section titled “6.5 React Implementation”import Script from 'next/script';
export function JsonLd({ schema }: { schema: Record<string, any> }) { return ( <Script id={`json-ld-${schema['@type']}`} type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }} /> );}7. URL Redirects Management
Section titled “7. URL Redirects Management”Editors can manage 301 (Permanent) and 302 (Temporary) redirects directly in Umbraco to handle legacy URLs or marketing campaigns.
7.1 Architecture
Section titled “7.1 Architecture”- Umbraco Redirect Tracker: Native tool tracks when a node is renamed/moved and auto-creates a 301.
- Custom Redirects Dashboard: Custom table in Umbraco for manual wildcard/regex redirects.
- Frontend Resolution:
proxy.tsqueries the Umbraco redirects API before rendering a 404.
7.2 Performance Consideration
Section titled “7.2 Performance Consideration”To avoid blocking requests, proxy.ts only checks for redirects if the local routing determines the path doesn’t match an active Next.js route or static file.
8. Acceptance Criteria
Section titled “8. Acceptance Criteria”-
<title>and<meta name="description">correctly populated on all pages, adhering to fallback hierarchy. - Open Graph (
og:*) and Twitter Card (twitter:*) tags present and valid. -
robots.txtdynamically servesDisallow: /on STAGE/DEV environments. -
sitemap.xmldynamically generated per site, including hreflang links, and excludingnoIndexpages. - JSON-LD Structured Data injected correctly on Homepage, Room details, Restaurant details, and FAQs.
- Breadcrumb JSON-LD corresponds exactly to the visual breadcrumbs component.
- Changing a page URL in Umbraco automatically generates a 301 redirect.
- Deleted pages return a 404 status code (not a soft 404/redirect to home).
Next document: 12_Forms_and_Data_Collection.md