Vícejazyčné weby v Next.js 16: next-intl, hreflang, lokalizace ticketing flow
Jak jsem postavil DJ BOBO Arena Tour weby pro CZ a PL — single codebase, dvě deploymenty, lokalizovaná města a ticketing vendoři. Konfigurace next-intl, hreflang, locale routing, ticketing flow.
Když Sixart Ticket potřeboval marketingové weby pro DJ BOBO Arena Tour 2026 v Česku a Polsku, požadavek zněl jednoduše: jeden codebase, dva deploye, plně lokalizovaný obsah včetně ticketing vendora. Tady je, jak jsem to postavil v Next.js 16 + next-intl za pár dní.
Live: sixartticket.cz a sixartticket.pl. 6 měst, 2 země, jeden codebase.
Návrh: kdy single codebase, kdy ne
První rozhodnutí je drsnější, než vypadá. Máš tři varianty:
- Jeden codebase, jeden deploy —
/csa/plsegmenty (i18n routing) - Jeden codebase, dva deploye — buildneš s ENV
LOCALE=csna.cz,LOCALE=plna.pl - Dva codebases — tradiční fork
Pro tenhle projekt jsem zvolil (2). Důvody:
- Domény jsou marketingové, každá v jiné zemi a má jinou ticketingovou platformu. Logika a vendor differs natolik, že
/csvs/plsegmenty by tahaly mrtvý kód do druhé domény. - SEO chce každá doména jako standalone. Český Google indexuje
.czjinak než.pl. - Vercel deploy preview per locale — můžu testovat
csbuild aplbuild odděleně.
Single codebase = sdílí komponenty, design system, page layout. Dva buildy = každý má jen svoje data.
next-intl konfigurace
Tenhle portfolio běží na variantě (1) — locale routing přes next-intl. Tady je essentials:
// src/lib/i18n.ts
import { hasLocale } from 'next-intl';
import { getRequestConfig } from 'next-intl/server';
export const locales = ['cs', 'en'] as const;
export const defaultLocale = 'cs';
export type Locale = (typeof locales)[number];
export default getRequestConfig(async ({ requestLocale }) => {
const requested = await requestLocale;
const locale = hasLocale(locales, requested) ? requested : defaultLocale;
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});// src/middleware.ts
import createMiddleware from 'next-intl/middleware';
import { defaultLocale, locales } from './lib/i18n';
export default createMiddleware({
locales,
defaultLocale,
localePrefix: 'as-needed', // cs bez prefixu, en jako /en
});
export const config = {
matcher: ['/((?!api|_next|.*\\..*).*)'],
};localePrefix: 'as-needed' znamená:
/→ CS (default, žádný prefix)/en→ EN/work→ CS work/en/work→ EN work
Pro SEO výhoda: hlavní jazyk si nese clean URL bez locale segmentu. Žádné /cs/work, jen /work. Trade-off: musíš si dávat pozor při generování linků (next-intl Link to řeší).
hreflang správně
Tohle je místo, kde 90 % multi-language webů selže. Bez hreflang Google neví, kterou verzi servovat na .cz vs .pl vyhledávači a ranky cestují kam chtějí.
Setup v každé page metadata:
import type { Metadata } from 'next';
import type { Locale } from '@/lib/i18n';
export async function generateMetadata({
params,
}: { params: Promise<{ locale: Locale }> }): Promise<Metadata> {
const { locale } = await params;
const path = '/services';
const csUrl = `https://ondrejknedla.cz${path}`;
const enUrl = `https://ondrejknedla.cz/en${path}`;
return {
alternates: {
canonical: locale === 'cs' ? csUrl : enUrl,
languages: { cs: csUrl, en: enUrl, 'x-default': csUrl },
},
openGraph: {
locale: locale === 'cs' ? 'cs_CZ' : 'en_US',
alternateLocale: locale === 'cs' ? 'en_US' : 'cs_CZ',
},
};
}x-default říká crawlerům, "když nevíš, kterou verzi, použij CS". To je důležité pro non-CZ a non-EN trhy (DE/SK Google a další).
Ticketing flow lokalizace
Tady to bylo zajímavé. CZ verze používá Sixart Ticket API. PL verze polského vendora s úplně jiným flow. Komponenta <TicketCTA> musí být polymorfní:
// components/ticket-cta.tsx
import type { Locale } from '@/lib/i18n';
type TicketingProvider = 'sixart' | 'polish-vendor';
const PROVIDER_BY_LOCALE: Record<Locale, TicketingProvider> = {
cs: 'sixart',
pl: 'polish-vendor',
};
export function TicketCTA({
locale,
eventId,
city,
}: { locale: Locale; eventId: string; city: string }) {
const provider = PROVIDER_BY_LOCALE[locale];
if (provider === 'sixart') {
return <SixartCTA eventId={eventId} city={city} />;
}
return <PolishVendorCTA eventId={eventId} city={city} />;
}Klíčové učení: nikdy si nelep vendor logiku do jedné komponenty s if/else. Polymorfní složka, kde každý provider je samostatný komponent. Když přidám DE verzi, jen rozšířím union a přidám <DeVendorCTA>.
Lokalizovaná města (ne jen překlad)
CS verze pokrývá Ostravu (17.10.) a Bratislavu (24.10.). PL verze Gdańsk, Łódź a Krakov. Každé město má jiné venue, jiný layout sálu (5+ kategorií sedadel včetně VIP "Golden Seat"), jiné ticketing IDs.
Ne stejná data prefixována locale. Úplně jiný dataset:
// data/events.cs.ts
export const events: Event[] = [
{
city: 'Ostrava',
date: '2026-10-17',
venue: 'Ostravar Aréna',
seatTiers: ['VIP Golden Seat', 'Standing Floor', 'Sezení A', ...],
ticketingId: 'sx-ostrava-2026',
},
// ...
];
// data/events.pl.ts — úplně jiné objekty, jiné tier names, jiný vendorKomponenta page jen importuje správný dataset:
import { events } from `data/events.${locale}`;Build-time discriminace přes ENV LOCALE určuje, který dataset se zabuildí. Druhá doména vůbec nemá kód té první.
Reálné metriky
| Metric | CS site | PL site |
|---|---|---|
| Cities | 2 (Ostrava, Bratislava) | 3 (Gdańsk, Łódź, Krakov) |
| Vendor | Sixart Ticket | Polish vendor |
| Build size | ~180 KB First Load JS | ~180 KB |
| Lighthouse SEO | 100 | 100 |
| Time to ship | shared | 4 dny pro oba |
Časté chyby (a jak se vyhnout)
1. Locale switcher, který drží query stringy. Když uživatel je na /work?filter=ai a přepne na EN, chceš /en/work?filter=ai. next-intl usePathname to řeší, ale musíš to explicitně preservovat.
2. Date formatting napříč locale. CS je 17. 10. 2026, PL je 17.10.2026, EN Oct 17, 2026. Nikdy si nepiš vlastní format funkci. Použij Intl.DateTimeFormat(locale).
3. SEO meta jen v jednom jazyce. Open Graph musí mít locale: 'cs_CZ' nebo 'pl_PL', ne jen 'cs'. Plus alternateLocale array pro ostatní lokace.
Co dál
- DJ BOBO Tour case study → — víc o produktu kolem těhle webů
- Claude Code workflow → — jak Claude Code napsal většinu té lokalizační logiky
- B2B lead pipeline → — jiný víkendový projekt
Pokud řešíš multi-locale Next.js projekt (e-commerce, ticketing, marketing site), napiš mi. Většina problémů je předvídatelná, správný setup ti ušetří týdny.