URL a SEO architektura univerzálního e-commerce jádra (Herbatica první konfigurace)
Standalone zadání. Tento issue obsahuje vše potřebné k implementaci bez další konverzace. Agent ani člověk nepotřebuje žádný externí kontext.
- Tento popis = krátká implementační specifikace. Stačí k naimplementování celého rozsahu.
- Komentáře 1–5 = plná normativní specifikace (~32 tis. slov) s důkazy, citacemi primárních zdrojů a evidenční přílohou. Číst při sporu nebo když krátká verze nestačí.
Datum rozhodnutí: 2026-08-05. Cílový release: 2026-08-20. Status: binding.
0. Kontext pro agenta (přečti první)
| Položka |
Hodnota |
| Repo |
TechsioCZ/new-engine, Nx + pnpm monorepo, Node 24, pnpm 11.15.1 |
| Dotčená aplikace |
apps/herbatika (Next.js 16.3.0-preview.5, App Router) |
| Backend |
apps/medusa-be (MedusaJS) |
| Sdílené knihovny |
libs/storefront-data, libs/storefront-i18n, libs/ui, libs/std |
| Nx wrapper |
./node_modules/.bin/nubx --node nx ... |
| Lint/format |
pnpm exec oxfmt --check ., pnpm exec oxlint . --type-aware, pnpm exec ultracite check <cesty> |
| Typecheck |
pnpm typecheck:tsc, pnpm typecheck:tsgo |
| Pravidla kódu |
CLAUDE.md v kořeni + nejbližší AGENTS.md/CLAUDE.md v podstromu |
Kritická hranice odpovědností:
- MedusaJS neřeší URL ani routing. Poskytuje katalogová a obchodní data.
- Next.js vlastní routing, generování a parsování URL, canonicalizaci, metadata, sitemap, robots a všechny redirecty vzniklé na novém webu.
- Nikdy nepřidávej do Medusy paralelní URL nebo taxonomický systém.
Business kontext: greenfield URL. Klient si sám řeší migraci ze starého Shoptetu a hosting historických redirectů. Staré URL nezachováváme, redirect mapa je mimo rozsah.
Trhy — pevné mapování, jedna doména = jeden trh = jeden sales channel:
| Doména |
Market |
Locale |
herbatica.sk |
sk |
sk-SK |
herbatica.cz |
cz |
cs-CZ |
herbatica.hu |
hu |
hu-HU |
herbatica.ro |
ro |
ro-RO |
Žádný locale prefix typu /cs/ v cestě. Produkty, kategorie, zařazení, názvy, slugy i dostupnost se mezi trhy liší.
1. Vítězná architektura — rozhodnutí
- Typový prefix ANO, lokalizovaný, v plurálu. Ne root slugy.
- Produkt bez kategorie v URL:
/produkty/{slug}. Produkt ve více kategoriích má jednu URL — žádné duplicity, žádné redirect chains.
- Kategorie plochá:
/kategorie/{slug}, ne vnořené cesty. Přesun nebo přejmenování kategorie v Medusa stromu URL nemění. Hierarchie žije jen v breadcrumbech, navigaci a structured data.
- Varianta nemá vlastní URL, jen
?varianta={sku}, canonical na produkt.
- Kolize slugů napříč typy jsou nemožné, protože každý typ má vlastní prefix. Uvnitř typu je hlídá unique index v URL registru.
- Trh se určuje výhradně z hostu. Neznámý host →
421. Žádný Accept-Language fallback, žádný default na SK.
- Slug je po publikaci stabilní. Změna názvu entity URL nemění. Ruční změna slugu vytvoří immutable alias.
Zamítnuto a proč:
| Varianta |
Důvod zamítnutí |
Root slugy bez prefixu (/{slug}) |
nevyhnutelné kolize mezi produkty, kategoriemi, značkami, články a systémovými routami; křehký resolver |
| Kategorie v produktové URL |
produkt ve více kategoriích = duplicitní URL nebo nestabilní canonical |
Vnořené kategoriální cesty (/kategorie/a/b/c) |
každý přesun v taxonomii vynutí hromadný redirect a rozbije zavedené URL |
Locale prefix /cs/ na doméně jednoho trhu |
redundantní, doména už trh jednoznačně určuje |
| Slug generovaný za běhu z názvu |
URL se mění při každé editaci obsahu, ztráta SEO stability |
Auto-suffixy -2 při kolizi |
tichá degradace URL kvality, nekontrolovatelné duplikáty |
| Neanglické URL logiky v Medusa metadatech |
paralelní systém, který nikdo neudrží |
2. Kompletní URL matice
Zápis {products} = klíč z registru lokalizovaných segmentů (sekce 3).
| Typ |
Tvar |
SK |
CZ |
HU |
RO |
Index |
Canonical |
Sitemap |
| Home |
/ |
/ |
/ |
/ |
/ |
✅ |
self |
✅ |
| Produkt |
/{products}/{slug} |
/produkty/zeleny-caj |
/produkty/zeleny-caj |
/termekek/zold-tea |
/produse/ceai-verde |
✅ |
self |
✅ |
| Varianta |
?varianta={sku} |
/produkty/zeleny-caj?varianta=ZC-100G |
totéž |
totéž |
totéž |
❌ |
produkt bez query |
❌ |
| Kategorie |
/{categories}/{slug} |
/kategorie/bylinne-caje |
/kategorie/bylinne-caje |
/kategoriak/gyogynovenyteak |
/categorii/ceaiuri-din-plante |
✅ |
self bez filtrů |
✅ |
| Rozcestník kategorií |
/{categories} |
/kategorie |
/kategorie |
/kategoriak |
/categorii |
✅ |
self |
✅ |
| Značka |
/{brands}/{slug} |
/znacky/pukka |
/znacky/pukka |
/markak/pukka |
/marci/pukka |
✅ |
self |
✅ |
| Rozcestník značek |
/{brands} |
/znacky |
/znacky |
/markak |
/marci |
✅ |
self |
✅ |
| Kolekce |
/{collections}/{slug} |
/kolekcie/darceky |
/kolekce/darky |
/gyujtemenyek/ajandekok |
/colectii/cadouri |
✅ |
self |
✅ |
| Kampaň |
/{campaigns}/{slug} |
/akcie/leto |
/akce/leto |
/akciok/nyar |
/promotii/vara |
✅ |
self |
✅ |
| SEO landing page |
/{campaigns}/{slug} |
stejný namespace jako kampaň |
|
|
|
✅ |
self |
✅ |
| Blog / poradna |
/{advice}/{slug} |
/poradna/{slug} |
/poradna/{slug} |
/tanacsok/{slug} |
/sfaturi/{slug} |
✅ |
self |
✅ |
| Rozcestník poradny |
/{advice} |
/poradna |
/poradna |
/tanacsok |
/sfaturi |
✅ |
self |
✅ |
| Statická stránka |
/{information}/{slug} |
/informacie/doprava |
/informace/doprava |
/informaciok/szallitas |
/informatii/livrare |
✅ |
self |
✅ |
| Právní stránka |
/{information}/{terms} |
/informacie/obchodne-podmienky |
/informace/obchodni-podminky |
/informaciok/altalanos-szerzodesi-feltetelek |
/informatii/termeni-si-conditii |
✅ |
self |
✅ |
| Hledání |
/{search}?q= |
/vyhladavanie |
/vyhledavani |
/kereses |
/cautare |
❌ noindex |
– |
❌ |
| Košík |
/{cart} |
/kosik |
/kosik |
/kosar |
/cos |
❌ |
– |
❌ |
| Checkout krok |
/{checkout}/{krok} |
/pokladna/platba |
/pokladna/platba |
/penztar/fizetes |
/finalizare-comanda/plata |
❌ |
– |
❌ |
| Návrat z platby |
/{checkout}/{paymentReturn} |
/pokladna/navrat-z-platby |
/pokladna/navrat-z-platby |
/penztar/fizetesi-visszateres |
/finalizare-comanda/retur-plata |
❌ |
– |
❌ |
| Potvrzení objednávky |
/{checkout}/{confirmation} |
/pokladna/potvrdenie-objednavky |
/pokladna/potvrzeni-objednavky |
/penztar/rendeles-visszaigazolas |
/finalizare-comanda/confirmare-comanda |
❌ |
– |
❌ |
| Účet |
/{account}/... |
/ucet/objednavky |
/ucet/objednavky |
/fiok/rendelesek |
/cont/comenzi |
❌ |
– |
❌ |
| Detail objednávky |
/{account}/{orders}/{id} |
/ucet/objednavky/{id} |
/ucet/objednavky/{id} |
/fiok/rendelesek/{id} |
/cont/comenzi/{id} |
❌ |
– |
❌ |
| Přihlášení |
/{account}/{login} |
/ucet/prihlasenie |
/ucet/prihlaseni |
/fiok/bejelentkezes |
/cont/autentificare |
❌ |
– |
❌ |
| Recenze přes token |
/{reviews}/{produkt}/{token} |
/recenzie/produkt/{token} |
/recenze/produkt/{token} |
/velemenyek/termek/{token} |
/recenzii/produs/{token} |
❌ |
– |
❌ |
| API |
/api/... |
nelokalizované |
|
|
|
❌ |
– |
❌ |
Chování při chybějící nebo změněné entitě:
| Stav |
Odpověď |
| Entita neexistuje |
404 |
| Publikovaná entita smazána (tombstone) |
410 |
| Starý alias slugu |
308 na aktuální canonical |
| Entita mimo sales channel daného trhu |
404 |
| Backend nedostupný |
503 — nikdy 404 |
| Neznámý query parametr |
400 |
| Neznámý host |
421 |
3. Registr lokalizovaných segmentů
Jediný zdroj pravdy, jeden TS soubor. URL hodnoty jsou ASCII transliterace nativních názvů.
| Klíč |
SK |
CZ |
HU |
RO |
products |
produkty |
produkty |
termekek |
produse |
categories |
kategorie |
kategorie |
kategoriak |
categorii |
brands |
znacky |
znacky |
markak |
marci |
collections |
kolekcie |
kolekce |
gyujtemenyek |
colectii |
campaigns |
akcie |
akce |
akciok |
promotii |
advice |
poradna |
poradna |
tanacsok |
sfaturi |
information |
informacie |
informace |
informaciok |
informatii |
search |
vyhladavanie |
vyhledavani |
kereses |
cautare |
cart |
kosik |
kosik |
kosar |
cos |
checkout |
pokladna |
pokladna |
penztar |
finalizare-comanda |
account |
ucet |
ucet |
fiok |
cont |
reviews |
recenzie |
recenze |
velemenyek |
recenzii |
checkout.contact |
kontakt |
kontakt |
kapcsolat |
contact |
checkout.shipping |
doprava |
doprava |
szallitas |
livrare |
checkout.payment |
platba |
platba |
fizetes |
plata |
checkout.review |
kontrola |
kontrola |
ellenorzes |
verificare |
checkout.paymentReturn |
navrat-z-platby |
navrat-z-platby |
fizetesi-visszateres |
retur-plata |
checkout.confirmation |
potvrdenie-objednavky |
potvrzeni-objednavky |
rendeles-visszaigazolas |
confirmare-comanda |
account.orders |
objednavky |
objednavky |
rendelesek |
comenzi |
account.lists |
zoznamy |
seznamy |
listak |
liste |
account.settings |
nastavenia |
nastaveni |
beallitasok |
setari |
account.login |
prihlasenie |
prihlaseni |
bejelentkezes |
autentificare |
account.register |
registracia |
registrace |
regisztracio |
inregistrare |
account.forgotPassword |
zabudnute-heslo |
zapomenute-heslo |
elfelejtett-jelszo |
parola-uitata |
account.resetPassword |
obnova-hesla |
obnova-hesla |
jelszo-visszaallitas |
resetare-parola |
reviews.product |
produkt |
produkt |
termek |
produs |
about |
o-nas |
o-nas |
rolunk |
despre-noi |
contact |
kontakt |
kontakt |
kapcsolat |
contact |
faq |
casto-kladene-otazky |
caste-dotazy |
gyakori-kerdesek |
intrebari-frecvente |
shipping |
doprava |
doprava |
szallitas |
livrare |
returns |
vratenie-tovaru |
vraceni-zbozi |
visszakuldes |
retururi |
terms |
obchodne-podmienky |
obchodni-podminky |
altalanos-szerzodesi-feltetelek |
termeni-si-conditii |
privacy |
ochrana-osobnych-udajov |
ochrana-osobnich-udaju |
adatvedelmi-tajekoztato |
politica-de-confidentialitate |
cookies |
cookies |
cookies |
cookie-tajekoztato |
politica-cookies |
⚠️ Hodnoty jsou návrh z výzkumu, ne překlad ověřený rodilým mluvčím. Před produkčním spuštěním musí projít nativním editorial sign-offem, právní stránky navíc právním. Schválení mění jen tuto tabulku, ne kód. Viditelný label v UI používá plnou diakritiku (Termékek, Gyűjtemények, Coș, Vyhľadávanie); ASCII je jen v URL.
4. Slug pravidla
Normalizační pipeline:
nativní název → NFC → lowercase → ASCII transliterace → nealfanumerické na "-"
→ sloučit opakované "-" → trim "-" → validace
- Transliterace:
š→s, č→c, ř→r, ž→z, ý→y, ě→e, ů→u, ő→o, ű→u, á→a, é→e, í→i, ó→o, ú→u, ș→s, ț→t, ă→a, â→a, î→i, ľ→l, ĺ→l, ŕ→r, ô→o, ä→a.
- Rumunština: normalizuj legacy cedilla
ş/ţ (U+015F/U+0163) na comma-below ș/ț (U+0219/U+021B) před transliterací.
- Povolené znaky výsledku: pouze
[a-z0-9-]. Max 80 znaků.
- Prázdný výsledek transliterace = chyba publikace. Nikdy nefallbackuj na ID nebo náhodný řetězec.
- Kolize uvnitř typu a trhu = chyba publikace, řeší člověk. Žádné auto-suffixy
-2.
- Rezervovaná slova (nesmí být slug):
api, _next, sitemap, sitemap-index, robots, favicon, assets, static, health, __nextjs.
- Slug se po publikaci negeneruje znovu. Editace názvu entity URL nemění.
- Ruční změna slugu = jedna transakce: zapiš nový
current, starý překlop na alias s aliasOf na nový current záznam.
- Aliasy se nikdy neřetězí. Při změně slugu přepiš
aliasOf u všech existujících aliasů dané entity na nový current. Alias platí jen pro GET/HEAD.
5. URL registr — vlastní Next.js, ne Medusa
Vlastní Postgres schema, vlastní migrace generované vlastníkem frameworku. Nikdy needituj existující migrace, přidávej nové.
export type Market = 'sk' | 'cz' | 'hu' | 'ro'
export type UrlKind =
| 'product' | 'category' | 'brand' | 'collection'
| 'campaign' | 'article' | 'page'
export type UrlStatus = 'current' | 'alias' | 'tombstone'
export type UrlRecord = {
id: string
market: Market
kind: UrlKind
slug: string
entityId: string // ID v Meduse nebo CMS
equivalenceKey: string // stejný napříč trhy pro tutéž entitu → hreflang
indexable: boolean
status: UrlStatus
aliasOf: string | null // vždy na current záznam, nikdy na alias
updatedAt: Date
}
Indexy:
UNIQUE (market, kind, slug) — vynucuje absenci kolizí
INDEX (market, kind, entityId) — reverzní lookup pro URL builder
INDEX (equivalence_key) — hreflang alternates
Seed: jednorázový import z Medusa handle a CMS slugů. Po seedu je registr jediným zdrojem pravdy pro URL; Medusa handle se už pro routing nečte.
6. Datový kontrakt s Medusou
Bereme jen tato pole. Nic víc, žádné URL, žádnou taxonomii navíc.
| Pole |
Vlastník |
Zdroj |
Existuje dnes? |
Fallback |
product.id |
Medusa |
core |
✅ |
– |
product.handle |
Medusa |
core |
✅ |
jen pro seed registru |
product.title (lokalizovaný) |
Medusa Translation |
translation module |
částečně |
chybějící překlad blokuje publikaci entity na daném trhu |
product.categories[] |
Medusa |
core |
✅ |
– |
product.metadata.primary_category_id |
Medusa |
custom merchandising extension |
❌ nutno doplnit |
nejhlubší leaf kategorie → nižší rank → nejnižší ID |
product.variants[].sku |
Medusa |
core |
✅ |
– |
category.id, handle, name |
Medusa |
core |
✅ |
– |
category.parent_category_id, rank |
Medusa |
core |
✅ |
– |
| sales-channel dostupnost |
Medusa |
core |
✅ na backendu, ❌ ve storefront kontextu |
– |
| ceny, sklad |
Medusa |
core (region + country) |
✅ |
– |
| editorial obsah |
CMS |
market + locale scoped |
❌ dnes natvrdo sk |
žádný — chybějící obsah = 404 |
Jediná potřebná custom položka je metadata.primary_category_id na produktu — a i bez ní systém funguje díky deterministickému fallbacku. Primary category ovlivňuje jen breadcrumb, structured data a merchandising, nikdy URL.
7. Next.js routing
Ověřený stav repozitáře: Next.js 16.3.0-preview.5, App Router, cacheComponents: true (apps/herbatika/next.config.ts:65), typedRoutes: true (:118), output: "standalone" (:83).
Klíčový princip: filesystem router nezná lokalizované segmenty. Řeší je proxy rewrite, takže nevznikají čtyři router forky.
apps/herbatika/src/
proxy.ts # NOVÝ (dnes chybí)
app/
(sf)/[market]/page.tsx # home
(sf)/[market]/[kind]/page.tsx # rozcestníky typů
(sf)/[market]/[kind]/[slug]/page.tsx # detaily entit
(sf)/[market]/flow/[...seg]/page.tsx # cart, checkout, account, auth, reviews
sitemap.ts # NOVÝ (dnes chybí)
robots.ts # NOVÝ (dnes chybí)
api/... # beze změny
Ruší se: app/p, app/c, app/znacka, app/blog, app/o-nas, app/faq, app/reset-password a kořenový app/[slug] (kolidoval by s každou novou statickou routou).
proxy.ts dělá pouze statickou práci — žádné DB ani Medusa dotazy:
- host → market podle allowlistu; neznámý host →
421
- první segment cesty →
kind podle statické segment mapy
- rewrite na
/{market}/{kind}/{slug}
- scrub klientských interních hlaviček, vložení trusted market kontextu
Route resolution v page (max 2 dotazy do registru):
- validace syntaxe cesty a query proti per-kind allowlistu → jinak
400
- jeden indexovaný lookup do URL registru
- klasifikace
current → render, alias → 308, tombstone → 410, missing → 404
- volitelně jeden bounded equivalence lookup pro hreflang
Nutná změna configu: skipTrailingSlashRedirect: true, aby se host, protokol, case, trailing slash, alias i query normalizace složily do jediného 308. Dnes chybí.
Jediné URL API — zákaz ad hoc skládání stringů:
buildUrl({ market, kind, slug }): string // '/produkty/zeleny-caj'
buildAbsoluteUrl({ market, kind, slug }): string // 'https://herbatica.sk/produkty/...'
buildCanonical(request): string // bez query kromě povolených
buildAlternates(equivalenceKey): Array<{ hrefLang, href }>
Dnes se URL skládají ručně na mnoha místech (/p/${product.handle} v product cards, objednávkách, košíku, autocomplete). Všechny musí zmizet, jinak se canonical a sitemap rozejdou se skutečnými URL.
Deployment: jedna multi-host standalone instance za reverse proxy. Multi-instance škálování je blokované, dokud není prokázaná cross-instance cache tag propagace — default cache handler je process-local. Kód ale musí podporovat i čtyři samostatná nasazení téhož buildu s runtime ALLOWED_MARKETS subsetem.
Market a canonical origin musí pocházet ze serverového runtime configu, ne z NEXT_PUBLIC_* — ty jsou build-time inline a jeden build by pak neuměl obsloužit čtyři trhy.
8. SEO pravidla
| Situace |
Chování |
| Canonical |
vždy https, non-www, lowercase, bez koncového /, absolutní URL |
| Filtr na PLP |
indexovatelné jen ?znacka= a ?kategorie=; ostatní noindex, follow |
| Kombinace 2+ filtrů |
noindex, canonical na čistou kategorii |
Řazení ?razeni= |
noindex, canonical na čistou kategorii |
Stránkování ?strana=2 |
indexovatelné, canonical self, žádné rel=prev/next |
Tracking parametry (utm_*, gclid, fbclid) |
ignorovat v canonical, žádný redirect |
| Neznámý query parametr |
400 |
| Interní odkazy |
jen na canonical tvary, nikdy na alias nebo filtrovanou variantu |
hreflang |
self + skutečné ekvivalenty přes equivalenceKey; chybějící trh se vynechá |
x-default |
nepoužívat — všechny čtyři trhy jsou rovnocenné, žádná globální varianta |
| Produkt jen na některých trzích |
hreflang jen pro trhy, kde je skutečně dostupný |
| Přepínač země |
odkazuje na ekvivalent, jinak na home daného trhu — nikdy na 404 |
| Sitemap |
sitemap index + per-typ soubory, jen indexovatelné URL, lastModified z registru |
robots.txt |
per-host, Disallow: /{cart}, /{checkout}, /{account}, /{search}, odkaz na sitemap index |
301 vs 308 |
vždy 308 (zachovává metodu i tělo) |
404 vs 410 |
404 = neexistuje, 410 = publikovaná entita smazána |
| Soft-404 |
zakázán — chybějící entita musí vrátit skutečný 404, ne stránku s chybovou hláškou a statusem 200 |
| Backend nedostupný |
503, nikdy 404 |
9. Implementační backlog
Must-have pro release
| # |
Úkol |
Závisí na |
Acceptance criteria |
| M01 |
Segment mapa 4 trhů v jednom TS souboru + collision validator v CI |
– |
CI selže při duplicitním segmentu nebo kolizi s rezervovaným slovem |
| M02 |
URL registr: schema, migrace, indexy |
– |
UNIQUE (market, kind, slug) vynucen v DB, ne jen v kódu |
| M03 |
Seed registru z Medusa handle a CMS slugů |
M02 |
všechny publikované entity mají current záznam ve všech trzích, kde jsou dostupné |
| M04 |
Admin CRUD pro ruční změnu slugu s atomickým zápisem aliasu |
M02 |
změna slugu vytvoří alias; opakovaná změna nevytvoří řetěz, přepíše aliasOf |
| M05 |
Slugifikační modul se 4jazyčnou transliterací |
M01 |
prázdný výsledek i kolize vyhodí typovanou chybu, ne fallback |
| M06 |
src/proxy.ts — host→market, segment→kind, rewrite, 421 |
M01 |
neznámý host vrátí 421 na drátě; žádná SK response pro cizí host |
| M07 |
Přestavba app/ na (sf)/[market]/[kind]/..., zrušení /p, /c, /znacka, /blog, root /[slug] |
M06 |
žádná dvojznačná dynamická routa; collision validator zelený |
| M08 |
URL API + odstranění všech ručně skládaných odkazů |
M02, M07 |
grep nenajde žádné /p/${, /c/${ ani obdobné skládání mimo URL API |
| M09 |
Status vrstva: 308, 404, 410, 503, 400 + notFound() na produktu |
M07 |
wire testy potvrdí skutečné HTTP statusy před flushem response |
| M10 |
generateMetadata — canonical + alternates |
M08 |
každá indexovatelná stránka má absolutní self canonical a reciproční hreflang |
| M11 |
app/sitemap.ts + sitemap index |
M02, M08 |
sitemap obsahuje jen indexovatelné URL, validní XML, per-host |
| M12 |
app/robots.ts per-host |
M01 |
každá doména vrací vlastní robots s vlastním sitemap odkazem |
| M13 |
skipTrailingSlashRedirect: true v next.config.ts |
M09 |
normalizace host + case + trailing slash + alias proběhne jedním 308 |
| M14 |
Odstranit natvrdo zadané CMS_LOCALE = "sk" (apps/herbatika/src/lib/storefront/cms-client.ts:9) |
– |
každý trh dostane obsah ve své locale; chybějící obsah = 404, ne slovenský fallback |
| M15 |
Přidat salesChannelId do market kontextu (market-context.ts) |
– |
katalogové dotazy filtrují podle sales channelu; entita mimo channel vrací 404 |
| M16 |
Zrušit Accept-Language fallback a default sk v resolveru trhu |
M06 |
neznámý host nikdy nevrátí obsah, jen 421 |
| M17 |
metadata.primary_category_id extension v Meduse + fallback logika |
– |
breadcrumb je deterministický i bez vyplněné hodnoty |
| M18 |
Nativní editorial + právní sign-off segment mapy |
M01 |
každá hodnota má schvalovatele a datum; právní stránky schváleny právně |
Should-have
- Kampaně a SEO landing pages jako plnohodnotný typ s vlastním editorem.
- Alias historie viditelná v adminu.
- Redirect a canonical audit jako CI job nad build outputem.
- Structured data (Product, BreadcrumbList, ItemList) navázané na URL API.
Later
- Multi-instance horizontal scaling — blokované process-local cache handlerem.
x-default, pokud vznikne skutečná globální varianta webu.
10. Testovací plán
Unit:
- slugifikace všech 4 jazyků včetně rumunské cedilla normalizace a maďarských dlouhých samohlásek
- prázdný a kolizní výsledek vyhodí chybu
- alias nikdy neřetězí (
aliasOf vždy míří na current)
- segment mapa bez kolizí a bez rezervovaných slov
- canonical builder: protokol, host, case, trailing slash, povolené query
Integration:
| Případ |
Očekávaný status |
| každý typ × 4 trhy, existující entita |
200 |
| alias slugu |
308 na canonical |
| tombstone |
410 |
| neexistující slug |
404 |
| produkt mimo sales channel trhu |
404 |
| neznámý host |
421 |
| neznámý query parametr |
400 |
| Medusa nedostupná |
503 |
POST na alias |
žádný redirect |
Browser / E2E:
- přepínač země vede na ekvivalent, jinak na home — nikdy na
404
- canonical a hreflang přítomné v HTML, hreflang reciproční mezi doménami
- stránkování
?strana=2 je indexovatelné, filtry noindex
- sitemap index i dílčí sitemapy jsou validní XML a obsahují jen indexovatelné URL
- trailing slash, velká písmena a
www se normalizují jedním hopem
- viditelné labely mají plnou diakritiku, URL jsou ASCII
11. Stav repozitáře — co je dnes rozbité
Ověřeno v pracovním stromu 2026-08-05.
| Zjištění |
Soubor |
| Žádný middleware ani proxy |
apps/herbatika/src/proxy.ts neexistuje |
| Žádná sitemap ani robots |
app/sitemap.ts, app/robots.ts neexistují |
| Žádné canonical ani hreflang nikde v aplikaci |
0 výskytů alternates |
Produkt nevolá notFound() → soft-404 se statusem 200 |
app/p/[handle]/page.tsx |
| CMS natvrdo slovensky pro všechny čtyři trhy |
src/lib/storefront/cms-client.ts:9 |
| Sales channel chybí v market kontextu, jede se jen na region + country |
src/lib/storefront/market-context.ts |
Kořenový /[slug] koliduje s libovolnou novou statickou routou |
app/[slug]/page.tsx |
Chybí skipTrailingSlashRedirect |
next.config.ts |
Trh fallbackuje přes Accept-Language na SK |
libs/storefront-i18n/src/core/markets.ts |
| Aktuální routy k odstranění |
app/p, app/c, app/znacka, app/blog, app/o-nas, app/faq, app/reset-password |
12. Definition of done
13. Metodologická poznámka
Specifikace vznikla vícefázovým multi-agentním výzkumem: průzkum repozitáře, primární webový research (Google Search Central, Next.js dokumentace a source instalované verze, RFC/WHATWG), tři nezávislé návrhy architektury, adversarial verifikace tvrzení a route kolizí, syntéza a completeness kritika. Plná evidenční příloha s citacemi, datem přístupu a mírou jistoty je v komentářích níže.
Kde primární zdroj nedával jednoznačnou odpověď, je rozhodnutí označeno jako architecture choice [AC] a nevydává se za doložené SEO pravidlo. Klíčová slova v URL nejsou v této specifikaci považována za významný ranking faktor; lokalizace slugů je UX a IA rozhodnutí.
Recenzní vlastnictví: @RedEyeCZ — proxy, deployment, Docker, CI, migrace. @KaiUweCZE — Next.js routing, frontend chování, libs/ui, přístupnost.
URL a SEO architektura univerzálního e-commerce jádra (Herbatica první konfigurace)
0. Kontext pro agenta (přečti první)
TechsioCZ/new-engine, Nx + pnpm monorepo, Node 24, pnpm 11.15.1apps/herbatika(Next.js16.3.0-preview.5, App Router)apps/medusa-be(MedusaJS)libs/storefront-data,libs/storefront-i18n,libs/ui,libs/std./node_modules/.bin/nubx --node nx ...pnpm exec oxfmt --check .,pnpm exec oxlint . --type-aware,pnpm exec ultracite check <cesty>pnpm typecheck:tsc,pnpm typecheck:tsgoCLAUDE.mdv kořeni + nejbližšíAGENTS.md/CLAUDE.mdv podstromuKritická hranice odpovědností:
Business kontext: greenfield URL. Klient si sám řeší migraci ze starého Shoptetu a hosting historických redirectů. Staré URL nezachováváme, redirect mapa je mimo rozsah.
Trhy — pevné mapování, jedna doména = jeden trh = jeden sales channel:
herbatica.sksksk-SKherbatica.czczcs-CZherbatica.huhuhu-HUherbatica.rororo-ROŽádný locale prefix typu
/cs/v cestě. Produkty, kategorie, zařazení, názvy, slugy i dostupnost se mezi trhy liší.1. Vítězná architektura — rozhodnutí
/produkty/{slug}. Produkt ve více kategoriích má jednu URL — žádné duplicity, žádné redirect chains./kategorie/{slug}, ne vnořené cesty. Přesun nebo přejmenování kategorie v Medusa stromu URL nemění. Hierarchie žije jen v breadcrumbech, navigaci a structured data.?varianta={sku}, canonical na produkt.421. ŽádnýAccept-Languagefallback, žádný default na SK.Zamítnuto a proč:
/{slug})/kategorie/a/b/c)/cs/na doméně jednoho trhu-2při kolizi2. Kompletní URL matice
Zápis
{products}= klíč z registru lokalizovaných segmentů (sekce 3).//////{products}/{slug}/produkty/zeleny-caj/produkty/zeleny-caj/termekek/zold-tea/produse/ceai-verde?varianta={sku}/produkty/zeleny-caj?varianta=ZC-100G/{categories}/{slug}/kategorie/bylinne-caje/kategorie/bylinne-caje/kategoriak/gyogynovenyteak/categorii/ceaiuri-din-plante/{categories}/kategorie/kategorie/kategoriak/categorii/{brands}/{slug}/znacky/pukka/znacky/pukka/markak/pukka/marci/pukka/{brands}/znacky/znacky/markak/marci/{collections}/{slug}/kolekcie/darceky/kolekce/darky/gyujtemenyek/ajandekok/colectii/cadouri/{campaigns}/{slug}/akcie/leto/akce/leto/akciok/nyar/promotii/vara/{campaigns}/{slug}/{advice}/{slug}/poradna/{slug}/poradna/{slug}/tanacsok/{slug}/sfaturi/{slug}/{advice}/poradna/poradna/tanacsok/sfaturi/{information}/{slug}/informacie/doprava/informace/doprava/informaciok/szallitas/informatii/livrare/{information}/{terms}/informacie/obchodne-podmienky/informace/obchodni-podminky/informaciok/altalanos-szerzodesi-feltetelek/informatii/termeni-si-conditii/{search}?q=/vyhladavanie/vyhledavani/kereses/cautare/{cart}/kosik/kosik/kosar/cos/{checkout}/{krok}/pokladna/platba/pokladna/platba/penztar/fizetes/finalizare-comanda/plata/{checkout}/{paymentReturn}/pokladna/navrat-z-platby/pokladna/navrat-z-platby/penztar/fizetesi-visszateres/finalizare-comanda/retur-plata/{checkout}/{confirmation}/pokladna/potvrdenie-objednavky/pokladna/potvrzeni-objednavky/penztar/rendeles-visszaigazolas/finalizare-comanda/confirmare-comanda/{account}/.../ucet/objednavky/ucet/objednavky/fiok/rendelesek/cont/comenzi/{account}/{orders}/{id}/ucet/objednavky/{id}/ucet/objednavky/{id}/fiok/rendelesek/{id}/cont/comenzi/{id}/{account}/{login}/ucet/prihlasenie/ucet/prihlaseni/fiok/bejelentkezes/cont/autentificare/{reviews}/{produkt}/{token}/recenzie/produkt/{token}/recenze/produkt/{token}/velemenyek/termek/{token}/recenzii/produs/{token}/api/...Chování při chybějící nebo změněné entitě:
404410308na aktuální canonical404503— nikdy4044004213. Registr lokalizovaných segmentů
Jediný zdroj pravdy, jeden TS soubor. URL hodnoty jsou ASCII transliterace nativních názvů.
productsproduktyproduktytermekekprodusecategorieskategoriekategoriekategoriakcategoriibrandsznackyznackymarkakmarcicollectionskolekciekolekcegyujtemenyekcolectiicampaignsakcieakceakciokpromotiiadviceporadnaporadnatanacsoksfaturiinformationinformacieinformaceinformaciokinformatiisearchvyhladavanievyhledavanikeresescautarecartkosikkosikkosarcoscheckoutpokladnapokladnapenztarfinalizare-comandaaccountucetucetfiokcontreviewsrecenzierecenzevelemenyekrecenziicheckout.contactkontaktkontaktkapcsolatcontactcheckout.shippingdopravadopravaszallitaslivrarecheckout.paymentplatbaplatbafizetesplatacheckout.reviewkontrolakontrolaellenorzesverificarecheckout.paymentReturnnavrat-z-platbynavrat-z-platbyfizetesi-visszateresretur-platacheckout.confirmationpotvrdenie-objednavkypotvrzeni-objednavkyrendeles-visszaigazolasconfirmare-comandaaccount.ordersobjednavkyobjednavkyrendelesekcomenziaccount.listszoznamyseznamylistaklisteaccount.settingsnastavenianastavenibeallitasoksetariaccount.loginprihlasenieprihlasenibejelentkezesautentificareaccount.registerregistraciaregistraceregisztracioinregistrareaccount.forgotPasswordzabudnute-heslozapomenute-hesloelfelejtett-jelszoparola-uitataaccount.resetPasswordobnova-heslaobnova-heslajelszo-visszaallitasresetare-parolareviews.productproduktprodukttermekprodusabouto-naso-nasrolunkdespre-noicontactkontaktkontaktkapcsolatcontactfaqcasto-kladene-otazkycaste-dotazygyakori-kerdesekintrebari-frecventeshippingdopravadopravaszallitaslivrarereturnsvratenie-tovaruvraceni-zbozivisszakuldesretururitermsobchodne-podmienkyobchodni-podminkyaltalanos-szerzodesi-feltetelektermeni-si-conditiiprivacyochrana-osobnych-udajovochrana-osobnich-udajuadatvedelmi-tajekoztatopolitica-de-confidentialitatecookiescookiescookiescookie-tajekoztatopolitica-cookies4. Slug pravidla
Normalizační pipeline:
š→s,č→c,ř→r,ž→z,ý→y,ě→e,ů→u,ő→o,ű→u,á→a,é→e,í→i,ó→o,ú→u,ș→s,ț→t,ă→a,â→a,î→i,ľ→l,ĺ→l,ŕ→r,ô→o,ä→a.ş/ţ(U+015F/U+0163) na comma-belowș/ț(U+0219/U+021B) před transliterací.[a-z0-9-]. Max 80 znaků.-2.api,_next,sitemap,sitemap-index,robots,favicon,assets,static,health,__nextjs.current, starý překlop naaliassaliasOfna nový current záznam.aliasOfu všech existujících aliasů dané entity na nový current. Alias platí jen proGET/HEAD.5. URL registr — vlastní Next.js, ne Medusa
Vlastní Postgres schema, vlastní migrace generované vlastníkem frameworku. Nikdy needituj existující migrace, přidávej nové.
Indexy:
UNIQUE (market, kind, slug)— vynucuje absenci kolizíINDEX (market, kind, entityId)— reverzní lookup pro URL builderINDEX (equivalence_key)— hreflang alternatesSeed: jednorázový import z Medusa
handlea CMS slugů. Po seedu je registr jediným zdrojem pravdy pro URL; Medusahandlese už pro routing nečte.6. Datový kontrakt s Medusou
Bereme jen tato pole. Nic víc, žádné URL, žádnou taxonomii navíc.
product.idproduct.handleproduct.title(lokalizovaný)product.categories[]product.metadata.primary_category_idrank→ nejnižší IDproduct.variants[].skucategory.id,handle,namecategory.parent_category_id,ranksk404Jediná potřebná custom položka je
metadata.primary_category_idna produktu — a i bez ní systém funguje díky deterministickému fallbacku. Primary category ovlivňuje jen breadcrumb, structured data a merchandising, nikdy URL.7. Next.js routing
Ověřený stav repozitáře: Next.js
16.3.0-preview.5, App Router,cacheComponents: true(apps/herbatika/next.config.ts:65),typedRoutes: true(:118),output: "standalone"(:83).Klíčový princip: filesystem router nezná lokalizované segmenty. Řeší je proxy rewrite, takže nevznikají čtyři router forky.
Ruší se:
app/p,app/c,app/znacka,app/blog,app/o-nas,app/faq,app/reset-passworda kořenovýapp/[slug](kolidoval by s každou novou statickou routou).proxy.tsdělá pouze statickou práci — žádné DB ani Medusa dotazy:421kindpodle statické segment mapy/{market}/{kind}/{slug}Route resolution v page (max 2 dotazy do registru):
400current→ render,alias→308,tombstone→410,missing→404Nutná změna configu:
skipTrailingSlashRedirect: true, aby se host, protokol, case, trailing slash, alias i query normalizace složily do jediného308. Dnes chybí.Jediné URL API — zákaz ad hoc skládání stringů:
Dnes se URL skládají ručně na mnoha místech (
/p/${product.handle}v product cards, objednávkách, košíku, autocomplete). Všechny musí zmizet, jinak se canonical a sitemap rozejdou se skutečnými URL.Deployment: jedna multi-host standalone instance za reverse proxy. Multi-instance škálování je blokované, dokud není prokázaná cross-instance cache tag propagace — default cache handler je process-local. Kód ale musí podporovat i čtyři samostatná nasazení téhož buildu s runtime
ALLOWED_MARKETSsubsetem.Market a canonical origin musí pocházet ze serverového runtime configu, ne z
NEXT_PUBLIC_*— ty jsou build-time inline a jeden build by pak neuměl obsloužit čtyři trhy.8. SEO pravidla
https, non-www, lowercase, bez koncového/, absolutní URL?znacka=a?kategorie=; ostatnínoindex, follownoindex, canonical na čistou kategorii?razeni=noindex, canonical na čistou kategorii?strana=2rel=prev/nextutm_*,gclid,fbclid)400hreflangequivalenceKey; chybějící trh se vynecháx-default404lastModifiedz registrurobots.txtDisallow: /{cart}, /{checkout}, /{account}, /{search}, odkaz na sitemap index301vs308308(zachovává metodu i tělo)404vs410404= neexistuje,410= publikovaná entita smazána404, ne stránku s chybovou hláškou a statusem200503, nikdy4049. Implementační backlog
Must-have pro release
UNIQUE (market, kind, slug)vynucen v DB, ne jen v kóduhandlea CMS slugůcurrentzáznam ve všech trzích, kde jsou dostupnéaliasOfsrc/proxy.ts— host→market, segment→kind, rewrite,421421na drátě; žádná SK response pro cizí hostapp/na(sf)/[market]/[kind]/..., zrušení/p,/c,/znacka,/blog, root/[slug]/p/${,/c/${ani obdobné skládání mimo URL API308,404,410,503,400+notFound()na produktugenerateMetadata— canonical + alternatesapp/sitemap.ts+ sitemap indexapp/robots.tsper-hostskipTrailingSlashRedirect: truevnext.config.ts308CMS_LOCALE = "sk"(apps/herbatika/src/lib/storefront/cms-client.ts:9)404, ne slovenský fallbacksalesChannelIddo market kontextu (market-context.ts)404Accept-Languagefallback a defaultskv resolveru trhu421metadata.primary_category_idextension v Meduse + fallback logikaShould-have
Later
x-default, pokud vznikne skutečná globální varianta webu.10. Testovací plán
Unit:
aliasOfvždy míří nacurrent)Integration:
200308na canonical410404404421400503POSTna aliasBrowser / E2E:
404?strana=2je indexovatelné, filtrynoindexwwwse normalizují jedním hopem11. Stav repozitáře — co je dnes rozbité
Ověřeno v pracovním stromu 2026-08-05.
apps/herbatika/src/proxy.tsneexistujeapp/sitemap.ts,app/robots.tsneexistujíalternatesnotFound()→ soft-404 se statusem200app/p/[handle]/page.tsxsrc/lib/storefront/cms-client.ts:9src/lib/storefront/market-context.ts/[slug]koliduje s libovolnou novou statickou routouapp/[slug]/page.tsxskipTrailingSlashRedirectnext.config.tsAccept-Languagena SKlibs/storefront-i18n/src/core/markets.tsapp/p,app/c,app/znacka,app/blog,app/o-nas,app/faq,app/reset-password12. Definition of done
pnpm exec oxfmt --check .apnpm exec oxlint . --type-awarezelenépnpm typecheck:tscapnpm typecheck:tsgozelenépnpm exec ultracite check <změněné cesty>zelené13. Metodologická poznámka
Specifikace vznikla vícefázovým multi-agentním výzkumem: průzkum repozitáře, primární webový research (Google Search Central, Next.js dokumentace a source instalované verze, RFC/WHATWG), tři nezávislé návrhy architektury, adversarial verifikace tvrzení a route kolizí, syntéza a completeness kritika. Plná evidenční příloha s citacemi, datem přístupu a mírou jistoty je v komentářích níže.
Kde primární zdroj nedával jednoznačnou odpověď, je rozhodnutí označeno jako architecture choice
[AC]a nevydává se za doložené SEO pravidlo. Klíčová slova v URL nejsou v této specifikaci považována za významný ranking faktor; lokalizace slugů je UX a IA rozhodnutí.Recenzní vlastnictví:
@RedEyeCZ— proxy, deployment, Docker, CI, migrace.@KaiUweCZE— Next.js routing, frontend chování,libs/ui, přístupnost.