Site configurationImplement domain-based locale routing

Implement domain-based locale routing

An agent-oriented specification for configuring locale ownership, domain redirects, locale-aware links, and multilingual CMS behavior in Creght.

Use this specification when an AI coding agent must configure one Creght site to serve different default languages on different domains. Treat the rules below as an implementation contract, not as optional UI guidance.

Required inputs

Collect these values before editing code:

  • The site-wide base defaultLocale. This is also the fallback layer for multilingual CMS fields.
  • The complete site-wide locales list.
  • Every hostname that is already bound to this same Creght site.
  • The unprefixed default locale owned by each hostname.
  • Any additional prefixed locales that each hostname must serve locally.
  • Whether automatic locale detection should be enabled.

Hard requirement: i18n.domains does not bind DNS or attach domains to a site. Every configured hostname MUST already be bound in Settings → Domains. If it is not bound, the request never reaches this site's locale router.

Canonical configuration

Write i18n as a plain object in talizen.config.ts. Do not import a configuration helper.

export default {
  i18n: {
    defaultLocale: 'zh-CN',
    locales: ['zh-CN', 'en'],
    localeDetection: false,
    domains: [
      {
        domain: 'www.example.cn',
        defaultLocale: 'zh-CN',
        locales: ['zh-CN', 'en'],
      },
      {
        domain: 'www.example.com',
        defaultLocale: 'en',
        locales: ['en'],
      },
    ],
  },
}

Keep every domain locale inside the top-level locales array. Including a domain's own defaultLocale in domains[].locales is not required for routing, but SHOULD be done for an explicit, auditable configuration.

Routing decision algorithm

  1. Match the request hostname against i18n.domains[].domain.
  2. Use that entry's defaultLocale as the routing default for unprefixed paths on that hostname.
  3. If the URL contains an explicit locale prefix, verify that locale is supported site-wide.
  4. If the current domain serves the requested locale—through its defaultLocale or locales—serve it on the current domain.
  5. Otherwise, find the configured domain whose defaultLocale owns the requested locale and issue a temporary 307 redirect to that domain.
  6. If no configured domain owns the locale, keep the request on the current domain and serve it with its explicit locale prefix.

Do not confuse defaults: top-level i18n.defaultLocale remains the site's base content locale. A domain entry's defaultLocale controls only the unprefixed routing default for requests on that hostname.

Domain locale ownership

Single-language domain

Omit additional locales when the domain should serve only its own defaultLocale. Requests for a locale owned by another domain redirect there.

Multilingual domain

List every additional locally served locale in domains[].locales. These locales remain on this host and use explicit path prefixes.

Unprefixed path

example.fr/about resolves to French when that domain declares defaultLocale: 'fr'. Do not add a redundant /fr prefix.

Explicit locale path

example.cn/en/about stays on example.cn only when that domain also serves en; otherwise it can redirect to the domain that owns English.

Internal links MUST use Talizen locale-aware navigation. Never concatenate /${locale} manually and never infer locale from window.location.pathname.

import { Link, localizedPath } from 'talizen'

// Preferred for rendered internal navigation.
<Link href='/about'>About</Link>

// Use when a component requires a raw URL string.
const href = localizedPath('/about', locale)

When the target locale is the current domain's unprefixed default, Link and localizedPath() correctly omit the locale prefix. A missing prefix in that case is expected behavior, not a bug.

Language switcher

A language switcher SHOULD preserve the current locale-less route instead of sending every user to the home page.

import { Link } from 'talizen'
import { getLocalePath } from 'talizen/router'

<Link href={getLocalePath()} locale='en'>English</Link>

Read locale and CMS content correctly

On the server, read the resolved locale from context.locale or getLocale(). In React components, use useLocale(). Do not parse the URL because the router can strip locale prefixes before page code runs.

import { getLocale } from 'talizen'
import { getContent } from 'talizen/cms'

export async function getServerSideProps() {
  const { locale } = getLocale()
  const page = await getContent('pages', 'about', {})
  return { props: { locale, page } }
}

talizen/cms returns fields already decoded for the current locale. Read the normal fields directly. Do not manually merge _i18n inside page code.

Acceptance matrix

For the canonical configuration above, verify this exact behavior:

www.example.cn/about       → zh-CN, 200, no locale prefix
www.example.cn/en/about    → en, 200, remains on .cn
www.example.com/about      → en, 200, no locale prefix
www.example.com/zh-CN/about → redirect or prefixed output according to domain ownership

If en is removed from www.example.cn.locales, then www.example.cn/en/about SHOULD return a 307 redirect to www.example.com/about.

Implementation checklist

  • Confirm every domain is bound to the same site before changing talizen.config.ts.
  • Confirm all domain defaults and additional domain locales exist in top-level i18n.locales.
  • Keep the config as a plain object; do not introduce Next.js configuration helpers.
  • Use Talizen Link or localizedPath() for internal links.
  • Use getLocale(), context.locale, or useLocale() instead of parsing URLs.
  • Test unprefixed and prefixed routes on every configured hostname.
  • Verify redirect status, destination hostname, path preservation, query preservation, and absence of redirect loops.
  • Verify CMS content resolves to the same locale selected by routing.

Forbidden implementations

  • Do not add react-router-dom, Next.js routing packages, or custom locale routers.
  • Do not hardcode /en, /fr, or another prefix into shared navigation components.
  • Do not treat top-level defaultLocale as if it changes per domain.
  • Do not assume that adding i18n.domains also binds or verifies a hostname.
  • Do not duplicate CMS entries per locale; use field-level translations in one stable content item.
  • Do not convert expected 307 locale redirects into permanent redirects unless the product's routing contract explicitly changes.

Render diagnostics