Platforma dla wielu marek
Regulowana platforma gier hazardowych online dla wielu marek w kilku jurysdykcjach europejskich - jedna baza kodu Next.js z App Router, statyczne strony z dynamicznymi wyspami i warstwa treści zarządzana przez headless CMS.
Statyczna strona bazowa + dynamiczne wyspy
page.tsx · export const dynamic = "force-static"
Szczegóły projektu
#Kontekst
Poprowadziłem samodzielnie kilka architektonicznych migracji frontendu w regulowanej platformie gier hazardowych online dla wielu marek - od podniesienia Next.js na App Router po fundament SEO pod crawlery AI. Szczegóły każdej z tych zmian w sekcji „Co dostarczyłem” niżej.
Wiele marek reużywało ten sam kod, każda na własnych domenach i w wielu wersjach językowych - wszystko w kilku europejskich systemach regulacyjnych o wyraźnie różnych zasadach. Większość mojej pracy jest objęta NDA (umową o zachowaniu poufności), więc to case study opisuje samą inżynierię - architekturę, wzorce i kompromisy - bez nazw marek, klientów końcowych i konkretnych dostawców.
Całość żyła w monorepo opartym na Next.js App Router: jedna bazowa aplikacja marki, kolejne marki re-eksportujące wspólny kod przez aliasy ścieżek, plus współdzielone packages i libs. Najtrudniejsza nie była żadna pojedyncza funkcja - chodziło o to, żeby wiele marek, domen i jurysdykcji trzymać w spójności, nie zatrzymując przy tym wydawania zmian.
Stack integrował typowe dla branży komponenty: SDK dostawcy platformy z gotowymi integracjami wymaganymi przez regulatora (limity, samowykluczenie, weryfikacja tożsamości), bramki płatności dla wielu metod per jurysdykcja, oraz CRM z warstwą motywacyjną (kampanie, bonusy, program lojalnościowy).
#Co dostarczyłem
- Aktualizacja Next.js do wersji z App Router: podniosłem wszystkie aplikacje na nowszy Next.js i przeniosłem cały routing z Pages Router na App Router. Całość w TypeScript, z biblioteką Zod do walidacji danych przekazywanych między backendem a klientem. Wykorzystałem route groups do organizacji obszarów funkcjonalnych (strony informacyjne, kasyno, konto, promocje) w jednej strukturze folderów. To był fundament pod dalszą pracę - dopiero App Router pozwolił precyzyjnie sterować tym, co ma być statyczne, a co dynamiczne, per segment aplikacji.
- Przejście na statyczne strony z dynamicznymi wyspami: pomysł podpatrzyłem u Astro - frameworka, który uwielbiam i na którym stoi ta strona portfolio - i przełożyłem go na Next.js App Router. Bazowe strony były statyczne, a fragmenty zależne od użytkownika żyły jako małe wyspy po stronie klienta. W praktyce: renderowanie statyczne wymuszone na całym drzewie stron, wykrywanie sesji w kliencie i wydzielone komponenty pobierające dane (React Query) tylko tam, gdzie to naprawdę potrzebne. Najpierw prototyp na jednej stronie, potem stopniowe wdrażanie kolejnych grup stron, we wszystkich markach. Na bieżąco dokumentowałem decyzje dla reszty zespołu, prezentowałem postępy na codziennych spotkaniach i rozwiązywałem konflikty z równolegle rozwijanymi funkcjami.
- Warstwa treści z CMS: zdefiniowałem typy treści w CMS i podpiąłem je pod generowanie stron, tak żeby edytorzy mogli sterować treścią, metadanymi i dyrektywami dla wyszukiwarek dla każdej strony osobno, bez konieczności ponownego wdrożenia. Dorzuciłem wspólne komponenty CMS, żeby schemat trzymał się spójnie we wszystkich markach. Przy nowych typach treści prowadziłem sesję wdrożeniową dla content managera.
- SEO i dane strukturalne: zbudowałem fundament JSON-LD (schema.org) -
Organizationdla każdej marki,FAQPagedla stron pomocowych, schematy treści dla stron produktowych. Sitemap generowany z listy stron w CMS, żeby każda nowa strona pojawiała się bez ręcznego dopisywania. Dodałem strategięrobots.txt+<meta robots>per strona pod nowe crawlery AI (GPTBot, ClaudeBot, PerplexityBot) - edytor mógł zdecydować w CMS, gdzie chce być indexowany, a gdzie nie. Do tego Open Graph na wszystkich stronach z treścią, żeby udostępnienia w social media miały poprawne obrazy i opisy zamiast domyślnych. - Wydajność i CDN: wydzieliłem style zdefiniowane w kodzie JavaScript (CSS-in-JS) do osobnych plików z arkuszami stylów, żeby CDN mógł je cache’ować jako niezmienne pliki - co zauważalnie zmniejszyło wagę każdej strony. Przejrzałem też i odchudziłem drzewo providerów, żeby niezalogowani odwiedzający nie ładowali stanu, którego nigdy nie używają.
- Funkcje konta wymagane przez regulatora: zbudowałem obszary konta użytkownika, w których zachowanie każdej opcji dyktowały przepisy. Stany pośrednie (
w toku,aktywna,wygasła) obsługiwane były lokalnie w komponentach - bez zewnętrznych bibliotek stanu, żeby logika specyficzna dla regulatora żyła blisko widoku, który ją prezentował.
Konkretny wpływ zmian
- -79%
Waga strony (mobile, jeden z ekranów statycznych)
- -75%
Waga strony (desktop, widok listy produktów)
- -77%
LCP (mobile, ekran statyczny)
- -88%
LCP (mobile, strona główna)
- -90%
TTFB (mobile, widok listy produktów)
- -73%
TBT (mobile, widok listy produktów)
Skąd te liczby
Liczby pochodzą z audytów Lighthouse, które sam przeprowadziłem podczas przeglądu PR-a jednej z prac nad poprawą wydajności strony. Porównałem produkcję (przed zmianami, jako baseline) z feature branchem po zmianach zdeployowanym na środowisko testowe.
Ograniczenia, które warto znać:
- Traktuję te wyniki jako kierunkowe - Lighthouse waha się między audytami, wynik jednego pomiaru nie jest całą prawdą. Dlatego zawsze wykonuję kilka audytów i patrzę na średnią.
- Podaję wyłącznie zmianę względną. Wartości bezwzględne po obu stronach to liczby klienta i zostają pod NDA - razem z metrykami RUM z produkcji.
- Na produkcji było jeszcze lepiej. Produkcja stoi za CDN-em, który mocniej cachuje statyczne pliki - w tym te arkusze CSS, które wyekstraktowałem. Środowisko testowe tego nie miało, więc realna poprawa była większa, niż pokazują te procenty.
Do e2e-testów dodałem Lighthouse, żeby kolejne regresje perf były wykrywane. Integracja pipeline’u CI/CD z automatycznym uruchamianiem po każdym deployu jest po stronie DevOps.
#Wyzwania
Utrzymanie spójności wielu marek z jednej bazy kodu
Kolejne marki re-eksportują markę bazową przez aliasy ścieżek, więc jedna
zmiana we wspólnym kodzie dotyka ich wszystkich naraz. Sztuka polegała na tym,
żeby wiedzieć, co należy do wspólnych packages, a co musi zostać specyficzne
dla marki. Trzeba było wyłapywać sytuacje, w których „wspólna” zmiana po cichu
psuła którejś marce wersję językową albo reguły jurysdykcji. Siatką
bezpieczeństwa były testy E2E uruchamiane osobno dla każdej marki.
Stopniowe przejście na statyczny rendering
Wielomarkowej aplikacji na produkcji nie da się przełączyć jednym ruchem. Najpierw udowodniłem wzorzec dynamicznych wysp na prototypie jednej strony. Potem migrowałem grupami stron - najpierw statyczne strony informacyjne, potem stronę główną, na końcu cięższe obszary po zalogowaniu. Każdy krok był weryfikowalny i odwracalny. Jedną optymalizację musiałem cofnąć i przebadać od nowa. Dojście do przyczyny problemu liczyło się tu tak samo jak sama zmiana.
Stany UI narzucone przez regulatora
Operacje zgodne z regulatorem to nie zwykłe formularze „zapisz/edytuj/usuń”. Akcja może wymagać potwierdzenia hasłem. Potem trafia w narzucony okres oczekiwania - zmiana czeka jako „w toku”, zanim wejdzie w życie. A do tego wszystko zachowuje się różnie w zależności od jurysdykcji. Poprawne odwzorowanie tych stanów pośrednich i pokazanie ich użytkownikowi w czytelny sposób - to była najtrudniejsza część interfejsu w całym produkcie.
#Wnioski
- W monorepo dla wielu marek architektura jest produktem. Największą wartość dawały nie pojedyncze funkcje, tylko wspólne wzorce - dzięki nim wszystkie marki mogły iść do przodu razem, bez wchodzenia sobie w drogę.
- Statyczne strony opłacają się najbardziej wtedy, gdy uczciwie nazwiesz, które części są naprawdę dynamiczne. Zawężenie dynamicznej części do kilku wysp przyspieszyło całą aplikację i uprościło jej cache’owanie.
- W wielomarkowym monorepo E2E per marka to nie luksus. Jedna zmiana we wspólnym kodzie potrafi po cichu naprawić jedną markę i zepsuć drugą - bez osobnego runnera per brand taki bug widzi dopiero użytkownik końcowy.
Pracujmy razem
Jestem senior frontend developerem. Buduję szybkie, dostępne aplikacje webowe w React i Next.js, z TypeScriptem. Zależy mi na Core Web Vitals, SSR/SSG i skalowalnej, łatwej w utrzymaniu architekturze. Jeśli szukasz kogoś takiego - daj znać.
Napisz do mnie →