Pular para o conteúdo principal

Games (Casino)

Integracao do cassino no template. Lista jogos, categorias, providers, busca, start-game, votos e estatisticas via @cactus-agents/games, com cache server-side via GamesCacheService sobre o platform-cache engine.

Arquitetura

As listagens de cassino sao uma casca SPA (commit 2e130e204, 2026-07-28, breaking): o loader do doc request nao paga o BFF de jogos. Ele resolve apenas meta (<title>, canonical, JSON-LD) e o getBase() leve; a grade e as rows chegam no client.

Doc request (SSR) Client (pos-mount)
| |
v v
loader shell-only useGamesListing -> /api/games/{category,provider}/:slug
(meta + getBase leve) useHubRows -> /api/games/rows/:page
| |
v v
GamesCacheService useGamesStore (Zustand)
|
v
platform-cache engine (~/services/platform-cache.server)
|
v
@cactus-agents/games (SDK) -> BFF

:::warning Trade-off de SEO aceito Rows e grade das listagens saem do HTML do doc request — nomes de jogos deixam de ser indexaveis nessas paginas. title/meta/canonical/JSON-LD de <head> continuam SSR. Decisao de produto registrada na spec 2026-07-28-spa-shell-listagens-design.md.

Consequencia operacional: o orderBy por stats do overlay categories.personalize passa a valer por pagina carregada, nao globalmente.

Excecoes que mantem SSR proprio: a grade da donald e o fluxo refer-friend do provedor. :::

O hook hibrido useHybridGamesList e o filtro in-memory filterLocal foram removidos no mesmo commit — existe um caminho unico, server-side.

Arquivos principais

ArquivoTipoDescricao
app/store/games.tsStore (Zustand)Estado completo do cassino
app/services/games.server.tsService (server)resolveCassinoMode(), createGamesCacheService(opts)
app/services/games.cache.server.tsService (server)GamesCacheService — todas as chamadas do SDK sobre o cache engine
app/utils/games-list-window.server.tsUtil (server)fetchListWindow() — janela por offset sobre getListPage
app/hooks/useGamesListing.tsHookListagem client-side de categoria/provedor (filtro, paginacao, facets)
app/hooks/useHubRows.tsHookRows curadas dos hubs /games e /games/live
app/config/sections/home-rows.legacy.tsConfigOrdenacao das rows da home (modo legacy)
app/config/sections/casino-rows.legacy.tsConfigRows do hub casino (modo legacy)
app/config/sections/casino-live-rows.legacy.tsConfigRows do hub casino.live (modo legacy)
app/config/catalog/categories.personalize.tsConfigOverlay opcional (orderBy + displayPriority) sobre categorias do BFF
app/config/catalog/category-chip-order.tsConfigOrdem curada dos chips de categoria (lista vazia = ordem do BFF)
app/config/content/game-details.server.tsConfig (server)SEO e descriptions de jogos (brand-overridable)
app/utils/game-details.server.tsUtil (server)Resolve templates e retorna details por jogo
app/utils/game-artwork.tsUtilgetGameArtworkProps() — fonte unica da arte de jogo

