Cross-Domain Canonicals for Multi-Tenant Headless
Emit the correct per-tenant canonical when one headless build serves many domains, so each tenant’s pages self-reference their own host and syndicated copies point back to the source.
When to Use This Approach
Use per-tenant canonical resolution when your setup shows any of these traits:
- A single build or deployment serves multiple customer domains, and a global
SITE_URLwould stamp every tenant with the same host. - Some tenants republish content from another tenant, so certain pages should canonicalize across domains to the original.
- A shared preview or platform domain is leaking into production canonical tags, splitting signals between the tenant domain and the platform host.
Implementation Steps
Step 1: Resolve the Tenant Domain at the Edge
Read the Host header at the edge and map it to a tenant identifier before the origin renders. Resolving at the edge means the tenant context is known on the first byte, which is the same layer where canonical URL enforcement already runs.
// edge/resolve-tenant.ts (Cloudflare Worker / Vercel Edge)
const TENANTS: Record<string, string> = {
'tenant-a.com': 'https://tenant-a.com',
'tenant-b.com': 'https://tenant-b.com',
};
export function resolveBaseUrl(host: string): string {
const clean = host.replace(/:\d+$/, '').toLowerCase();
const base = TENANTS[clean];
if (!base) throw new Error(`Unknown tenant host: ${clean}`);
return base;
}
Validation: curl -s -H 'Host: tenant-a.com' https://origin.example/blog/my-post | grep -oiE 'rel="canonical"[^>]*' must show a tenant-a.com host.
Step 2: Map Each Tenant to Its Production Base URL
Select the base URL from the resolved tenant, never from a single global environment variable. Pass the resolved base into the same canonical builder your framework already uses.
// lib/canonical.ts
export function tenantCanonical(baseUrl: string, path: string): string {
const clean = `/${path.replace(/^\/+|\/+$/g, '').toLowerCase()}`;
return `${baseUrl}${clean}`;
}
// e.g. tenantCanonical('https://tenant-a.com', 'blog/my-post')
// -> 'https://tenant-a.com/blog/my-post'
Validation: unit-test the builder with two tenants and the same path; assert the two outputs differ only by host.
Step 3: Point Syndicated Copies at the Source
When a tenant republishes another tenant’s content, emit a canonical to the original source URL instead of a self-canonical, so signals consolidate on the source.
// entry.syndicatedFrom holds the source absolute URL, or null for originals
export function resolveCanonical(entry: {
path: string;
syndicatedFrom?: string | null;
}, baseUrl: string): string {
if (entry.syndicatedFrom) return entry.syndicatedFrom; // cross-domain canonical
return tenantCanonical(baseUrl, entry.path); // self-canonical
}
Validation: curl a syndicated page and confirm its canonical host is the source tenant, not the republishing tenant.
Step 4: Validate the Canonical Per Tenant
Fetch each tenant domain directly and confirm the canonical host matches that tenant, with no shared build or preview host leaking through.
# Check the same route on two tenant domains
for host in tenant-a.com tenant-b.com; do
echo "== $host =="
curl -s "https://$host/blog/my-post" \
| grep -oiE '<link[^>]+rel="canonical"[^>]*>'
done
Validation: each tenant’s output must show its own host; a preview or platform host in either result is a leak to fix.
Step 5: Diff Canonicals Across Tenants in CI
Render a shared route on every tenant domain in CI and assert each canonical host equals its own tenant so a regression cannot ship.
# Fails the job if any tenant's canonical host is wrong
node scripts/assert-tenant-canonicals.js \
--route /blog/my-post \
--tenants tenant-a.com,tenant-b.com
Validation: the script exits non-zero when a canonical host does not match the tenant it was fetched from; wire that into the deploy gate.
SEO Impact Summary
| Signal | What improves | What breaks if misconfigured |
|---|---|---|
| Per-tenant self-canonical | Each domain consolidates its own signals on its own host | A global base URL stamps every tenant with one host and merges rankings |
| Syndication consolidation | Cross-domain canonicals send equity to the source copy | Self-referencing copies compete across domains as duplicates |
| Host isolation | Edge resolution keeps preview/platform hosts out of tags | A leaked shared host splits indexation between two domains |
| Regression safety | CI diff catches a wrong-host canonical before release | An unguarded build ships mismatched canonicals to every tenant |
Measurable signals to watch:
- Per-property GSC: each tenant’s “Duplicate, Google chose different canonical” count should stay at zero.
- Cross-domain crawl: syndicated copies should report the source URL as
googleCanonicalin URL Inspection. - CI diff: the wrong-host assertion should never fail on the main branch.
Edge Cases and Gotchas
Localized tenants overlap with hreflang When a tenant operates several locale variants, the canonical is per-locale self-referencing while the locale set is expressed separately. Keep the two concerns distinct and follow hreflang and international routing so a locale variant is not mistaken for a cross-domain duplicate.
Apex and subdomain host variants per tenant Each tenant may resolve on both its apex domain and a leading-subdomain variant of the same domain. Redirect one to the other with a 301 before the canonical is computed, and store only the chosen host in the tenant map so the builder never emits the losing variant.
Custom domains added after launch Tenants that attach their own domains later will 500 or misresolve if the tenant map is static and unshipped. Load the map from a source that updates without a full redeploy, and default an unknown host to a hard failure rather than a wrong self-canonical.
Shared preview domain leakage
A platform-wide preview host (*.preview.platform.dev) can render every tenant and, if unguarded, stamp its own host into canonicals. Exclude preview hosts from indexing and never let them into a sitemap; the safe path is the same first-fix as in fixing canonical mismatches in Next.js App Router — audit the raw HTML per environment.
Frequently Asked Questions
Can a canonical point to another domain? Yes. A cross-domain canonical tells search engines the authoritative copy lives on a different host, which is the correct signal for syndicated or republished content. The target must be a live, indexable URL that returns 200 and itself self-references, otherwise the signal is ignored and both copies may be indexed.
How do I resolve tenant domain in a shared build?
Read the incoming Host header at the edge and map it to a tenant identifier, then select that tenant’s base URL from a tenant-to-domain map. Never derive the canonical from a single global environment variable, because one shared build serving many domains would stamp every tenant with the same host.
Do syndicated copies need canonicals to the original? Yes. When a tenant republishes another tenant’s content the copy should carry a canonical to the original source URL so ranking signals consolidate on the source. If each copy self-references instead, search engines see competing duplicates across domains and may select the wrong one.
What happens if the cross-domain target is blocked or 404s?
The signal is discarded and the copy can be indexed on its own host, reintroducing the duplicate you were trying to consolidate. Before pointing a canonical at a source URL, confirm that URL returns 200, is not noindex, and self-references so the chain terminates cleanly.
Part of: Canonical URL Enforcement
Related
- Canonical URL Enforcement — the multi-layer approach to declaring and enforcing canonicals across a headless stack
- Fixing Canonical Mismatches in Next.js App Router — resolve Google-chosen-canonical mismatches at the framework layer
- hreflang & International Routing in Headless — keep locale variants distinct from cross-domain duplicates