URL structure
Every route is prefixed with a country and locale segment:[country] and [locale] segments are the first two dynamic params of the App Router tree (src/app/[country]/[locale]/…). Because region lives in the path, every URL is shareable and independently cacheable, and search engines index each region separately.
The middleware
A visitor who lands on a bare path (no region prefix) is redirected to the correct/{country}/{locale}/… URL by an edge middleware. It’s wired in src/proxy.ts:
createSpreeMiddleware (from src/lib/spree) does three things:
- Redirects bare paths to
/{country}/{locale}/…. - Detects the visitor’s country and locale (see below).
- Syncs
spree_countryandspree_localecookies with the URL segments, so server-side fetches viagetLocaleOptions()resolve the same market the URL asks for.
/{country}/{locale} prefix pass through untouched — the middleware only refreshes the cookies to match.
Detection chain
For a request without a region prefix, each value is resolved from the first source that has it:
Geo headers are set by the hosting edge —
x-vercel-ip-country on Vercel, cf-ipcountry behind Cloudflare. On a host that provides neither, detection falls back to Accept-Language for locale and the configured default for country. A returning visitor’s cookie always wins, so a manual region change sticks.
Configuration
createSpreeMiddleware accepts:
In the storefront these defaults come from
NEXT_PUBLIC_DEFAULT_COUNTRY and NEXT_PUBLIC_DEFAULT_LOCALE (see Environment Variables). The matcher in proxy.ts additionally excludes API routes, Next.js internals, and any path with a file extension, so the middleware only runs on page navigations.
Switching regions
TheCountrySwitcher component (in src/components/layout/) lets a visitor change region manually. Selecting a new region navigates to the matching URL prefix; the middleware then updates the spree_country / spree_locale cookies so the choice persists across future visits.
How region reaches the data layer
Region-aware reads don’t take a country/locale argument — they callgetLocaleOptions() from src/lib/spree, which reads the spree_country / spree_locale cookies the middleware keeps in sync with the URL. That value is passed to @spree/sdk, which sends it to the Store API so prices, currency, and translated content come back for the right market. See Architecture for the wider server-first data flow.
