Multi-market
Part 1 — For business and content users
A market in Laioutr is a regional slice of your storefront: a named region (e.g. Switzerland, Germany) with its own domains, languages, and currency. You configure markets in Cockpit → Markets.
Each market has one or more domains. A domain maps a host and optional path prefix to a language:
| Domain | Language |
|---|---|
www.shop.ch | German |
www.shop.ch/fr | French |
www.shop.de | German |
One domain per market is the default (the root URL without path prefix). You can also limit pages to specific markets in Studio; other markets won't show that route.
The combination of market and language is the context a customer browses in. When someone visits www.shop.ch/fr, Laioutr resolves the market (Switzerland) and the language (French). Everything downstream (currency, measurement system, content translations, available pages) follows from this context.
Languages are defined once in Cockpit → Translations and assigned to markets via domains. See Multi-language support.
Part 2 — For developers
Configuration
Markets and their domains come from RC (laioutrrc.markets). Each RcMarket has:
id,slug,namecurrency(ISO 4217, e.g.CHF)regionCodes(e.g.["CH"])defaultDomainIdstatus:'active'or'draft'(optional; absent reads as'active')domains: dictionary ofRcMarketDomain, each withid,host, optionalpath, andlanguageId
The project itself carries laioutrrc.defaultMarketId, naming the market that acts as the default.
At build time, Frontend Core transforms this into a RenderI18nConfig (via buildI18nConfig()) with resolved types (RenderMarket, RenderMarketDomain, RenderLanguage) and lookup maps (marketById, marketBySlug, hostToMarket). You access this derived config through composables, never the raw RC.
Market status
A draft market is configured but not launched. It still serves its own host, so you can check it before go-live, but the frontend never links to it and never lets it be indexed.
buildI18nConfig() resolves status into two predicates on RenderMarket. Read these, not status itself: a future status then costs one mapping change instead of a sweep through every consumer.
| Field | active | draft |
|---|---|---|
status | 'active' | 'draft' |
isLinkable | true | false |
isIndexable | true | false |
A third predicate, isDefault, is independent of status — see Default market.
What follows from them:
isLinkable: false: the market is absent fromhreflangalternates,og:locale:alternate, andx-default, andlinkResolver.switchMarketUrl()returns'#market-not-active'for it.isIndexable: false: every page served from that market rendersrobots: noindex, nofollow, overriding both the page's own SEO config and anyfrontend-core:page-head:resolvehandler.
Routes are unaffected: a draft market keeps its aliases and answers 200 on its own host. Delisted, not unreachable.
markets vs allMarkets
RenderI18nConfig carries two arrays, and picking the wrong one is the easiest mistake here:
| Contains | Use for | |
|---|---|---|
markets | linkable markets only | market switchers, alternates, anything the visitor can follow |
allMarkets | every configured market, drafts included | routing, host resolution, preview, tooling |
The lookup maps (marketById, marketBySlug, hostToMarket) always stay complete. That is what keeps a draft market's own host resolving to it.
Default market
RenderI18nConfig.defaultMarket resolves in this order:
- the market named by
laioutrrc.defaultMarketId, if it exists and is active; - otherwise the first active market;
- otherwise the first market, so an all-draft configuration still renders.
It drives x-default, the primary (non-alias) path of every route, the unknown-host fallback, nuxt-i18n's defaultLocale, and the market Studio opens on. Leaving defaultMarketId unset reproduces the old behaviour, which took whichever market happened to come first. Set it explicitly on any project with more than one market.
The resolved market is reachable two ways, and they name the same object:
i18nConfig.defaultMarket === i18nConfig.allMarkets.find((m) => m.isDefault);
Use defaultMarket when you have the config and want the market. Use isDefault when you have a market and want to know whether it is the default one — a switcher marking its primary entry, or a component that only ever receives a single RenderMarket. Exactly one market in allMarkets carries the flag; it is missing from markets only when every configured market is draft, which is also the one case where no x-default is emitted.
Host-to-market constraint
Each host (e.g. www.shop.ch) must belong to exactly one market. Within that market, path prefixes differentiate languages (e.g. / for German, /fr for French). You cannot assign the same host to two different markets with different path prefixes — hostToMarket maps each host to a single market, so only the last-processed market would be reachable.
If you need two regions on the same domain, use a single market with multiple domains (path-prefixed), or use separate hosts per market.
(host, path) pairs within one market, but does not prevent two markets from sharing a host. If this happens, a warning is logged at build time and one market silently shadows the other.Composables
const market = useMarket() // ComputedRef<RenderMarket>
const language = useLanguage() // ComputedRef<RenderLanguage>
const domain = useMarketDomain() // ComputedRef<RenderMarketDomain>
const currency = useCurrency() // ComputedRef<string>, shorthand for market.currency
const config = useI18nConfig() // RenderI18nConfig (static, not reactive)
const marketPath = useMarketPath() // (path: string) => string, prepends domain path prefix
These are always defined. Resolution falls back to the default market when the host is unknown.
Use useI18nConfig() when you need the full list of markets and languages, for example to build a market picker.
Use useMarketPath() when you need to prepend the current domain's path prefix to a URL (e.g. turning /products into /fr/products on a French-prefixed domain).
Currency: use useCurrency() when passing the currency code to APIs. For displaying prices, use the $money formatter directly; it reads the currency from the Money object, not from useCurrency().
Region / measurement: use useLanguage().value.measurementSystem (metric/imperial) or useMarket().value.regionCodes for region-specific behaviour. See the Measurement reference for how to pick the unit you emit based on the active language.
Page-level market scope
RcPage.marketIds (optional): if set, the page only exists in routes for those markets. A "CH only" landing page won't generate routes in the Germany market.
Validation
validateI18nConfig(languages, markets, defaultMarketId) runs at build time and checks: valid BCP 47 codes, existing defaultDomainId and languageId references, no duplicate (host, path) pairs, no two domains within one market serving the same language, a defaultMarketId that names an existing and active market, and at least one active market. Issues are logged as warnings.
The last check keeps (market, language) a unique key for a domain — the pair the server uses to resolve clientEnv.domain. If a market legitimately spans two same-language regions (e.g. Germany and Austria on one EUR market), give each domain a region-qualified locale (de-DE vs de-AT) so they stay distinguishable.
Dev hosts
toDevHost(host) maps production hosts to local development hosts (e.g. www.shop.ch → shop-ch.local.laioutr.tech). A *.local.laioutr.tech wildcard DNS record points to 127.0.0.1, so you can test multi-domain setups locally. The default market is also aliased to localhost as an offline fallback, and the startup banner marks draft markets with [draft].
Fallback behaviour
Market and language are never null at runtime. Resolution always produces a result:
| Scenario | What happens |
|---|---|
| No market matches the request host | Falls back to the default market (see Default market). Warning logged. |
| No path prefix matches within the market | Uses the market's default domain. |
Page not in current market (marketIds) | No route alias exists. Standard 404. |
| Language has no path for a page | No alias generated. linkResolver.switchLocalePath() returns '#'. |
| Content has no value for the locale chain | unlocalize() returns undefined. Components handle missing data. |
| Invalid RC config (dangling refs, bad BCP 47) | Build-time warnings from validateI18nConfig(). |
Multi-language Support
Laioutr's multi-language support lets you run storefronts in multiple languages and regions, with language switchers, localized paths, and BCP 47–based configuration managed in Cockpit.
Page Types
Page types define the kinds of pages customers can add in Studio (Home, Product Detail, Landing Page, etc.) and control routing, data loading, and link resolution.