:::note Nao existe mais um games.client.ts O acesso client-side e feito pelos hooks (useGamesListing, useHubRows) chamando as rotas internas /api/games/* direto. Nao ha service singleton no client. :::

Store — useGamesStore

Estado principal (lista viva — consulte app/store/games.ts):

  • homeRows — rows da home page (banners, categorias, widgets)
  • categories / providers — metadados do catalogo
  • hubChips — chips de categoria por hub (cassino / live), semeados pelo useHubRows
  • gamesBySlug / gameStatsCache — caches client de jogo e estatisticas
  • topWins / lastWins — maiores ganhos e ultimos ganhos
  • searchTerm / searchResults / searchTotal — busca
  • favoriteSlugs — slugs favoritados (ver Favorites)
  • activeGame / gameStats / gameIframe — pagina de detalhe e iframe
  • userVote / voteCount — likes/dislikes

Modos de cassino — CASSINO_MODE

resolveCassinoMode(cf) (app/services/games.server.ts) le a env CASSINO_MODE. Valor ausente ou invalido cai em "legacy".

Modohomecasino / casino.live
legacyconfig estatico (*-rows.legacy.ts)config estatico
api_newBFF curado (GET /casino-games/page/{home,cassino,cassino_live})BFF curado
mixedBFF curadoconfig estatico

mixed cobre o caso em que o backoffice ja curou so a home da brand (getPageRows("home") retorna rows, mas cassino/cassino_live ainda retornam 0).

Em legacy cada page e montada a partir de um arquivo *-rows.legacy.ts (brand-overridable):

// app/config/sections/home-rows.legacy.ts
export const legacyHomeRows = [
{ slug: "home-banners", type: "widget" },
{ slug: "search-field", type: "widget" },
{ title: "Slots", slug: "slots", type: "games-api", maxItems: 15 },
// ...
];

Tipos de row aceitos em config: widget, games-api, providers-api, games-fixed.

Pra curar ordenacao/stat exibida em categorias vindas do BFF (tanto em legacy quanto em api_new), use o overlay app/config/catalog/categories.personalize.ts.

Rotas

Os paths abaixo sao os defaults — configuraveis via Route Registry (~/config/routes/paths). Use routeHref() e gameHref() de ~/utils/routes para gerar links.

ChavePath padraoDescricao
casino/gamesHub do cassino (rows curadas)
casino.live/games/liveHub de cassino ao vivo
casino.category/games/category/:slugCategoria de jogos
casino.providers/games/providersGrid de providers
casino.provider/games/providers/:slugJogos de um provider
casino.play/games/:provider/:gameDetalhe de jogo (SEO, stats, votos, iframe)

Existe tambem um redirect /casino → path de cassino da brand (routes/casino-redirect.ts), registrado apenas quando o path da brand nao e literalmente /casino — o sportsbook First manda usuarios pra /casino.

API Routes

Todas registradas em app/router/routes.ts (fonte unica):

RotaDescricao
api/games/listListagem paginada generica (proxy do getListPage)
api/games/category/:slugListagem de categoria por offset/limit (+ q, provider)
api/games/provider/:slugListagem de provedor, mesmo contrato
api/games/rows/:pageRows curadas de um hub (cassino / cassino_live)
api/games/searchBusca de jogos
api/games/suggestionsSugestoes de busca
api/games/by-slugsResolve varios jogos por slug
api/games/top-gamesTop games
api/games/top-winsTop wins
api/games/stats-batchEstatisticas em lote
api/games/statistics-dlEstatisticas brutas (download)
api/games/startStart game (autenticado)
api/games/voteCriar/remover voto
api/cache/purgePurge do cache (nao existe mais api/cache/games/purge)
api/cache/inspectInspeciona o estado do cache

Cache server-side

O GamesCacheService encapsula as chamadas do SDK sobre o platform-cache engine (app/services/platform-cache.server.ts). O antigo ServerCache / cache.server.ts nao existe mais.

Cada metodo pede um resource name ao engine; TTL e stale-while-revalidate sao politica do engine, declarada em front-ops/config/cache/defaults.yml — nao no template. Resources usados hoje (14, consulte app/services/games.cache.server.ts): homeRows, casinoRows, casinoLiveRows, gamesBase, allGames, gameCategory, gameProvider, gameDetail, gameStats, gameListPage, topGames, topWins, lastWins, gameTopWins.

Ver Caching pra numeros de TTL, geracao de cache e as camadas acima disso. Nao duplicamos TTL nesta pagina — ele muda por deploy do front-ops.

Metodos principais

GamesCacheService (app/services/games.cache.server.ts) — lista viva, consulte o arquivo:

MetodoDescricao
getHome()Rows da home
getCasinoRows() / getCasinoLiveRows()Rows dos hubs
getBase()Categorias + providers
getAllGames()Agregado de todos os jogos
getListPage(params)Pagina do BFF com filtro (categories, providers, term, page, perPage)
getByCategory(slug) / getByProvider(slug)Agregados por categoria/provedor
getDetail(slug) / resolveGamesBySlugs(slugs)Jogo(s) por slug
getStats(slug) / getStatsBatch(slugs) / getStatsBatchFromRows(rows)Estatisticas
getTopGames() / getTopWins() / getLastWins() / getGameTopWins(slug) / getHighPayers()Widgets de ganhos
getSuggestions(params) / search(params)Busca
purge(segment) / purgeAll()Invalidacao

Facets no GET /casino-games/list

O endpoint de list do BFF passou a devolver facets (core 68ec4ad; type GameListFacets em @cactus-agents/games), com a disponibilidade de providers e categorias pra aquela query:

interface GameListFacets {
providers?: GameListFacetProviderRaw[];
categories?: GameListFacetCategoryRaw[];
}

O transform faz passthrough (transformGameListResponse) e GameFilterResult.facets fica opcional — undefined quando a query nao tem filtro e em filtros in-memory (filterGames). Consumidores tratam undefined como "sem informacao de disponibilidade" (tudo ativo).

No template os facets sao consumidos assim:

  1. fetchListWindow() (app/utils/games-list-window.server.ts) devolve facets da primeira pagina tocada — o BFF retorna o mesmo conjunto pra qualquer pagina da mesma query.
  2. As rotas api/games/{category,provider}/:slug repassam no envelope { data, total, offset, limit, hasMore, facets }.
  3. useGamesListing captura os facets so da query base (sem term nem sub-filtro) e os congela, pra que os chips nao pisquem enquanto o usuario filtra por cima.
  4. GamesFilterBar usa availableProviderSlugs / availableCategorySlugs derivados dali pra desabilitar chips sem resultado.

Janela por offset (fetchListWindow)

O infinite scroll pede por offset, que pode desalinhar das paginas do BFF (SSR manda 120, batches de 48 → offset 120 cruza as paginas 3 e 4). fetchListWindow busca no maximo 2 paginas — ambas do cache gameListPage — e fatia o intervalo exato.

Listagem client-side — useGamesListing

import { useGamesListing } from "~/hooks/useGamesListing";

const [state, actions] = useGamesListing({ kind: "category", slug });

Comportamentos garantidos pelo hook:

  • fetch inicial offset=0&limit=48; loading ate resolver (grade mostra GameGridSkeleton);
  • term com debounce de 250ms; troca de provider/categoria refetcha imediato;
  • loadMore() pagina por offset e concatena;
  • um requestId descarta respostas fora de ordem (filtro trocado no meio);
  • falha no fetch inicial → error: true + retry(); falha no loadMore e silenciosa;
  • enabled: false desliga o hook (usado quando a pagina tem lista SSR propria).

Arte de jogo — regra dura

Sempre getGameArtworkProps(image, sizes) de ~/utils/game-artwork. Nunca monte um ladder de widths proprio, nunca passe aspectRatio/height.

O Cloudflare Images codifica o transform no path da URL, entao cada combinacao de largura/altura/recorte e um recurso distinto: cache-key de browser propria, entrada de cache CF propria, download proprio. Duas superficies pintando a mesma arte com ladders diferentes baixam a mesma imagem duas vezes.

import { getGameArtworkProps } from "~/utils/game-artwork";

const artwork = getGameArtworkProps(game.image, "(max-width: 640px) 115px, 180px");
<img src={artwork.src} srcSet={artwork.srcSet} sizes={artwork.sizes} alt={game.name} />;
  • sizes e o unico parametro por superficie — descreve o slot de layout em CSS px.
  • O recorte fica 100% no CSS (object-cover/object-contain numa caixa de aspect fixa).
  • O teto do ladder nao passa de ~300px, a largura do master da arte de jogo. Acima disso o CF faz upscale: mais bytes, zero detalhe novo e cache frio.
  • O unico knob e gameCardConfig.thumbWidths / thumbFallbackWidth (~/config/widgets/game-card, brand-overridable) — e ele vale pra todas as superficies, nao so pros cards.
  • Nao "corrija" um sizes de game page pro slot real sem checar a convergencia em DPR 1: um degrau intermediario separa card e preview e vira um download extra silencioso. O teste game-artwork-brands.test.ts cobre isso.

Detalhes completos (incluindo a tabela de slots medidos e as duas regras de convergencia) no docblock de app/utils/game-artwork.ts e em Imagens.

Componentes

Lista viva — consulte app/components/games/.

Hubs e listagem

  • GameSection — secao generica (titulo + grid/carrossel)
  • GameCarousel — carrossel horizontal de jogos
  • GameGrid / GameGridSkeleton — grid responsivo e seu skeleton
  • GameCard + variantes (GameCardClassic, GameCardCover, GameCardStacked) resolvidas por game-card-registry.ts
  • GamesFilterBar / CassinoFilterChips — filtros por categoria/provider
  • GamesPageHeader, CassinoGridLayout, HubShellSkeleton
  • ProvidersGrid — grid de providers

Detalhe

  • GameIframe — iframe do jogo (full-screen ou embed)
  • GameDetailsBar — tags, RTP, description com HTML
  • GameStats / GamePreviewStatPills — estatisticas por periodo
  • GameVotes, GameShareRow, GameWinners, GameFaqPreview
  • RelatedGames — jogos relacionados (curadoria opcional em ~/config/catalog/smart-related)
  • Subpastas game-page/ e pre-game/ com os blocos especificos da pagina

Busca

A busca tem paginas proprias (search, search.casino, search.sports) e componentes em app/components/search/ (SearchPageInput, IdleState, ResultsState).

Game Details Config

app/config/content/game-details.server.ts centraliza SEO e descriptions de jogos. E server-only — nunca entra no bundle client.

Estrutura

export const gameDetails: GameDetailsConfig = {
defaults: {
meta_title: "{game_name} - Jogar Online | {brand_name}",
meta_description: "{game_name} é um jogo de cassino online disponível no {brand_name}...",
front_description: "<strong>{game_name}</strong> é um jogo de cassino online...",
},
games: {
"pgsoft/fortune-tiger": {
meta_description: "Fortune Tiger é um dos slots mais populares...",
front_description: "<strong>Fortune Tiger</strong> é um dos slots...",
// meta_title omitido → herda do defaults
},
},
};

Campos

CampoUsoFormato
meta_title<title> e og:titletexto puro
meta_description<meta description> e JSON-LDtexto puro
front_descriptionExibido na pagina do jogoHTML (<strong>, <em>, etc.)

Template tags

TagValor
{game_name}Nome do jogo
{game_provider}Nome do provedor
{game_rtp}RTP (fallback: 97 se nao disponivel)
{brand_name}Nome da marca (brand.settings.name)

Resolucao

  1. Se o jogo tem entry em games, cada campo presente sobrescreve o default
  2. Campos omitidos no entry herdam do defaults
  3. Apos o merge, template tags sao resolvidas com dados reais

Override por brand

O arquivo e brand-overridable (overrides/<brand>/app/config/content/game-details.server.ts). Como todo override, a substituicao e do arquivo inteiro — nao ha deep-merge com o base. Veja Override Files.

Os knobs de <head> por jogo (title/description dedicados, h1/h3 visiveis, AggregateRating) vivem num arquivo separado: app/config/seo/games-seo.server.ts — ver SEO.