Creation:2026-04-02Last update:2026-04-02

    Custom Domains

    Intlayer supports domain-based locale routing, allowing you to serve specific locales from dedicated hostnames. For example, Chinese visitors can be directed to intlayer.zh instead of intlayer.org/zh.

    How It Works

    The domains map in routing associates each locale with a hostname. Intlayer uses this map in two places:

    1. URL generation (getLocalizedUrl): when the target locale lives on a different domain than the current page, an absolute URL is returned (e.g. https://intlayer.zh/about). When both domains match, a relative URL is returned (e.g. /fr/about).
    2. Server proxy (Next.js & Vite): incoming requests are redirected or rewritten based on the domain they arrive on.

    Exclusive vs. shared domains

    The key distinction is exclusivity:

    • Exclusive domain - only one locale maps to that hostname (e.g. zh → intlayer.zh). The domain itself identifies the locale, so no locale prefix is added to the path. https://intlayer.zh/about serves Chinese content.
    • Shared domain - multiple locales map to the same hostname (e.g. en and fr both map to intlayer.org). Normal prefix-based routing applies. intlayer.org/fr/about serves French content.

    Configuration

    intlayer.config.ts
    import { Locales, type IntlayerConfig } from "intlayer";const config: IntlayerConfig = {  internationalisation: {    locales: [      Locales.ENGLISH,      Locales.FRENCH,      Locales.SPANISH,      Locales.CHINESE,    ],    defaultLocale: Locales.ENGLISH,  },  routing: {    mode: "prefix-no-default",    domains: {      // Shared domain - en and fr use prefix routing on intlayer.org      en: "intlayer.org",      // Exclusive domain - zh has its own hostname, no /zh/ prefix needed      zh: "intlayer.zh",    },  },};export default config;

    Locales that are not listed in domains continue to use the standard prefix routing without any domain override.

    URL Generation

    getLocalizedUrl automatically produces the correct URL type based on the calling context.

    Same-domain locale (relative URL)

    ts
    // Current page: intlayer.org/aboutgetLocalizedUrl("/about", "fr", { currentDomain: "intlayer.org" });// → "/fr/about"getLocalizedUrl("/about", "en", { currentDomain: "intlayer.org" });// → "/about"  (default locale, no prefix)

    Cross-domain locale (absolute URL)

    ts
    // Current page: intlayer.org/aboutgetLocalizedUrl("/about", "zh", { currentDomain: "intlayer.org" });// → "https://intlayer.zh/about"  (exclusive domain, no /zh/ prefix)

    Serving from the locale's own domain

    ts
    // Current page: intlayer.zh/aboutgetLocalizedUrl("/about", "zh", { currentDomain: "intlayer.zh" });// → "/about"  (already on the correct domain - relative URL)getLocalizedUrl("/about", "fr", { currentDomain: "intlayer.zh" });// → "https://intlayer.org/fr/about"  (cross-domain link back to intlayer.org)

    Current domain auto-detection

    currentDomain is optional. When omitted, getLocalizedUrl resolves it in this order:

    1. The hostname of an absolute input URL (e.g. https://intlayer.org/aboutintlayer.org).
    2. window.location.hostname in browser environments.
    3. If neither is available (SSR without explicit option), a relative URL is returned for same-domain locales and no absolute URL is produced - this is the safe fallback.
    ts
    // Browser - window.location.hostname === 'intlayer.org'getLocalizedUrl("/about", "zh");// → "https://intlayer.zh/about"  (auto-detected from window)// From an absolute URL - domain detected automaticallygetLocalizedUrl("https://intlayer.org/about", "zh");// → "https://intlayer.zh/about"

    getMultilingualUrls with domains

    getMultilingualUrls calls getLocalizedUrl for every locale, so it produces a mix of relative and absolute URLs depending on the caller's domain:

    ts
    // currentDomain: 'intlayer.org'getMultilingualUrls("/about", { currentDomain: "intlayer.org" });// {//   en: "/about",//   fr: "/fr/about",//   es: "/es/about",//   zh: "https://intlayer.zh/about",// }

    These absolute URLs are ready to use in <link rel="alternate" hreflang="..."> tags for SEO.

    Proxy Behaviour

    Next.js

    The intlayerProxy middleware handles domain routing automatically. Add it to your middleware.ts:

    middleware.ts
    export { intlayerProxy as default } from "next-intlayer/proxy";export const config = {  matcher: "/((?!api|static|assets|robots|sitemap|.*\\..*|_next).*)",};

    Redirect - request arrives on the wrong domain for a given locale prefix:

    plaintext
    GET intlayer.org/zh/about→ 301 https://intlayer.zh/about

    Rewrite - request arrives on the locale's exclusive domain without a prefix:

    plaintext
    GET intlayer.zh/about→ rewrite to /zh/about  (internal Next.js routing only, URL stays clean)

    Vite

    The intlayerProxy Vite plugin applies the same logic during development:

    vite.config.ts
    import { defineConfig } from "vite";import { intlayerProxy } from "vite-intlayer";export default defineConfig({  plugins: [intlayerProxy()],});
    Note: in local development you are typically on localhost, so cross-domain redirects will point to the live domains rather than another local port. Use a hosts-file override (e.g. 127.0.0.1 intlayer.zh) or a reverse proxy if you need to test multi-domain routing locally.

    Locale Switcher

    The useLocale hook from next-intlayer handles domain-aware navigation automatically. When a user switches to a locale on a different domain the hook performs a full-page navigation (window.location.href) instead of a client-side router push, because the Next.js router cannot cross origins.

    components/LocaleSwitcher.tsx
    "use client";import { useLocale } from "next-intlayer";export const LocaleSwitcher = () => {  const { availableLocales, locale, setLocale } = useLocale();  return (    <ul>      {availableLocales.map((l) => (        <li key={l}>          <button            onClick={() => setLocale(l)}            aria-current={l === locale ? "true" : undefined}          >            {l.toUpperCase()}          </button>        </li>      ))}    </ul>  );};

    No extra configuration is required - useLocale detects window.location.hostname internally and decides between router.replace (same domain) and window.location.href (cross-domain).

    Domain-based routing is commonly used together with hreflang to tell search engines which URL to index for each language. Use getMultilingualUrls to generate the full set of alternate URLs:

    app/[locale]/layout.tsx
    import { getMultilingualUrls } from "intlayer";import type { Metadata } from "next";export const generateMetadata = (): Metadata => {  const alternates = getMultilingualUrls("/", {    currentDomain: process.env.NEXT_PUBLIC_DOMAIN, // e.g. "intlayer.org"  });  return {    alternates: {      languages: alternates,    },  };};

    This produces:

    html
    <link rel="alternate" hreflang="en" href="https://intlayer.org/" /><link rel="alternate" hreflang="fr" href="https://intlayer.org/fr/" /><link rel="alternate" hreflang="es" href="https://intlayer.org/es/" /><link rel="alternate" hreflang="zh" href="https://intlayer.zh/" />

    Core Utilities

    Utility Description
    getLocalizedUrl(url, locale, { currentDomain }) Returns relative or absolute URL depending on whether the target locale is on the current domain.
    getMultilingualUrls(url, { currentDomain }) Returns a locale-keyed map of localised URLs, mixing relative and absolute as needed.
    getPrefix(locale, { domains }) Returns an empty prefix for exclusive-domain locales, normal prefix otherwise.