DJ BOBO Arena Tour 2026 - multi-jazyčné event weby
Marketingové weby pro arena turné DJ BOBO v ČR/SR (sixartticket.cz) a Polsku (sixartticket.pl). Lokalizovaný obsah, integrace ticketingu, countdown timery, multiple seating tiery včetně VIP Golden Seat. Identický kód, dvě odrůdy nasazení.
6 měst, 2 země
Brief
DJ BOBO slaví v roce 2026 třicet let kariéry a jede arena turné po střední Evropě - Ostrava, Bratislava, Gdańsk, Łódź, Kraków, plus jedno doplňkové město. Promotér potřeboval dvě paralelní landing pages pro dva trhy: česko-slovenský přes vendora SixArt Ticket a polský přes lokálního partnera. Cílové publikum: eurodance fanoušci 30+, kteří kupují vstupenky přes mobil v průměru o nedělním večeru, ale konvertují i přes desktop ve čtvrtek na pracovišti.
Komerční model je standardní touring revenue: prodej probíhá přes vendora, web je primárně konverzní funnel (impressions → email capture → klik na ticketing CTA → vendor checkout). Měříme CTR z hero sekce na vendor a abandonment rate na seat selection. Všechno ostatní je marketingová dramaturgie.
Architektura
Šel jsem do single repo, dva builds. Monorepo by tu byl overkill - neaktualizujeme produkty, ale dvě varianty stejného webu. Setup je jednodušší než typický Turborepo:
sixartticket/
├── src/
│ ├── app/[locale]/...
│ ├── components/ # sdílené komponenty (hero, lineup, countdown)
│ └── content/
│ ├── tour.cs.ts # CZ/SK města + venues + ticketing config
│ └── tour.pl.ts # PL města + venues + lokální vendor
├── deployments/
│ ├── cz.vercel.json # nasazení na sixartticket.cz
│ └── pl.vercel.json # nasazení na sixartticket.pl
Build orchestruje environment variable NEXT_PUBLIC_TOUR_REGION=cz|pl - ten řídí, který content modul se importuje a jaké locale routy se generují. Identický bundle, dvě konfigurace. Ze 100 % kódu sdílíme 94 %, lokální specifika tvoří jen content moduly a tři komponenty (lokální vendor button, regulatory footer text, distribuční partneři).
Lokalizační strategie
Lokalizace tady neznamená "překlad UI stringů". Znamená to, že každý trh má odlišnou skladbu měst, venues, dat, cen a vendora. Zvolil jsem typed config per locale:
// src/content/tour.cs.ts
import type { TourConfig } from '@/types/tour';
export const tour: TourConfig = {
region: 'cz',
defaultLocale: 'cs',
supportedLocales: ['cs', 'sk'],
cities: [
{
slug: 'ostrava',
name: { cs: 'Ostrava', sk: 'Ostrava' },
venue: 'Ostravar Aréna',
date: '2026-10-17T20:00:00+02:00',
vendor: { kind: 'sixart', eventId: 'BOBO-OSTRAVA-2026' },
seatTiers: ['standing', 'tribune', 'vip', 'golden'],
},
{
slug: 'bratislava',
name: { cs: 'Bratislava', sk: 'Bratislava' },
venue: 'Ondrej Nepela Arena',
date: '2026-10-24T20:00:00+02:00',
vendor: { kind: 'sixart', eventId: 'BOBO-BA-2026' },
seatTiers: ['standing', 'tribune', 'vip', 'golden'],
},
],
newsletter: { provider: 'resend', audienceId: 'aud_bobo_cz' },
};Polský tour.pl.ts má jinou strukturu měst, jiný vendor (lokální), jiný timezone offset a vlastní distribuční partnery. URL generuju z config - /cs/koncert/ostrava, /sk/koncert/bratislava, /pl/koncert/gdansk. App Router generateStaticParams jede přes tour.cities a vyhrne všechny permutace.
Integrace ticketingu
SixArt API je REST, autentizovaný HMAC-podepsaným headerem. Request shape pro availability lookup vypadá takhle:
// src/lib/sixart.ts
export async function getEventAvailability(eventId: string): Promise<Availability> {
const ts = Date.now().toString();
const signature = hmacSha256(`${ts}:${eventId}`, process.env.SIXART_SECRET!);
const res = await fetch(`https://api.sixart.cz/v2/events/${eventId}/availability`, {
headers: {
'X-SixArt-Timestamp': ts,
'X-SixArt-Signature': signature,
},
next: { revalidate: 60 }, // 60s - fresh enough for "lístků zbývá X"
});
if (!res.ok) throw new SixArtError(res.status, await res.text());
const data = (await res.json()) as {
eventId: string;
tiers: Array<{ id: string; name: string; available: number; priceCzk: number }>;
soldOut: boolean;
};
return data;
}Polský vendor naopak používá OAuth2 client credentials flow s access tokeny platnými 1 hodinu, takže jsem napsal tenkou abstrakci TicketProvider interface, kterou implementují obě varianty. CTA komponenta dostane jen <TicketButton city={city} /> a polymorfně vybere správného providera.
Performance
Cílem byl LCP pod 1.8 s na mobilu (Google PageSpeed > 90). Hero pozadí jsem dělal jako <Image priority /> s AVIF + WebP fallback, optimalizace přes Next/Image runs server-side a velikosti shrunkly z původních 2.4 MB na 180 kB AVIF varianty. Countdown timer jsem psal s requestAnimationFrame, ne setInterval - setInterval(..., 1000) na pozadí krade frame budget a Safari ho zpomaluje když je tab idle. RAF + visibility API mi umožnil pozastavit počítání když je tab schovaný:
useEffect(() => {
let raf = 0;
let lastTick = performance.now();
const tick = (now: number) => {
if (now - lastTick >= 1000 && !document.hidden) {
lastTick = now;
setRemaining(target.getTime() - Date.now());
}
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [target]);Reálně naměřené: LCP 1.4 s na 4G mobile, FID < 50 ms, CLS 0.02. Ticketing CTA dynamický fetch trvá p95 240 ms s revalidate: 60 cache hitem, p95 820 ms na cold miss z Edge.
Rozhodnutí, která se vyplatila
| Rozhodnutí | Důvod | Výsledek |
|---|---|---|
| Single repo, dva deploye | Vendor lock-in nezní jako fakta | 94 % code reuse, 2 produkční domény |
Typed TourConfig per locale | Vyhneme se i18n hell | Nový venue = 1 commit, ne 5 |
revalidate: 60 ne no-store | Vendor API je drahé | -85 % SixArt requestů |
| AVIF + WebP fallback | Polská 4G není česká 4G | LCP -1.0 s na slabé síti |
Co překvapilo
- Polský vendor odpovídal jinou strukturou než dokumentace. Pole
availableu nich neexistuje, místo tohoinventory.remaining. Adapter si tohle musí zmapovat. Kdybych šel produkčně na první pokus, sites by lhaly o dostupnosti. - Timezone bugs. CZ/SK je
+02:00v říjnu (DST), Polsko stejně. Ale Bratislava 2025-10-26 02:00 → 03:00 byl DST přechod, který jsem omylem chytl při testovacím datu. RAF countdown se v tom okamžiku "zasekl" o hodinu. Přepnutí na pevný UTC offset to vyřešilo. - Email capture konverze byla 4× vyšší než jsem čekal. ~7 % návštěvníků dalo email pro pre-sale notification. Resend audience šel rovnou do Mailchimp pro promotéra.
Lessons
Vendor APIs jsou v eventovém světě divočina. Pokud máš dva trhy a dva vendory, postav si provider abstrakci hned, ne až když budeš mít 200 řádků duplicitního fetch logiky. Dva deploye z jednoho repa fungují skvěle pro paralelní marketing kampaně, ale potřebuješ disciplínu kolem process.env.NEXT_PUBLIC_TOUR_REGION - jakákoliv komponenta, která to ignoruje, ti zlomí jeden ze dvou builds.
Pro klienta to znamenalo dvě live domény za 11 dní od kickoffu, sdílený codebase a ticketing integrace, kterou můžou recyklovat na další turné v 2027.