Routing helpers
Locale-aware links, preview and live preview helpers.
On this page
The gateway routes custom Admin hosts and locales, and @systhemaui/next/locale provides the routing and navigation helpers your own code uses. This page also covers how Preview and Live Preview keep their locale.
Custom Admin host matchingLink to this section
Custom admin domain matching uses Host, falling back to request.nextUrl.hostname when the header is empty or invalid. It ignores x-forwarded-host by default. This restores public-host routing when Next.js constructs its request URL from the internal bind address. Matching ignores letter case and ports on both sides, including localhost on any port and bracketed IPv6 such as [::1]:3000. With customAdminURL configured, /admin on other hosts still rewrites to /not-found; without it, /admin remains available on the site host. Host routing selects a virtual host; Payload authentication still controls access to the CMS.
trustForwardedHostLink to this section
Enable trustForwardedHost: true only if the proxy replaces Host with an internal upstream host and supplies the public host in x-forwarded-host:
export const proxy = systhemaGateway({
customAdminURL: process.env.ADMIN_URL,
trustForwardedHost: true,
})With this option, the first comma-separated forwarded value takes precedence over Host. Each candidate is trimmed and parsed as a hostname; an empty or invalid first forwarded value falls back to Host, then the request URL. The proxy must overwrite client-supplied x-forwarded-host, including any client-supplied list, and the origin must only accept traffic through that proxy. Enabling the option on a directly reachable server lets clients select admin versus public routing. This option applies to custom-admin routing only; the locale middleware retains next-intl's header policy.
Choose the option from the proxy's actual header configuration:
- Direct
next start, localhost, and proxies preserving the publicHostneed no opt-in. This includes a standard Railway deployment. Vercel documentsx-forwarded-hostas identical toHost(opens in new tab), so that setup needs no opt-in either. - nginx defaults to the upstream
Host(opens in new tab). Prefer preserving the public host withproxy_set_header Host $host;. If the upstream requires its internal host, setproxy_set_header X-Forwarded-Host $host;and enable the option. - Caddy overwrites forwarded headers by default (opens in new tab). When Caddy preserves
Host, leave the option off. An HTTPS upstream configuration that replacesHostneeds the option if the public host is only available inx-forwarded-host. - For Cloudflare Tunnel or a custom proxy chain, inspect the headers reaching Node. The platform name alone does not establish whether an opt-in is needed or safe.
Routing and navigation helpersLink to this section
Use createSysthemaRouting() and createSysthemaNavigation() from @systhemaui/next/locale for routing and navigation helpers, and the advanced createSysthemaLocaleProxy() middleware factory from @systhemaui/next/locale/proxy if you need to compose the next-intl middleware yourself. src/proxy.ts calls systhemaGateway({ customAdminURL }) from @systhemaui/next/gateway — no locales/localeMiddleware argument; the gateway resolves them itself from the withSysthema-injected env (falling back to a locale-off no-op when it's absent, and still accepting an explicit options.locales/options.localeMiddleware override for tests or advanced setups). It composes custom-admin routing before locale negotiation and preserves Preview and Live Preview handling.
Preview and Live PreviewLink to this section
The explicit preview locale of Preview and Live Preview overrides a stale NEXT_LOCALE cookie, is validated against locales.supported, and keeps all stable-document and project query parameters through next-intl redirects. With prefix routing, a relative Live Preview can remain on a configured custom Admin origin. With domain routing, the explicit Preview locale selects that locale's canonical origin and preserves the Preview query. If the Admin and selected frontend are on different registrable domains, use Payload client Live Preview unless authenticated draft-cookie scope has been deliberately solved; never weaken Preview secret or user authentication. See Cross-domain preview.
Routing modesLink to this section
The Next-only starter sets adminRouting: false, so ordinary routes such as /admin are not reserved when Payload is absent. always is the multi-locale default and prefixes every locale; as-needed keeps the default locale unprefixed. A domain map instead gives every locale an unprefixed path on its own canonical origin. An exact-one-locale contract stays unprefixed with detection disabled. The non-unique never mode is not part of Systhema's path-routing contract; Systhema uses it only in the generated one-locale-per-domain projection where each origin is already unique.
The Payload locale boundaryLink to this section
For Payload sites, the managed boundary layout at app/(site)/(systhema)/[[...segments]]/layout.tsx is the real locale boundary — no literal [locale] folder is needed. A managed not-found.tsx sits beside it, re-exporting the consumer's app/(site)/not-found.tsx: a thrown notFound() renders the nearest not-found boundary, and only one below the boundary layout renders inside the site shell — with RootLayout's CSS, event listeners, and the requested locale's strings and dir (keep customizing the 404 in app/(site)/not-found.tsx; the managed file just repositions it). The layout is Systhema-owned (do-not-modify, regenerated by sync) and contains the only locale-aware line: const route = await resolveSysthemaLocaleRoute((await params).segments) (from @systhemaui/payload/next; a no-op returning the default locale when locales is off), injected as locale into the consumer-owned app/(site)/shell.tsx, which passes it to RootLayout, RootHeader, and RootFooter. Those components derive everything else from config internally — RootLayout sets both <html lang> and dir (via getSysthemaLocaleDirection) and resolves the frontend message catalog from systhema.config.ts (messages.catalogs + messages.fallback) whenever the frontendMessages prop is absent, and renders SysthemaProvider so client children can read strings via useSysthemaFrontendTranslator. The catch-all page.tsx itself stays the plain, locale-unaware baseline — SysthemaPage, generateSysthemaStaticParams, and generateSysthemaMetadata resolve locale internally: with locales on, static params fan out across every supported locale so locale-specific slugs remain statically renderable and revalidate with the existing ISR setting, and metadata resolves/strips the locale segment before lookup. The shell is converted once and then fully project-owned: sync preserves your customizations (while freely regenerating everything inside (systhema)), and disabling locales refuses to discard a customized shell until you move those changes back to template.tsx yourself.