- Vrealmatic
- Cloudflare
- Web pro více zemí
Web pro více zemí z jednoho kódu
Společný kód, individualizace pro každou zemi a vlastní doména na Cloudflare Workers - případová studie DatováMapa
Jak jsme postavili DatovouMapu pro více zemí: jedno jádro, kontrakt, který každá země naplní, adresy a texty v jazyce země, funkce podle toho, co země umí, a každá země jako samostatný Worker na vlastní doméně. Technicky, s ukázkami kódu z produkce. Jak to funguje, uvidíte rovnou na datovamapa.cz.
Obsah článku
00 · Přehled
Web pro více zemí v kostce
DatováMapa běží ve více zemích z jednoho kódu. Každá země má vlastní doménu, jazyk, adresy stránek, zdroje dat i služby, jádro aplikace ale existuje jen jednou a o žádné zemi neví. Ukazujeme, jak je to postavené: hranici mezi jádrem a zemí, kontrakt, který ji hlídá, lokalizaci a provoz každé země jako samostatného Workeru na Cloudflare.
Architektura ve zkratce
- Jádro - routy, protokol, API, MCP, mapa a cache v
src/, bez jediné zmínky o konkrétní zemi - Kontrakt - tři typy (
CountryConfig, obsah,CountryServices) určují, co země musí a co může dodat - Země - složka
countries/<id>: konfigurace, zdroje dat, obsah, překlady a dokumentace v jazyce země - Výběr před buildem - symlink
src/countryvybere jednu zemi, bundle obsahuje jen ji - Vlastní doména - každá země je samostatný Worker s vlastní doménou, D1 a nasazením
Co v článku najdete
| Téma | Kapitola |
|---|---|
| Co patří do jádra a co do země | Jádro a země → |
| Kontrakt, který hranici typově hlídá | Kontrakt země → |
| Jak se jádro dostane k zemi, aniž by o ní vědělo | Načtení země → |
| Adresy a texty v jazyce každé země | Lokalizace → |
| Funkce, které má jen některá země | Volitelné služby → |
| Jak změna jádra nerozbije jinou zemi | Kontrola všech zemí → |
| Každá země jako samostatný Worker na vlastní doméně | Vlastní domény → |
| Pravidla, která se nám osvědčila | Poučení → |
DatováMapa je otevřený portál, který v jedné mapě spojuje veřejná data z různých zdrojů. Architekturu navrhl, postavil a provozuje Vrealmatic: Next.js 16 přes OpenNext na Cloudflare Workers. Ukázky kódu jsou zkrácené z produkčního repozitáře, ID databází jsou nahrazena zástupnými hodnotami. Stav k říjnu 2026.
01 · Architektura
Co patří do jádra a co do země
Celá architektura stojí na jednom pravidle: jádro nesmí předpokládat konkrétní zemi ani konkrétní data. Všechno, co se mezi zeměmi liší, patří do složky země. Jádro to čte jen přes kontrakt a s chybějícími částmi počítá.
Co patří kam
| Oblast | Jádro (src/) | Země (countries/<id>) |
|---|---|---|
| Routy a stránky | neutrální anglické routy, layout, komponenty | veřejné adresy v jazyce země (/mista × /miesta) |
| Protokol, API, MCP | schéma, validace, endpointy, nástroje: všude stejné a anglické | nic |
| Texty pro lidi | český zdrojový text ve volání t("…") | katalog překladů messages.ts a dokumentace v docs/*.md |
| Identita | jen typ CountryConfig | id, jazyk, kanonická doména, časové pásmo, kraje, výřez mapy |
| Zdroje dat | proxy, adaptéry, cache, katalog | registr poskytovatelů a jejich adaptéry v providers/ |
| Místopis | hledání, stránky obcí a krajů | obce, kraje a hranice (JSON z vlastních skriptů země) |
| Obsah | jak se vykreslí | sestavené mapy, tematické bloky obce, redakční kalendář, sociální sítě |
| Služby | co se stane, když služba chybí | geokodér, hranice obcí, čísla za kraj (každá nepovinná) |
| Provoz | worker.ts, cache, migrace D1 | vlastní wrangler.jsonc: Worker, D1, doména, e-mail |
Proč ne fork pro každou zemi
Jádro + země
- oprava chyby nebo nová funkce v jádru platí pro všechny země
- nová země = nová složka, první verze může být jen kostra
- rozdíly mezi zeměmi jsou na jednom místě a typově hlídané
Fork nebo větvení podle země
- každá oprava se přenáší ručně a forky se rozjíždějí
if (country === "sk")rozeseté po kódu, které nikdo neudrží- nikdo neví, jestli změna nerozbila zemi, kterou zrovna nevyvíjí
Pokud se domény liší jen brandingem a canonical URL (white-label), nepotřebujete vrstvu zemí ani Worker pro každou doménu. Jak v tom případě bezpečně cachovat, popisuje kapitola Jeden Worker, více domén v návodu Workers Cache.
02 · Kontrakt
Kontrakt země: co musí a co může dodat
Kontrakt jsou tři typy v jádru. Říkají, co země musí dodat (konfiguraci), co může dodat prázdné (obsah) a co nemusí mít vůbec (služby). Tvar hlídá TypeScript: země, která něco vynechá nebo pojmenuje jinak, neprojde typovou kontrolou.
.
├── src/ # core: knows a country only through the contract
│ ├── app/ # routes with neutral English names
│ ├── lib/country-config.ts # contract: identity, language, domain, public paths
│ ├── lib/country-content.ts # contract: curated maps, place themes, calendar
│ ├── lib/country-services.ts # contract: optional services
│ └── country -> ../countries/cz # symlink to the selected country
├── countries/
│ ├── index.ts # registry of all countries (links between them)
│ ├── cz/
│ │ ├── config.ts # CountryConfig
│ │ ├── services.ts # CountryServices
│ │ ├── messages.ts # UI translations (empty for Czech)
│ │ ├── providers/ # the country's data sources
│ │ ├── content/ # curated maps, gazetteer, country outline
│ │ ├── docs/ # public documentation in the country's language
│ │ └── scripts/ # generators of the country's data
│ └── sk/ # the same shape + its own wrangler.jsonc
├── worker.ts # one entry point for every country
├── wrangler.jsonc # worker of the default country (cz)
└── scripts/ # use-country, check-countries, cf (deploy)CountryConfig: identita a adresy
Konfigurace je jediný povinný popis země: kdo je, jakým jazykem mluví, na jaké doméně bydlí a jak se v jejím jazyce jmenují stránky. Klíče stránek, souborů a dokumentů jsou neutrální (places, docs) a hodnoty jsou v jazyce země.
// src/lib/country-config.ts (shortened): everything the core may know about a country
export type PageKey = "places" | "maps" | "topics" | "sources" | "docs" | "register" | "unreviewed" | "datasets";
export type FileKey = "places.geojson" | "municipalities.json" | "sources.md" | "examples";
export type DocKey = "providers" | "ai-agents" | "ai-assistants" | "protocol" | "api" | "about";
export type CountryConfig = {
id: string; // "cz", the same as the folder name
locale: string; // "cs-CZ": language of texts for people
origin: string; // canonical domain: "https://datovamapa.cz"
bounds: [number, number, number, number]; // where the map opens
timeZone: string; // what "today" means
regions: { name: string; slug: string }[]; // top-level administrative units
basemap?: { tileJsonUrl?: string; tileUrl?: string; attribution?: string };
paths: {
pages: Record<PageKey, string>; // public URL segments in the country's language
files: Record<FileKey, string>;
docs: Record<DocKey, string>;
categories: Record<string, string>;
};
};// countries/sk/config.ts (shortened)
import type { CountryConfig } from "../../src/lib/country-config";
export const country: CountryConfig = {
id: "sk",
locale: "sk-SK",
origin: "https://datovamapa.sk",
bounds: [16.83, 47.73, 22.57, 49.62],
timeZone: "Europe/Bratislava",
regions: [
{ name: "Bratislavský kraj", slug: "bratislavsky-kraj" },
// ... 7 more
],
// until Slovakia has its own tile server: OpenFreeMap (OpenMapTiles schema)
basemap: { tileJsonUrl: "https://tiles.openfreemap.org/planet" /* , attribution: ... */ },
paths: {
pages: {
places: "miesta", maps: "mapy", topics: "temy", sources: "zdroje",
docs: "dokumentacia", register: "registracia", unreviewed: "neschvalene-data", datasets: "mapa",
},
// files, docs and categories the same way
},
};Co musí složka země obsahovat
| Soubor | Obsah | Může být prázdný? |
|---|---|---|
config.ts | identita, jazyk, doména, výřez mapy, kraje, veřejné cesty | ne |
providers/registry.ts | připojení poskytovatelé dat a jejich adaptéry | ano |
content/maps.ts, place-themes.ts… | sestavené mapy, bloky přehledu obce, redakční kalendář, sociální sítě | ano |
content/docs.ts + docs/*.md | veřejná dokumentace v jazyce země | ne |
messages.ts | překlady textů z jádra | u výchozího jazyka ano |
content/places.json… | místopis: obce, kraje, hranice (generují skripty země) | ne |
content/outline.json | obrys státu pro mapu a prokliky mezi zeměmi | ne |
services.ts | volitelné služby | ano |
wrangler.jsonc | Worker země (jen mimo výchozí zemi) | - |
Každé nové pole v kontraktu musí doplnit všechny země. Proto je výhodné nové věci přidávat jako nepovinné (služba, prázdný obsah) a povinnými je dělat jen tehdy, když bez nich jádro opravdu nemůže fungovat.
03 · Načtení
Jak se jádro dostane k zemi
Jádro importuje zemi vždy přes jedinou cestu @/country/…. Za ní je symbolický odkaz src/country, který skript před buildem nasměruje na countries/cz, nebo countries/sk. Jádro tak nikdy neřeší, o kterou zemi jde.
// the core reaches the selected country through one path only
import { country } from "@/country/config";
import { services } from "@/country/services";
import { registry } from "@/country/providers/registry";// scripts/use-country.ts (shortened): src/country -> countries/<COUNTRY>
import { existsSync, lstatSync, readlinkSync, symlinkSync, unlinkSync } from "node:fs";
import { join } from "node:path";
const root = join(import.meta.dirname, "..");
const id = process.env.COUNTRY ?? "cz";
if (!/^[a-z]{2,8}$/.test(id) || !existsSync(join(root, "countries", id, "config.ts"))) {
console.error(`Country "${id}" does not exist: countries/${id}/config.ts is missing.`);
process.exit(1);
}
const link = join(root, "src", "country");
const target = join("..", "countries", id);
const stat = lstatSync(link, { throwIfNoEntry: false });
if (stat && !stat.isSymbolicLink()) {
console.error("src/country must be a symlink, not a folder.");
process.exit(1);
}
if (!stat || readlinkSync(link, { encoding: "utf8" }) !== target) {
if (stat) unlinkSync(link);
symlinkSync(target, link, "dir");
}Přepnutí zařídí npm skripty samy: dev i build nejdřív přepnou symlink podle COUNTRY a vygenerují katalog překladů pro prohlížeč. Ručně stačí COUNTRY=sk npm run country.
// package.json: dev and build switch the country by themselves
"predev": "tsx scripts/use-country.ts && tsx scripts/i18n.ts --write",
"prebuild": "tsx scripts/use-country.ts && tsx scripts/i18n.ts --write",
"country": "tsx scripts/use-country.ts"Proč symlink a ne proměnná za běhu
Symlink před buildem
- bundle obsahuje jen data jedné země, Worker neroste s počtem zemí
- statické importy a plné typy, žádné větvení podle země
- typová kontrola běží proti skutečné zemi, ne proti sjednocení všech
process.env.COUNTRY za běhu
- bundler zabalí data všech zemí najednou
- dynamický import s proměnnou cestou (
import(`./countries/${id}`)) bundler často nerozpozná - chyba v datech jedné země může rozbít build všech
Registr všech zemí
Jednu věc potřebuje každá země vědět o ostatních: že existují. Mapa DatovéMapy zakryje okolní státy a podporované země nabídne jako proklik na jejich doménu. K tomu slouží malý registr nezávislý na symlinku. Obsahuje jen konfiguraci a obrys, žádná data.
// countries/index.ts: every country, independent of the symlink
import type { CountryConfig } from "../src/lib/country-config";
import { country as cz } from "./cz/config";
import czOutline from "./cz/content/outline.json";
import { country as sk } from "./sk/config";
import skOutline from "./sk/content/outline.json";
export type CountryEntry = { config: CountryConfig; outline: [number, number][][][] };
export const countries: CountryEntry[] = [
{ config: cz, outline: czOutline.shape as [number, number][][][] },
{ config: sk, outline: skOutline.shape as [number, number][][][] },
];I jedna země může přerůst limit velikosti Workeru. Velké soubory (u DatovéMapy například zdravotnictví) se importují líně přes await import() a malá metadata k nim leží zvlášť v *.meta.json.
04 · Lokalizace
Adresy a texty v jazyce země
Každá země má adresy i texty ve svém jazyce: datovamapa.cz/mista, datovamapa.sk/miesta. Uvnitř aplikace se ale nic nepřekládá ručně. Routy jsou neutrální, odkazy se skládají přes funkce a texty přes katalog země.
Adresy stránek v jazyce země
- Routy v
src/appmají neutrální anglické názvy (places,docs). Veřejné adresy na ně vedou přesrewritesvygenerované zconfig.paths. - Každý odkaz vzniká přes
routeHref, nikdy ručně. Stejný kód vytvoří/dokumentace/apiv Česku a/dokumentacia/apina Slovensku. - Interní adresy se nevydávají.
worker.tspožadavek na interní routu přesměruje na veřejnou, aby stránka neexistovala pod dvěma URL.
// src/lib/paths.ts (shortened): neutral routes inside, the country's language outside
import { country } from "../country/config";
const { pages: PAGE_PATHS } = country.paths;
// routeHref("docs", "api") -> "/dokumentace/api" in cz, "/dokumentacia/api" in sk
export const routeHref = (key: PageKey, ...rest: string[]) => `/${[PAGE_PATHS[key], ...rest].join("/")}`;
// for next.config.ts: public segment -> internal route
export function pathRewrites() {
return Object.entries(PAGE_PATHS).flatMap(([internal, publicName]) => [
{ source: `/${publicName}`, destination: `/${internal}` },
{ source: `/${publicName}/:path*`, destination: `/${internal}/:path*` },
]);
}Texty: zdrojový řetězec je klíč
Jádro píše texty česky přímo v kódu, t("…") je zároveň klíč. Česko má katalog prázdný, Slovensko v countries/sk/messages.ts překládá zdroj na slovenštinu. Chybějící překlad vrátí zdrojový text, takže nová funkce v jádru nikdy nerozbije jinou zemi. Ve vývoji na chybějící překlad upozorní konzole.
Prohlížeč nedostane celý katalog. Skript vybere jen překlady textů z klientských komponent (messages.client.ts, generuje se před buildem a není v gitu) a Next.js ho prohlížeči podstrčí přes alias. Server používá celý katalog.
// in the core: the Czech source text is the key
const text = t("Chyba: {message}", { message });
// countries/sk/messages.ts: Czech source -> Slovak translation (the Czech catalog is empty)
export const messages: Record<string, string> = {
"DatováMapa": "DátováMapa",
"## Dokumentace": "## Dokumentácia",
"[lon, lat] ve WGS84, např. [14.4378, 50.0755]": "[lon, lat] vo WGS84, napr. [17.1077, 48.1486]",
};// next.config.ts: the browser gets only the translations of client code, the server the whole catalog
turbopack: {
resolveAlias: {
"@/country/messages": { browser: "./src/country/messages.client.ts" },
},
},
async rewrites() {
return pathRewrites();
},COUNTRY=sk npm run i18n # sources, missing translations, unused keys
COUNTRY=sk npm run i18n -- --missing # missing sources as JSON, input for translation
COUNTRY=sk npm run i18n -- --check # fails when anything is missing or left overCo se nepřekládá
Protokol, API, MCP nástroje, chybové kódy a JSON Schema jsou ve všech zemích stejné a anglické. Klient nebo AI agent, který umí jednu zemi, tak umí všechny. Zemi patří jen to, co čtou lidé: texty stránek, dokumentace, názvy míst a adresy.
05 · Služby
Volitelné služby a zdroje dat
Země nemají stejné možnosti. Česko má veřejný geokodér adres (RÚIAN), Slovensko zatím ne. Kontrakt to řeší nepovinnými službami: co země nemá, to jádro vypne, a nespadne.
// src/lib/country-services.ts (shortened): every service is optional
export type CountryServices = {
/** Address, street or municipality search. */
geocoder?: { attribution: string; search(text: string): Promise<GeocoderHit[]> };
/** Municipality boundary by code. */
boundaries?: { download(code: string): Promise<Ring[][] | undefined> };
/** Figures for a region. */
regionStatistics?: { providerId: string; load(regionName: string): Promise<RegionFigures[]> };
};// countries/sk/services.ts: no address search yet, so the core switches it off
export const services: CountryServices = {
boundaries: { download: async (code) => shapes.get(code) },
regionStatistics: { providerId: REGION_STATISTICS_PROVIDER, load: regionStatistics },
};// src/app/[region]/page.tsx: no statistics service, no figures on the region page
const statistics = (await services.regionStatistics?.load(region.name)) ?? [];
// src/lib/server/geocode.ts: no geocoder, a clear error instead of a crash
const geocoder = services.geocoder;
if (!geocoder) throw new RequestError(t("Hledání adres a míst v této zemi zatím není dostupné."), 501, "geocoding_unavailable");- Bez geokodéru vrátí hledání míst chybu
geocoding_unavailablea MCP ani OpenAPI nástrojfind_placevůbec nenabízí. - Bez čísel za kraj má stránka kraje jen seznam obcí a dat, bez karet s čísly.
- Bez hranic obcí portál nezná hranici menších obcí a jejich data ukáže jako nenačtená, aby neuváděl počty za špatné území.
Zdroje dat patří zemi
Každá země má vlastní registr poskytovatelů (providers/registry.ts): Česko ČSÚ, ČHMÚ nebo data měst, Slovensko ŠÚ SR, ÚGKK, Bratislavu a Košice. Adaptéry na rozhraní mimo protokol (ArcGIS REST, vlastní JSON) jsou v jádru a země je jen používá. Proto může nová země začít s prázdným registrem a zdroje přidávat postupně.
- Nová schopnost = nepovinná služba v kontraktu. Země ji dodá, až ji má.
- Jádro na chybějící službu reaguje vypnutím funkce, ne chybou.
- Zemi nikdy neblokuje to, co jiná země umí navíc.
06 · Kontrola
Kontrola všech zemí jedním příkazem
Největší riziko sdíleného jádra: změníte ho při práci na jedné zemi a rozbijete druhou, kterou zrovna nevyvíjíte. Proto jeden příkaz projde všechny země a pro každou spustí stejné kontroly.
// scripts/check-countries.ts (shortened): every country, one after another
const countries = readdirSync("countries").filter((id) => existsSync(`countries/${id}/config.ts`));
const previous = basename(readlinkSync("src/country", { encoding: "utf8" }));
const run = (id: string, args: string[]) =>
spawnSync("npx", args, { stdio: "inherit", env: { ...process.env, COUNTRY: id } }).status === 0;
const failed: string[] = [];
try {
for (const id of countries) {
run(id, ["tsx", "scripts/use-country.ts"]);
if (!run(id, ["tsc", "--noEmit"])) failed.push(`${id}: types`);
if (!run(id, ["tsx", "scripts/check-docs.ts"])) failed.push(`${id}: docs`);
if (!run(id, ["tsx", "scripts/i18n.ts", "--check"])) failed.push(`${id}: translations`);
}
} finally {
run(previous, ["tsx", "scripts/use-country.ts"]); // back to the country selected before
}
if (failed.length) process.exit(1);| Kontrola | Co odhalí |
|---|---|
tsc --noEmit | zemi, které chybí nové pole kontraktu, nebo jádro, které předpokládá data jedné země |
check-docs.ts | nefunkční ukázky a odkazy v dokumentaci země, kolizi adresy kraje s routou |
i18n.ts --check | chybějící nebo nepoužité překlady |
| registr a obrys | zemi, která chybí v countries/index.ts nebo nemá outline.json |
- Po každé změně jádra spusťte
npm run check:countries, i když nasazujete jen jednu zemi. - Změna protokolu = změna schématu a dokumentace protokolu všech zemí, kontrola to ověří.
- Skript na konci vrátí symlink na zemi, která byla vybraná předtím, takže vás nepřepne jinam.
07 · Domény
Každá země na vlastní doméně
Individualizace končí u infrastruktury: každá země je samostatný Cloudflare Worker na vlastní doméně, s vlastní databází D1, rate limitem, odesílatelem e-mailů a secrets. Výpadek, chybná data nebo nasazení jedné země se druhé nedotkne.
wrangler.jsonc ve složce země
Výchozí země (Česko) používá kořenový wrangler.jsonc, ostatní země vlastní soubor ve své složce. Cesty v něm jsou relativní k souboru (../../worker.ts), takže všechny země sdílí stejný vstupní bod a výstup buildu.
// countries/sk/wrangler.jsonc: every path is relative to THIS file
{
"$schema": "../../node_modules/wrangler/config-schema.json",
"name": "datovamapa-sk",
"main": "../../worker.ts",
"workers_dev": false,
"preview_urls": false,
"compatibility_date": "2026-08-12",
"compatibility_flags": ["global_fetch_strictly_public"],
"assets": { "binding": "ASSETS", "directory": "../../.open-next/assets" },
"images": { "binding": "IMAGES" },
// must match "name": a copied "datovamapa" would call the Czech worker
"services": [{ "binding": "WORKER_SELF_REFERENCE", "service": "datovamapa-sk" }],
"d1_databases": [
{ "binding": "DB", "database_name": "datovamapa_sk", "database_id": "<D1_ID_SK>", "migrations_dir": "../../migrations" }
],
// namespace_id must be unique in the account (cz uses 1001)
"ratelimits": [{ "name": "REGISTRATION_LIMIT", "namespace_id": "2001", "simple": { "limit": 5, "period": 60 } }],
"send_email": [{ "name": "EMAIL" }],
"version_metadata": { "binding": "CF_VERSION_METADATA" },
"vars": { "NOTIFY_FROM": "registracia@mail.datovamapa.sk" },
"observability": { "enabled": true },
"upload_source_maps": true,
"limits": { "cpu_ms": 750 }
}| Klíč | Česko | Slovensko | Proč zvlášť |
|---|---|---|---|
name | datovamapa | datovamapa-sk | samostatný Worker, verze a logy |
d1_databases | datovamapa-d1 | datovamapa_sk | registrace a schválení patří jedné zemi |
namespace_id | 1001 | 2001 | se stejným ID by země sdílely počítadlo |
NOTIFY_FROM | mail.datovamapa.cz | mail.datovamapa.sk | odesílatel na doméně země |
WORKER_SELF_REFERENCE | datovamapa | datovamapa-sk | OpenNext volá sám sebe |
Nasazení jedné země
Všechny příkazy (build, nasazení, migrace D1, schvalování) jdou přes skript, který podle COUNTRY přepne symlink a Wrangleru doplní --config. Člověk ho nikdy nepíše ručně, takže slovenský build nemůže skončit v českém Workeru.
// scripts/lib/wrangler.ts: the country's worker and D1 for every wrangler call
import { existsSync } from "node:fs";
import { join } from "node:path";
export const root = join(import.meta.dirname, "..", "..");
export const countryId = process.env.COUNTRY ?? "cz";
const config = join("countries", countryId, "wrangler.jsonc");
/** "--config ..." for a country with its own config, otherwise nothing (cz uses the root file). */
export const wranglerConfigArgs = existsSync(join(root, config)) ? ["--config", config] : [];npm run deploy # Czechia: root wrangler.jsonc
COUNTRY=sk npm run deploy # Slovakia: countries/sk/wrangler.jsoncDoména země a odkazy mezi zeměmi
config.originje kanonická doména země. Canonical, Open Graph, sitemap i odkazy v dokumentaci míří na ni a každá země je pro vyhledávače samostatný web ve svém jazyce.- Edge cache ve
worker.ts(Cache API) má v klíči celou URL včetně domény a ID nasazené verze. Nasazení jedné země vyprázdní jen její cache. Cache API nefunguje na*.workers.dev, proto mají Workeryworkers_dev: falsea běží jen na vlastní doméně. - Prokliky mezi zeměmi skládá mapa z registru
countries/index.ts: název sousední země v jazyce té aktuální (Intl.DisplayNames) a odkaz na jejíorigin.
08 · Poučení
Co se nám osvědčilo
Architektura jádro + země stojí na tom, jestli hranice mezi nimi vydrží. Tahle pravidla se nám při stavbě a provozu osvědčila.
- Země nesmí prosáknout do jádra. Jakmile se v
src/objevícountry.id === "cz"nebo natvrdo zapsaná česká adresa, hranice padla. Rozdíl vždy patří do konfigurace, obsahu nebo služby země. - Odkazy jen přes funkce.
href="/mista"by fungoval jen v Česku, proto se všechny odkazy skládají přesrouteHrefadocHref. - Přeložit jde jen literál. Řetězec v
t()musí být literál, jinak ho skript nenajde a překlad by chyběl jen v jedné zemi. - Kontrakt roste nepovinně. Povinné pole by zablokovalo všechny země, dokud ho nedoplní. Nové schopnosti proto začínají jako nepovinná služba nebo prázdný obsah.
- Kontroluje se vždy všechno.
tscproti symlinku ověří jen vybranou zemi, proto každá změna jádra projdecheck:countries. - Konfigurace Workeru se nekopíruje naslepo. Stejné
namespace_idby sdílelo počítadlo, zkopírovaný service binding by volal cizí Worker. Proto--configdoplňuje skript, nikdy člověk. - Build a nasazení jsou jeden krok.
.open-next/obsahuje jen naposledy sestavenou zemi, takže se nasazuje výhradně přes skript, který zemi přepne a sestaví.
Díky tomu je nová země hlavně práce s daty: nová složka, konfigurace, místopis a překlady. Slovensko se spustilo jako kostra s místopisem z Eurostatu a OpenStreetMap, bez poskytovatelů dat a služeb, a obsah přibývá postupně jako nasazení jen slovenského Workeru.
- Jádro o žádné zemi neví. Zná jen kontrakt: konfiguraci, obsah a nepovinné služby.
- Země je složka, ne fork. Do jádra se dostane přes symlink před buildem a bundle obsahuje jen ji.
- Adresy a texty v jazyce země vznikají z konfigurace a katalogu, protokol a API zůstávají všude stejné.
- Co země nemá, jádro vypne. Proto může nová země začít jako kostra.
- Každá země běží na vlastní doméně jako samostatný Worker a změna jádra se vždy ověří na všech.
Zdroje
Oficiální dokumentace a další čtení
Článek vychází z produkční architektury DatovéMapy a z dokumentace použitých nástrojů (anglicky):
- Next.js rewritesVeřejné adresy v jazyce země na neutrální routy.
- Turbopack resolveAliasJiný modul pro prohlížeč a pro server.
- Intl.DisplayNamesNázvy zemí v jazyce aktuální země.
- Konfigurace Wrangleruwrangler.jsonc, relativní cesty a bindingy.
- Custom domainsVlastní doména pro každý Worker.
- Rate LimitingBinding ratelimits a unikátní namespace_id.
- OpenNext pro CloudflareBuild Next.js pro Workers.
Související
- Cloudflare Workers CacheEdge cache před Workerem, včetně jednoho Workeru pro více domén.
- Next.js na CloudflareStatický build, Worker OpenNext, D1, R2 a incremental cache (anglicky).
- Cloudflare wikiAPI tokeny, deploy přes token, R2 z příkazové řádky.
- DatováMapaPortál z případové studie, živá ukázka architektury.
Jak citovat tuto publikaci (ČSN ISO 690:2022)NEPOR, Vladimír. Web pro více zemí z jednoho kódu: Next.js na Cloudflare. Online. Vrealmatic, 2026. Dostupné z: https://vrealmatic.com/cloudflare/web-pro-vice-zemi. [citováno ].
Spouštíte aplikaci ve více zemích?
Navrhneme architekturu se společným jádrem a nastavíme provoz každé země na vlastní doméně na Cloudflare.