Pular para o conteúdo principal

Brand Loading (Server-Side)

Visão geral

A configuração de marca (brand) é carregada no server-side via React Router loader, com cache e tratamento de erro robusto, e passada aos componentes via cadeia de Providers.

Fluxo de dados

_layout.tsx loader (server-side)
├── loadBrandConfig(cloudflareEnv) → BrandResult
│ ├── Cache hit (getBrandCache) → { ok: true, brand, fromCache: true }
│ ├── Cache miss:
│ │ ├── createClient(cloudflareEnv) → ApiClient
│ │ ├── createBrandFromClient(client, { country, language }) → BrandConfig
│ │ │ ├── GET /appearance → transformAppearance
│ │ │ ├── GET /bff/features → transformFeatures
│ │ │ └── POST /bookmaker-settings → transformSettings
│ │ └── { ok: true, brand, fromCache: false }
│ └── Erro → { ok: false, error: { message, status, detail? } }
├── Monta clientEnv (17 campos)
├── loadAuthProfile(request, env) → auth profile (token do cookie)
├── generateSmarticoUserHash(userId, env) → smarticoHash
└── return { brand, clientEnv, auth, smarticoHash }

DefaultLayout (client-side)
├── <EnvProvider env={clientEnv}>
│ └── <BrandProvider brand={brand}>
│ └── <CountryProvider>
│ └── <TranslationProvider>
│ └── Componentes filhos (useBrand(), useClientEnv(), useCountry(), useTranslation())

BrandResult — Discriminated Union

O loadBrandConfig retorna um discriminated union para tratamento explícito de erro:

type BrandResult =
| { ok: true; brand: BrandConfig; fromCache: boolean }
| { ok: false; error: BrandError }

type BrandError = {
message: string;
status: number;
detail?: string;
}

No loader, o resultado é verificado:

const result = await loadBrandConfig(context.cloudflare?.env);
if (!result.ok) {
// Renderiza BrandErrorPage com informações do erro
return { brandError: result.error };
}
const { brand } = result;

Na _layout.tsx, quando brandError está presente, o layout renderiza o componente BrandErrorPage ao invés do conteúdo normal.

Cache de Brand

O brand config é cacheado para evitar 3 requests a cada page load:

  • getBrandCache(ttl) — retorna instância do cache com TTL configurável
  • BRAND_CACHE_TTL — variável de ambiente que define o TTL (default: 3600 segundos / 1 hora)
  • No cache hit, retorna { ok: true, brand, fromCache: true } sem fazer requests
  • No cache miss, faz os 3 requests e armazena o resultado
// Internamente:
const cache = getBrandCache(Number(env.BRAND_CACHE_TTL) || 3600);
const cached = await cache.get(cacheKey);
if (cached) return { ok: true, brand: cached, fromCache: true };

Três requests em paralelo

O @cactus-agents/brand faz 3 requests simultâneos:

GET /appearance

Retorna dados visuais: logo, banners, social links, sponsorships, CSS customizado, cores do tema.

GET /bff/features

Feature flags por país: social auth, carousels, maintenance mode, auth config, módulos ativos, validação, contatos.

POST /bookmaker-settings

Configurações de negócio: nome, SEO, limites de depósito/saque/apostas, bônus, rollover, registro, analytics.

Transformações

O @cactus-agents/brand transforma os dados raw da API em tipos limpos:

  • Banners: {"1":{...}} objects OU []Banner[] sorted by order
  • Links: dedup (último LINK_TO_X entry vence)
  • JSON string fields: parsed via safeParseJson
  • Flags 0|1boolean
  • Country filtering: selectByCountry com fallback chain

BrandConfig resultante

type BrandConfig = {
appearance: BrandAppearance // logo, banners, social, links, sponsorships, colors
features: BrandFeatures // feature flags, auth config, modules, contacts
settings: BrandSettings // name, SEO, limits, bonus, registration, analytics
}

Cadeia de Providers

O DefaultLayout monta a cadeia completa de providers:

EnvProvider → BrandProvider → CountryProvider → TranslationProvider
  1. EnvProvider — disponibiliza clientEnv (17 campos) via useClientEnv()
  2. BrandProvider — disponibiliza BrandConfig via useBrand()
  3. CountryProvider — configuração por país (moeda, locale, validações) via useCountry()
  4. TranslationProvider — setup i18next com namespace e idioma da marca via useTranslation()

Acesso nos componentes

import { useBrand } from '~/context/brand';
import { useClientEnv } from '~/context/env';

function MyComponent() {
const brand = useBrand();
const env = useClientEnv();
// brand.appearance.logo
// brand.features.maintenanceMode
// brand.settings.name
// env.API_BASE_URL
}

clientEnv — 17 campos

O loader monta clientEnv com todas as variáveis necessárias no client-side:

interface ClientEnv {
// Obrigatórios (5)
API_BASE_URL: string;
ORIGIN_DOMAIN: string;
BRAND_LANGUAGE: string;
BRAND_COUNTRY: string;
BRAND_CURRENCY: string;

// Smartico — opcionais (5)
SMARTICO_LABEL_KEY?: string;
SMARTICO_BRAND_KEY?: string;
SMARTICO_VISITOR_LABEL_KEY?: string;
SMARTICO_VISITOR_BRAND_KEY?: string;
SMARTICO_LIBRARY_URL?: string;

// Sports — opcionais (7)
SPORTS_MAIN_PROVIDER?: string;
SPORTS_TEST_PROVIDER?: string;
SPORTS_ALTENAR_INTEGRATION?: string;
SPORTS_ALTENAR_LIBRARY_URL?: string;
SPORTS_BETBY_BRAND_ID?: string;
SPORTS_BETBY_THEME?: string;
SPORTS_BETBY_LIBRARY_URL?: string;
}

Nota: BRAND_TIMEZONE é server-only e não faz parte da ClientEnv.

Meta function — SEO

A _layout.tsx exporta uma meta function que gera tags SEO abrangentes a partir do BrandConfig:

  • <title>, <meta name="description"> — dados de brand.settings
  • Open Graph tags (og:title, og:description, og:image, og:url)
  • Twitter Card tags
  • Favicon e theme-color
  • Canonical URL

Loader completo

Além do brand, o loader da _layout.tsx também:

  1. Carrega o auth profile (token extraído do cookie do request)
  2. Gera o smarticoHash (MD5 para identificação no Smartico)
  3. Monta o clientEnv completo (17 campos)
export async function loader({ context, request }: Route.LoaderArgs) {
const env = context.cloudflare?.env;
const result = await loadBrandConfig(env);
if (!result.ok) return { brandError: result.error };

const clientEnv: ClientEnv = { /* 17 campos */ };
const auth = await loadAuthProfile(request, env);
const smarticoHash = auth?.user
? generateSmarticoUserHash(String(auth.user.id), env)
: null;

return { brand: result.brand, clientEnv, auth, smarticoHash };
}

Arquivos relevantes

ArquivoPapel
app/services/brand.server.tsloadBrandConfig() — BrandResult com cache
app/services/api.server.tscreateClient() — ApiClient SSR
app/routes/_layout.tsxLoader (brand + clientEnv + auth + smarticoHash) + meta function
app/context/brand.tsxBrandProvider + useBrand()
app/context/env.tsxEnvProvider + useClientEnv()
app/context/country.tsxCountryProvider + useCountry()
app/context/i18n.tsxTranslationProvider
app/layouts/DefaultLayout.tsxMonta cadeia de providers
app/components/common/BrandErrorPage.tsxPágina de erro quando brand falha