---
title: "Multi-Site & URL Configuration"
manual: "Headless"
version: "main"
permalink: "https://docs.typo3.org/permalink/friendsoftypo3/headless:multisite@main"
source: "Configuration/MultiSite.rst"
rendered: "2026-10-14T09:29:03+00:00"
---

# Multi-Site & URL Configuration {#multisite}

The headless setup typically has **two domains** — one for the API
(TYPO3 backend) and one for the frontend app. This page is the single
source of truth for how `EXT:headless` rewrites URLs, manages cookies
and routes assets across sites.

## Glossary {#glossary}

| Key | Meaning |
| --- | --- |
| `base` | Public URL of the TYPO3 site (the API). |
| `frontendBase` | Public URL of your SPA / frontend app. Used by `UrlUtility` to rewrite links in the JSON response. |
| `frontendApiProxy` | Public URL of the API as seen by browsers going through the frontend's reverse proxy (e.g. `https://example.com/headless`). |
| `frontendFileApi` | Public URL for processed files (images, PDFs). Used together with the `headless.storageProxy` feature flag. |
| `cookieDomain` | Domain to scope session cookies to. Needed when API and frontend share a root domain. |
| `baseVariants` | Per-environment overrides of any of the above, gated by an expression-language condition. |

## Single-domain setup {#single-domain-setup}

API and frontend both on the same host. No URL rewriting needed.

```yaml
# config/sites/<identifier>/config.yaml
rootPageId: 1
base: https://example.com
headless: 1
```

## Two-domain setup (typical) {#two-domain-setup-typical}

API on `api.example.com`, frontend on `example.com`. Set
`frontendBase` so URLs in the JSON response point at the frontend.

```yaml
rootPageId: 1
base: https://api.example.com
headless: 1
frontendBase: https://example.com
# Optional, used with the headless.storageProxy feature flag:
# processed-file URLs are served through these proxy paths
# (page/typolink URLs always use frontendBase):
frontendApiProxy: https://example.com/headless
frontendFileApi:  https://example.com/headless/fileadmin
```

Now any `typolink` or page URL in the JSON response is rewritten from
`https://api.example.com/...` to `https://example.com/...`.

## Multi-language overrides {#multi-language-overrides}

Each language can override the URL keys independently:

```yaml
languages:
  - languageId: 0
    title: English
    base: /
    locale: en_US.UTF-8
    frontendBase: https://example.com
  - languageId: 1
    title: Deutsch
    base: /de/
    locale: de_DE.UTF-8
    frontendBase: https://example.de
```

## Per-environment overrides (`baseVariants`) {#per-environment-overrides-basevariants}

Same site, different dev/stage/prod values:

```yaml
baseVariants:
  - base: https://api.dev.example.com
    condition: 'applicationContext == "Development"'
    frontendBase: https://dev.example.com
    frontendApiProxy: https://dev.example.com/headless
  - base: https://api.stage.example.com
    condition: 'applicationContext == "Staging"'
    frontendBase: https://stage.example.com
    frontendApiProxy: https://stage.example.com/headless
```

`condition` is evaluated by `TYPO3CMSCoreExpressionLanguageResolver`
with the `site` scope — available are the default variables
(`applicationContext`, `typo3`, `date`, `features`) and functions like
`getenv("…")`; `request` is **not** available here.

The first variant whose condition matches wins, and its values are read
verbatim: a key missing from the matching variant resolves to an empty
string, it does **not** fall back to the site-level value — repeat every
key in every variant.

## Shared root domain & cookies {#shared-root-domain-cookies}

When `api.example.com` and `example.com` share a root domain
(`example.com`), the browser can carry session cookies between them
**only if** the cookie's `Domain` attribute is set to the shared root.

Option A — set globally (simple, single-site instance):

```php
// config/system/additional.php
$GLOBALS['TYPO3_CONF_VARS']['BE']['cookieDomain'] = '.example.com';
```

Option B — per-site (multi-domain instance): enable
`headless.cookieDomainPerSite` and put `cookieDomain` in the site
config. The `CookieDomainPerSite` middleware looks up the site by
**exact request host** and injects its `cookieDomain` for the
duration of the request.

```php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['headless.cookieDomainPerSite'] = true;
```

```yaml
baseVariants:
  - base: https://api.example.com
    condition: 'applicationContext == "Production"'
    cookieDomain: .example.com
```

> [!IMPORTANT]
> If you changed `cookieDomain` after a backend login, remove the
> stale `be_typo_user` cookie from your browser or you won't be able
> to log back in.

## Hidden-page preview {#hidden-page-preview}

Preview of hidden pages relies on the same cookie flow as above. As
long as the frontend forwards all cookies to the API request, the
backend user's preview cookie reaches TYPO3 and hidden pages render.

> [!NOTE]
> If you have a truly multi-domain setup (e.g. `api1.example1.com`
> and `api2.example2.com`, no shared root), per-domain cookies are
> not portable. You'll need custom middleware to bridge auth, or
> token-based auth on the frontend side.

## XML sitemap URLs {#xml-sitemap-urls}

The links in the sitemap *index* (the `t3://page?uid=current&type=…`
sitemap-type links) resolve through `frontendApiProxy`; all other page
links use `frontendBase`. To make the index links use `frontendBase`
too, set:

```yaml
# config/sites/<identifier>/settings.yaml
headless:
  sitemap:
    key: frontendBase
```

Index links are matched by the sitemap page type. With a custom sitemap
typeNum, set `headless.sitemap.type` (default `1533906435`) as well, or
the index links keep pointing at the API host:

```yaml
headless:
  sitemap:
    type: '2400000000'
```

## Storage proxy (asset routing) {#storage-proxy-asset-routing}

`headless.storageProxy` plus `frontendFileApi` in the site config
makes processed-file URLs (images, PDFs) point at the frontend's
proxy instead of the TYPO3 fileadmin. Useful when you want the
browser to fetch assets from the same origin as the SPA.

```php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['headless.storageProxy'] = true;
```
