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
| Arquivo | Tipo | Descricao |
|---|---|---|
app/store/games.ts | Store (Zustand) | Estado completo do cassino |
app/services/games.server.ts | Service (server) | resolveCassinoMode(), createGamesCacheService(opts) |
app/services/games.cache.server.ts | Service (server) | GamesCacheService — todas as chamadas do SDK sobre o cache engine |
app/utils/games-list-window.server.ts | Util (server) | fetchListWindow() — janela por offset sobre getListPage |
app/hooks/useGamesListing.ts | Hook | Listagem client-side de categoria/provedor (filtro, paginacao, facets) |
app/hooks/useHubRows.ts | Hook | Rows curadas dos hubs /games e /games/live |
app/config/sections/home-rows.legacy.ts | Config | Ordenacao das rows da home (modo legacy) |
app/config/sections/casino-rows.legacy.ts | Config | Rows do hub casino (modo legacy) |
app/config/sections/casino-live-rows.legacy.ts | Config | Rows do hub casino.live (modo legacy) |
app/config/catalog/categories.personalize.ts | Config | Overlay opcional (orderBy + displayPriority) sobre categorias do BFF |
app/config/catalog/category-chip-order.ts | Config | Ordem curada dos chips de categoria (lista vazia = ordem do BFF) |
app/config/content/game-details.server.ts | Config (server) | SEO e descriptions de jogos (brand-overridable) |
app/utils/game-details.server.ts | Util (server) | Resolve templates e retorna details por jogo |
app/utils/game-artwork.ts | Util | getGameArtworkProps() — 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 catalogohubChips— chips de categoria por hub (cassino/live), semeados pelouseHubRowsgamesBySlug/gameStatsCache— caches client de jogo e estatisticastopWins/lastWins— maiores ganhos e ultimos ganhossearchTerm/searchResults/searchTotal— buscafavoriteSlugs— slugs favoritados (ver Favorites)activeGame/gameStats/gameIframe— pagina de detalhe e iframeuserVote/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".
| Modo | home | casino / casino.live |
|---|---|---|
legacy | config estatico (*-rows.legacy.ts) | config estatico |
api_new | BFF curado (GET /casino-games/page/{home,cassino,cassino_live}) | BFF curado |
mixed | BFF curado | config 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.
| Chave | Path padrao | Descricao |
|---|---|---|
casino | /games | Hub do cassino (rows curadas) |
casino.live | /games/live | Hub de cassino ao vivo |
casino.category | /games/category/:slug | Categoria de jogos |
casino.providers | /games/providers | Grid de providers |
casino.provider | /games/providers/:slug | Jogos de um provider |
casino.play | /games/:provider/:game | Detalhe 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):
| Rota | Descricao |
|---|---|
api/games/list | Listagem paginada generica (proxy do getListPage) |
api/games/category/:slug | Listagem de categoria por offset/limit (+ q, provider) |
api/games/provider/:slug | Listagem de provedor, mesmo contrato |
api/games/rows/:page | Rows curadas de um hub (cassino / cassino_live) |
api/games/search | Busca de jogos |
api/games/suggestions | Sugestoes de busca |
api/games/by-slugs | Resolve varios jogos por slug |
api/games/top-games | Top games |
api/games/top-wins | Top wins |
api/games/stats-batch | Estatisticas em lote |
api/games/statistics-dl | Estatisticas brutas (download) |
api/games/start | Start game (autenticado) |
api/games/vote | Criar/remover voto |
api/cache/purge | Purge do cache (nao existe mais api/cache/games/purge) |
api/cache/inspect | Inspeciona 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:
| Metodo | Descricao |
|---|---|
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:
fetchListWindow()(app/utils/games-list-window.server.ts) devolvefacetsda primeira pagina tocada — o BFF retorna o mesmo conjunto pra qualquer pagina da mesma query.- As rotas
api/games/{category,provider}/:slugrepassam no envelope{ data, total, offset, limit, hasMore, facets }. useGamesListingcaptura os facets so da query base (semtermnem sub-filtro) e os congela, pra que os chips nao pisquem enquanto o usuario filtra por cima.GamesFilterBarusaavailableProviderSlugs/availableCategorySlugsderivados 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;loadingate resolver (grade mostraGameGridSkeleton); termcom debounce de 250ms; troca de provider/categoria refetcha imediato;loadMore()pagina por offset e concatena;- um
requestIddescarta respostas fora de ordem (filtro trocado no meio); - falha no fetch inicial →
error: true+retry(); falha noloadMoree silenciosa; enabled: falsedesliga 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} />;
sizese o unico parametro por superficie — descreve o slot de layout em CSS px.- O recorte fica 100% no CSS (
object-cover/object-containnuma 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
sizesde 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 testegame-artwork-brands.test.tscobre 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 jogosGameGrid/GameGridSkeleton— grid responsivo e seu skeletonGameCard+ variantes (GameCardClassic,GameCardCover,GameCardStacked) resolvidas porgame-card-registry.tsGamesFilterBar/CassinoFilterChips— filtros por categoria/providerGamesPageHeader,CassinoGridLayout,HubShellSkeletonProvidersGrid— grid de providers
Detalhe
GameIframe— iframe do jogo (full-screen ou embed)GameDetailsBar— tags, RTP, description com HTMLGameStats/GamePreviewStatPills— estatisticas por periodoGameVotes,GameShareRow,GameWinners,GameFaqPreviewRelatedGames— jogos relacionados (curadoria opcional em~/config/catalog/smart-related)- Subpastas
game-page/epre-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
| Campo | Uso | Formato |
|---|---|---|
meta_title | <title> e og:title | texto puro |
meta_description | <meta description> e JSON-LD | texto puro |
front_description | Exibido na pagina do jogo | HTML (<strong>, <em>, etc.) |
Template tags
| Tag | Valor |
|---|---|
{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
- Se o jogo tem entry em
games, cada campo presente sobrescreve o default - Campos omitidos no entry herdam do
defaults - 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.