Skip to content

Multi-brand platform

Regulated online gambling platform serving multiple brands across several European jurisdictions - one Next.js codebase with App Router, static pages with dynamic islands, and a content layer managed through a headless CMS.

Architecture

Static base page + dynamic islands

page.tsx · export const dynamic = "force-static"
staticHeader
staticHero
dynamic island · use clientUser session widgetReact Query · fetch after hydration
staticCategories
staticFooter
Statically rendered block (build time, CDN cached)Dynamic island (hydrated after page load)

Project details

#Context

I led a series of architectural frontend migrations myself on a regulated online gambling platform serving multiple brands - from upgrading Next.js to App Router to laying the SEO foundation for AI crawlers. Details on each of these in the “What I delivered” section below.

Multiple brands reused the same code, each on their own domains and locales - all inside multiple European regulatory regimes with distinctly different rules. Most of my work on this project is covered by an NDA (Non-Disclosure Agreement), so this case study describes the engineering - architecture, patterns, and trade-offs - without naming brands, end customers, or specific vendors.

The whole thing lived in a Next.js App Router monorepo: one base brand app, sibling brands re-exporting shared code through path aliases, plus shared packages and libs. The hardest part wasn’t any single feature - it was keeping multiple brands, domains, and jurisdictions consistent without stalling delivery.

The stack integrated the components you’d expect in this industry: a platform provider SDK with built-in integrations for regulator requirements (limits, self-exclusion, identity verification), payment gateways covering multiple methods per jurisdiction, and a CRM with a gamification layer (campaigns, bonuses, loyalty programme).

#What I delivered

  • Next.js upgrade to a version with App Router: I brought all apps up to a newer Next.js and moved all routing from Pages Router to App Router. The whole thing in TypeScript, using Zod to validate data exchanged between the backend and the client. I used route groups to organise functional areas (info pages, casino, account, promos) inside a single folder tree. This was the foundation for the rest of the work - only App Router gave us precise control over what should be static and what should be dynamic, per segment of the app.
  • Move to static pages with dynamic islands: I borrowed the pattern from Astro - a framework I love and the one this portfolio site runs on - and translated it to Next.js App Router. Base pages were static, and user-dependent fragments lived as small client-side islands. In practice: static rendering forced across the whole page tree, session detection on the client, and dedicated data-fetching components (React Query) only where genuinely needed. First a prototype on one page, then a gradual rollout by page group across all brands. Along the way I kept the rest of the team in the loop - documenting the decisions, presenting progress at daily standups, and untangling conflicts with features being developed in parallel.
  • CMS content layer: I defined the content types in the CMS and wired them into page generation so editors could control content, metadata, and search-engine directives per page, without a redeploy. I added shared CMS components so the schema stayed consistent across all brands. For new content types I ran an onboarding session with the content manager.
  • SEO and structured data: I built the schema.org JSON-LD foundation - Organization per brand, FAQPage for help pages, content schemas for product pages. The sitemap was generated from the CMS story list, so every new page appeared without hand-editing sitemap.xml. I added a robots.txt + per-page <meta robots> strategy targeting the new AI crawlers (GPTBot, ClaudeBot, PerplexityBot) - editors could decide in the CMS where they wanted to be indexed and where not. On top of that, Open Graph on every content page so social-media shares carried the right image and description instead of the defaults.
  • Performance and CDN: I extracted the CSS-in-JS styles into standalone stylesheet files so the CDN could cache them as immutable assets - which noticeably shrank every page’s weight. I also went through the provider tree and trimmed it down, so logged-out visitors weren’t loading state they’d never use.
  • Regulator-mandated account features: I built the account areas where every option’s behaviour was dictated by the rules. Intermediate states (pending, active, expired) were handled locally in the components - no external state library - so the regulator-specific logic lived next to the view that rendered it.
Metryki

Real impact of the changes

  • Page weight (mobile, one of the static screens)

    -79%
  • Page weight (desktop, product list view)

    -75%
  • LCP (mobile, static screen)

    -77%
  • LCP (mobile, homepage)

    -88%
  • TTFB (mobile, product list view)

    -90%
  • TBT (mobile, product list view)

    -73%
Where these numbers come from

The numbers come from Lighthouse audits I ran myself during the code review of one of the PRs improving the site’s performance. I compared production (pre-change, as the baseline) with the feature branch deployed to the test environment.

Caveats worth knowing:

  • I treat these as directional - Lighthouse fluctuates between audits, and a single measurement isn’t the whole truth. That’s why I always run several audits and look at the average.
  • I report relative change only. The absolute values on both sides are the client’s own numbers and stay under the NDA, along with the RUM metrics from production.
  • Production came out even better - prod sits behind a CDN with more aggressive caching of static assets, including the extracted stylesheet files, which the test environment didn’t have. So I treat these deltas as conservative.

I added Lighthouse to our e2e tests so future perf regressions get caught. The CI/CD pipeline integration - running Lighthouse automatically on every deploy - sits on the DevOps side.

#Challenges

Keeping multiple brands consistent from a single codebase

Sibling brands re-export the base brand through path aliases, so a single change in shared code touches all of them at once. The art was knowing what belongs in shared packages and what has to stay brand-specific. We had to catch the cases where a “shared” change would silently break one brand’s locale or one jurisdiction’s rules. Our safety net was E2E tests, run separately per brand.

Gradual move to static rendering

A live multi-brand app can’t be flipped in one move. I first proved the dynamic-islands pattern on a prototype for one page. Then I migrated by page group - static info pages first, then the homepage, and finally the heavier logged-in areas. Every step was verifiable and reversible. I had to roll back one optimisation and re-investigate before shipping it safely. Getting to the root cause counted here as much as the change itself.

UI states mandated by the regulator

Compliance flows aren’t your typical CRUD. An action might require a password confirmation. Then it lands in a mandated waiting period - the change sits as pending before taking effect. And the whole thing behaves differently depending on jurisdiction. Modelling those intermediate states correctly - and surfacing them to the user clearly - was the hardest part of the interface in the whole product.

#Takeaways

  • In a multi-brand monorepo, the architecture is the product. The biggest wins didn’t come from individual features - they came from the shared patterns that let all brands move forward together without getting in each other’s way.
  • Static pages pay off the most when you’re honest about which parts are genuinely dynamic. Narrowing the dynamic surface to a handful of islands sped the whole app up and simplified its caching story.
  • In a multi-brand monorepo, brand-scoped E2E isn’t a luxury. A single change in shared code can quietly fix one brand and break another - without a per-brand runner, that bug reaches the end user before you notice.

Let's work together

I'm a senior frontend engineer focused on building fast, accessible web experiences with clean, type-safe TypeScript. I work mainly with React and Next.js, and I care about SSR/SSG, Core Web Vitals, and maintainable component systems. I value clear communication and shipping practical solutions - if that sounds like what you need, let's talk.

Get in touch →