# Regions and languages

> Build a website with several regions or languages: address patterns and the folders for each, reading the right region and language, fallbacks and notices, hreflang, language switchers, previews and cache refresh per region.

Source: https://docs.bifrostorbit.com/cms/developers/regions-and-languages/

A space can have several regions, such as Germany and Switzerland, each with one or more languages, such as `de-DE` and `en-DE`. The CMS keeps their content apart; your website decides how visitors reach each one. This page shows how. It builds on [Connect a website](https://docs.bifrostorbit.com/cms/developers/connect-a-website/).

## How the space is set up

Open **Configuration › Environments** and an environment to see its regions. Each region has:

- **a key**, such as `germany`, used by the API and by webhooks;
- **a short code**, such as `de`, which the API also accepts;
- **an address prefix**, such as `/de`, when several regions share one domain;
- **languages**, one of them the default, each optionally with a fallback language.

**Configuration › Domains** says which domain each environment, region and language is published on. The CMS uses it, with the prefix, to build full addresses for canonical links, hreflang and sitemaps. It does not route visitors: your website does.

Read the regions your key may see:

```ts
const regions = await cms.getRegions()
// [{ key: 'germany', name: 'Germany', locales: [{ code: 'de-DE', name: 'German', isDefault: true }, { code: 'en-DE', name: 'English', isDefault: false }] }]

const locales = await cms.getLocales('germany')
// [{ code: 'de-DE', name: 'German', isDefault: true }, …]
```

`getLocales()` without a region uses the client's `defaultRegion`, or lists every language if there is none. A key limited to some regions or languages only sees those, and gets `401` for the others.

## Ask for a region and language

Every page call takes `region` and `locale`:

```ts
await cms.resolve({ path: '/services', region: 'germany', locale: 'en-DE' })
```

| Option | When left out |
| --- | --- |
| `region` | The client's `defaultRegion`, or the space's first region if that is not set |
| `locale` | The region's default language |

The page's address is the same whatever the region prefix: `/services`, not `/de/services`. Your route takes the prefix off before asking.

Always pass the region's **key**, not its short code. Pages are cached under a tag with the region you pass, and webhooks name the region by its key; see [Caching and refreshing pages](https://docs.bifrostorbit.com/cms/developers/caching-and-refresh/).

## Choose an address pattern

| Pattern | Example | Folders |
| --- | --- | --- |
| One region, one language | `example.com/services` | `app/[[...slug]]/page.tsx`, as in the tutorial |
| One region, several languages | `example.com/en/services` | `app/[lang]/[[...slug]]/page.tsx` |
| Several regions on one domain | `example.com/de/services` | `app/[region]/[[...slug]]/page.tsx` |
| Regions and languages on one domain | `example.com/de/en/services` | `app/[region]/[lang]/[[...slug]]/page.tsx` |
| A domain per region | `example.de/services`, `example.ch/services` | `app/[region]/[[...slug]]/page.tsx` with a proxy, or one deployment per domain |

Whichever you choose, set the same prefixes on the regions in the CMS and the same domains in **Configuration › Domains**, so the addresses the CMS builds match your website's.

## Several regions on one domain

Keep a map from the address prefix to the region key in one place:

```ts
// lib/regions.ts
export const REGIONS = {
  de: { key: 'germany', lang: 'de-DE' },
  ch: { key: 'switzerland', lang: 'de-CH' },
} as const

export type RegionPrefix = keyof typeof REGIONS

export function regionFor(prefix: string) {
  return prefix in REGIONS ? REGIONS[prefix as RegionPrefix] : null
}
```

The folders:

```text
app/
├── [region]/
│   ├── layout.tsx              <html lang>, menu and footer for the region
│   ├── not-found.tsx
│   └── [[...slug]]/
│       └── page.tsx
├── api/…                       unchanged
└── page.tsx                    / redirects to the default region
```

The layout, with the region's menu and language:

```tsx
// app/[region]/layout.tsx
import type { ReactNode } from 'react'
import { notFound } from 'next/navigation'
import { CmsBridge } from '@bifrost-orbit-cms/sdk/next'

import { cms } from '@/lib/cms'
import { regionFor } from '@/lib/regions'

type Props = { children: ReactNode; params: Promise<{ region: string }> }

export default async function RegionLayout({ children, params }: Props) {
  const region = regionFor((await params).region)
  if (!region) notFound()
  const menu = await cms.findNavigation({ key: 'main', region: region.key }).catch(() => null)

  return (
    <html lang={region.lang}>
      <body>
        {/* menu from `menu` */}
        {children}
        <CmsBridge client={cms} />
      </body>
    </html>
  )
}
```

When each region has its own `<html lang>`, the region layout is the root layout: remove `app/layout.tsx` and put `<html>` and `<body>` here, as above. Otherwise keep `app/layout.tsx` and leave `<html>` out of this one.

The page route:

```tsx
// app/[region]/[[...slug]]/page.tsx
type Props = { params: Promise<{ region: string; slug?: string[] }> }

async function resolveFor({ params }: Props) {
  const { region: prefix, slug = [] } = await params
  const region = regionFor(prefix)
  if (!region) notFound()
  const { isEnabled } = await draftMode()
  return load(`/${slug.join('/')}`, region.key, isEnabled)
}

const load = cache(async (path: string, region: string, preview: boolean) => cms.resolve({ path, region, preview }))
```

The rest of the page route is as in the tutorial. Redirect targets from `resolve` are addresses within the region, such as `/services/new`; add the prefix back before redirecting: `redirect('/' + prefix + result.to)` for addresses starting with `/`.

The home page:

```tsx
// app/page.tsx
import { redirect } from 'next/navigation'

export default function Home() {
  redirect('/ie')
}
```

To send visitors to their own region instead, read the `Accept-Language` header or a country header from your hosting in `proxy.ts`, and redirect `/` there.

Links between pages: links and menu items from the CMS carry addresses within the region, such as `/services`. Add the region prefix when rendering them, in one helper used by every link component:

```ts
export const withRegion = (prefix: string, href: string) => (href.startsWith('/') ? `/${prefix}${href === '/' ? '' : href}` : href)
```

## Several languages

Map the address segment to a language code the same way:

```ts
// lib/languages.ts
export const LANGUAGES = { de: 'de-DE', en: 'en-DE' } as const
```

and pass it as `locale`:

```ts
await cms.resolve({ path, region: 'germany', locale: LANGUAGES[lang] })
```

The default language can live at the root, without a prefix, by rewriting in `proxy.ts`: requests without a language prefix are rewritten to `/de/…` internally, so `app/[lang]` serves them while the address stays `/services`.

```ts
// proxy.ts
import { NextResponse, type NextRequest } from 'next/server'

const PREFIXES = ['de', 'en']

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl
  if (PREFIXES.some((prefix) => pathname === `/${prefix}` || pathname.startsWith(`/${prefix}/`))) return NextResponse.next()
  return NextResponse.rewrite(new URL(`/de${pathname}`, request.url))
}

export const config = { matcher: ['/((?!api/|_next/|.*\\..*).*)'] }
```

If you also need the preview redirect from the tutorial, do both in one `proxy` function: check for `?preview=` first.

## A domain per region

Two ways:

- **One deployment per domain.** Deploy the same code twice, with `CMS_DEFAULT_REGION=germany` on one and `CMS_DEFAULT_REGION=switzerland` on the other, and the single-region folders from the tutorial. Simplest; each deployment has its own webhook.
- **One deployment for every domain.** Keep `app/[region]/…` and rewrite by host name in `proxy.ts`, so `example.de/services` is served by `/de/services` internally and stays cached:

```ts
// proxy.ts
const HOSTS: Record<string, string> = { 'www.example.de': 'de', 'www.example.ch': 'ch' }

export function proxy(request: NextRequest) {
  const prefix = HOSTS[request.headers.get('host') ?? '']
  if (!prefix) return NextResponse.next()
  return NextResponse.rewrite(new URL(`/${prefix}${request.nextUrl.pathname}`, request.url))
}
```

With rewrites, links stay without a prefix: skip `withRegion`.

## Fallbacks and notices

When a page is not translated, the API can answer with another language, and when a region does not have a page, with another region. The space's settings decide; see [Space settings](https://docs.bifrostorbit.com/cms/editors/space-settings/). Every page says what happened:

```ts
page.meta.requestedLocale      // 'en-DE', what you asked for
page.meta.locale               // 'de-DE', what answered
page.meta.fallbackApplied      // true: another language answered
page.meta.requestedRegion      // 'germany'
page.meta.region               // 'germany'
page.meta.regionFallbackApplied // false
```

Show a notice so visitors know:

```tsx
{page.meta.fallbackApplied ? <p role="note">This page is not available in English yet, so it is shown in German.</p> : null}
```

Set `lang` on the content to the language that answered, so screen readers pronounce it correctly: `<main lang={page.locale}>`.

To refuse fallbacks for a request, pass `strict: true`. A page missing in the language or region asked for then answers `notFound`. Use it for content where showing another language would be wrong, such as legal terms:

```ts
await cms.resolve({ path, region: 'germany', locale: 'en-DE', strict: true })
```

Globals follow the language fallback but not the region fallback, and lists never fall back. If a list is empty in a language, read it in the default language yourself.

## hreflang and language switchers

`toMetadata(page)` adds `alternates.languages` from `page.seo.alternates`, so Next.js writes a `<link rel="alternate" hreflang>` for each language and region the page exists in. Nothing else is needed for search engines. Which versions are linked, and how x-default is chosen, is in [Languages, regions and search engines](https://docs.bifrostorbit.com/cms/editors/languages-in-search/); SEO health checks the live pages about once a day.

The same list builds a language switcher:

```tsx
import { hreflang } from '@bifrost-orbit-cms/sdk/seo'

const alternates = hreflang(page) // { 'de-DE': 'https://www.example.com/de/services', 'en-DE': 'https://www.example.com/de/en/services' }

<ul>
  {page.seo.alternates.map((entry) => (
    <li key={entry.hreflang}>
      <a href={new URL(entry.href).pathname} hrefLang={entry.hreflang} lang={entry.hreflang}>
        {entry.hreflang}
      </a>
    </li>
  ))}
</ul>
```

The addresses are built from **Configuration › Domains** and the region prefixes, so they are only right when those match your website. Use the path only, as above, so the switcher also works on staging.

Give the languages their own names with `getLocales(region)`: each has `code` and `name`.

## Previews

The editor opens the **Preview URL** from the space's settings followed by the page's address, which has no region prefix. For a website with region prefixes:

1. Set **Preview URL** to the default region, such as `https://staging.example.com/de`.
2. Add each other region under **Other preview URLs**, such as `https://staging.example.com/ch`, named after the region.
3. When editing a page in another region, choose its address in the list above the preview.

The preview key must be allowed to read every region editors preview.

## Sitemaps and robots

Each region and language gets its own sitemap at its own address, such as `/de/sitemap.xml`, listed in a sitemap index. See [Metadata, sitemaps and robots](https://docs.bifrostorbit.com/cms/developers/seo-with-the-sdk/).

## Cache refresh

Webhook messages name each page's region key, and the SDK refreshes `cms:page:<region>:<path>`. As long as your website passes the region key, publishing a page in one region refreshes only that region's copy. Pages shown through the region fallback catch up on expiry; see [Caching and refreshing pages](https://docs.bifrostorbit.com/cms/developers/caching-and-refresh/).
