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_URL would 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.
Per-tenant canonical resolution A shared build passes each request through an edge resolver that reads the Host header and maps it to a tenant, so tenant A and tenant B each receive a canonical on their own domain. Shared Build one origin Edge resolver Host → tenant tenant-a.com self-canonical tenant-b.com self-canonical

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 googleCanonical in 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