Changelog - 09/05/2026
Geo-SEO multi-país da rede 7k (BR / CL / NG / FI)
- Cluster recíproco de hreflang sem
x-default: cada um dos quatro domínios da família 7k declara as outras três variantes como alternates explícitas —7k.bet.br(pt-BR, self) +cl.bet7k.com(es-CL) +ng.7k.bet(en-NG) +fi.7k.bet(fi-FI). Os três overrides novos (overrides/{cl-bet7k-com,fi-7k-bet,ng-7k-bet}/app/config/seo/hreflang.ts) e a atualização do 7k-bet-br removem ox-defaultque apontava pro Brasil — cada brand é geo-exclusiva (KYC, regulação e moeda locais), então não existe "world fallback" legítimo. Resultado: o Google só serve a variante quando o idioma/região do usuário matcha um alternate explícito (ex: russo ou alemão não vê nenhum domínio, em vez de cair no7k.bet.br). Schema.org/Organizationenriquecida com sinais de país:buildOrganizationJsonLd()(app/utils/seo.ts) agora aceitaaddressCountry(ISO-3166 alpha-2) +areaServedName(nome do país em inglês) e emiteaddress.addressCountry+areaServed.@type=Country. O_layout.tsxresolve ambos viagetCountryByAlpha3de@cactus-agents/country-config— mesmo catálogo usado pelo api-client pra derivar geo headers, evita duplicar mapping alpha-3 → alpha-2 + nome do país.- Novo JSON-LD
WebSitecominLanguage: segundo blob de Schema.org reforçando o idioma da página de forma independente do<html lang>. Google parseiainLanguageseparadamente, então emitir os dois dá ao algoritmo dois sinais redundantes de locale. - Header HTTP
Content-Languageinjetado em toda response HTML peloapp/entry.server.tsx(text/html only — JSON/JS unaffected). Sourced do define__BRAND_LANGUAGE__do Vite e mapeado pra forma BCP 47. Adiciona uma terceira camada de sinal de locale (header HTTP + JSON-LD +<html lang>). - Helper compartilhado
app/utils/locale.tsconsolida os mapasBRAND_LANGUAGE → BCP 47(pra<html lang>,Content-Language,inLanguage, hreflang) eBRAND_LANGUAGE → og:locale(pra OpenGraph). Antes os dois mapas ficavam inline emroot.tsxe_layout.tsx— havia risco de drift entre<html lang="pt-BR">eContent-Language: pt. Cobertura: oito códigos ativos (pt-br,pt,en,es,es-mx,es-cl,en-ng,fi-fi) com fallback prapt-BR. useCasinoNomenclatureagora é guiado por i18n, não mais por regex country-aware. O hook colapsa qualquer string vinda do BFF (campocasinoNomenclature) num enum de 2 valores ("casino" | "games") e resolve o substantivo traduzido viat("layout:sidebar.casino")/t("layout:sidebar.nomenclature_games"). Adicionar país novo virou uma linha em cadalayout.jsondo core — zero mudança no front. Dependência deuseBrand().features.country?.coderemovida.- Doc operacional fora-do-código: o sinal mais forte de geo-targeting pra gccTLDs (
.com,.bet) continua sendo o Google Search Console → International Targeting → Country, que é manual. Walkthrough escrito emdocs/superpowers/specs/2026-05-09-google-search-console-geo-targeting.mdpro time de SEO/marketing.
Redirects e cobertura de 404 legados
- Cross-brand redirects pro grupo 7k (PR #782): novo arquivo base
app/config/routes/legacy-redirects-cross-brand.ts(default vazio, brand-overridable) carrega o mapa de redirects entre os domínios 7k. Cada uma das quatro brands (7k-bet-br, cl-bet7k-com, fi-7k-bet, ng-7k-bet) ganha um override idêntico de 242 entradas com os patterns PT-BR / ES / EN — o filtroactiveLegacyRedirectsdropa entriesfrom === toem runtime, então cada brand só consome o que aponta pra fora do seu próprio path. Resolve o cenário em que o Cloudflare roteia entre brands irmãs (geo mismatch) e o visitor caía num path foreign-shaped (ex:/casino/juego/evolution/fan-tanaterrissando em7k.bet.br). - Cobertura ampliada de redirects do legado Vue (PR #784):
app/config/routes/legacy-redirects.tscresceu de poucas dezenas pra cobrir o umbrella inteiro/casino/...(live, category/:slug, providers, providers/:slug, :provider/:game, :provider/:game/faq) — recriando o catchall/casino/[...casino].vueque o Nuxt tinha e o novo stack nunca reproduziu. Inclui ainda variantes localizadas (/cassino/...PT-BR,/casino/{en-vivo,categoria,proveedores,juego/...}ES,/casino/play/...EN), esportes (/esportes,/deportes), help (/ajuda,/ayuda,/help), gamification em todos os locales, área de usuário (/usuario/*,/jugador/*,/player/*), histórico legado (/user/history/{casino,full,poker,sports}),/user/irpf→user.incomeReport, e LPs estáticas (/cassino-online,/cassino-pix,/casa-de-aposta,/poker, etc.). Repro fixado:/casino/live→ 301/cassino/ao-vivo. - Mitigação dos top 404 do Cloudflare Analytics (PR #786) em três frentes:
- Filtro de garbage paths no Worker (~30k 404s/dia eliminados):
handleMiddlewareRoute()curto-circuita paths com URL/HTML injection ([http://,[https://,://,<,>,%3a,%2f,%3c,%3e) com 204 No Content + cache imutável de 24h. Bots e marketing tags com macros não-substituídas (ex:/cassino/jogar/evolution/[https://bat.bing.net/bat.js]) param de poluir o Observability — o CF cacheia o body vazio e hits repetidos nem chegam ao Worker. 42 unit tests fixam o regex. - Restaura
/twa.jspara 7k-bet-br e cassino-bet-br (~4.6k 404s/dia + bridge AppsFlyer corrigida): script restaurado byte-a-byte do legado Nuxt (md5799d7078, idêntico nas três brands BR antigas) bootstrappawindow.twa.getPostMessageService()consumido pelouseAppsFlyer.tsdentro do APK Android (br.bet.k.twa,cassino.bet.br.twa). Sem o script, o AppsFlyer caía silenciosamente pro path HTTP S2S. /cassino/category/:slughíbrido PT+EN (~5.9k 404s/dia): paths indexados como/cassino/category/cassino-ao-vivo/popular(PT-prefix + EN-segment + filtro/popularlegado) agora redirecionam pracasino.category(/cassino/categoria/:slug).
- Filtro de garbage paths no Worker (~30k 404s/dia eliminados):
- Combinado com PR #784, a plataforma mitiga ~46k 404s/dia entre todas as brands.
404 dentro do layout
- Catch-all splat (
app/routes/$.tsx) registrado como último filho de_layout, então URLs não-matched (ex:/asda) preservam header / sidebar / footer / topbar em vez de cair na ErrorBoundary do root com tela cheia "nua". ErrorBoundaryde layout reusa o loader data viauseRouteLoaderDatae renderiza<ErrorPage variant="inline">como children do layout. Aplica também a child routes que dão throw 404 (promotions/$slug,lp/$slug,blog/$slug, recents com flag desligada, etc.).ErrorPageganhavariant: "standalone" | "inline"— standalone mantém o full-screen wrapper usado peloroot.tsxem falha catastrófica; inline drop-a o wrapper pra fluir dentro do main do layout. Removidos o CTA verde "Voltar ao início" (as 3 nav pills cobrem) e o label "OU NAVEGUE PARA". Pills encolhem em telas pequenas (gap, padding, font-size, icon size) pra caber em 320px sem wrap.- Novo bloco
ErrorDetailssempre visível abaixo das nav pills: payload copy-pasteable em texto puro (URL, user id, build, timestamp, referrer, full UA) pro suporte triar quando o cliente printar/filmar um 404. Auth-reactive viauseAccountsStorecom o pattern padrãoauthHydrated+ SSR fallback. - Status HTTP agora retorna 404 real (era 200 em alguns casos bubbleados) com
Cache-Control: no-storepra crawlers e monitoring verem o code certo. - Bump do
@cactus-agents/i18npra^0.87.0pelas novas chaveserror_page.details_*(pt-br, pt, es, en).
Sitemap reorganizado
- Sitemap de jogos splittado por provider sob namespace
/sitemap-games/(PR #781). Estrutura nova:
/sitemap.xml (root index)
├── /sitemap-static.xml
├── /sitemap-games/<provider>.xml (NEW — um urlset por provider)
├── /sitemap-games/categories.xml (renomeado de /sitemap-categories.xml)
├── /sitemap-games/providers.xml (renomeado de /sitemap-providers.xml)
├── /sitemap-promotions.xml
├── /sitemap-blog.xml
├── /sitemap-faq.xml
└── /sitemap-legal.xml (NEW — incorpora PR #743)
- Por que splittar por provider: Search Console reporta indexing status por sitemap → granularidade por provider expõe qual está falhando crawl; invalidação de cache CDN isolada por família (game novo da PG Soft não bust-a o sitemap da Pragmatic); payloads naturalmente abaixo do limite de 50k URLs sem precisar shard. Formato directory-style (
/sitemap-games/<provider>.xml) escolhido em vez do dash-style porque ocompilePathdo React Router 7 só matcha tokens:paramprecedidos de/. - Slugs reservados:
categorieseprovidersnão podem ser claimados por provider real — as rotas literais irmãs vencem por especificidade no RR e o loop do index pula essas keys defensivamente. A rota legada/sitemap-games.xmlflat foi removida (404). Recomendação pós-deploy: ressubmeter/sitemap.xmlno Search Console pra acelerar reindex.
API e classificação de erros
- Helper
app/utils/proxy-error.server.ts(proxyErrorResponse(err, fallback, init?)+unauthorizedNoToken()) substituiu o envelope{ ok: false, error, detail }em 60 rotas internas/api/*(wallet, payments, user, kyc, rewards, income-report, validation, sports, games, address, favorites,auth/{profile,recheck-spa,refresh}). Em 401/403 o body do BFF é passado intacto pro classifier de@cactus-agents/api-client— antes, o top-levelerror: "wallet_fetch_failed"lockava a lookup e areasonreal do BFF (escondida emdetail) nunca era avaliada. Resultado: todo 401 colapsava emEM0005(catch-all), todo 403 emEM0011, perdendoEM0001 no_token,EM0003 expired,EM0006 ip_changed,EM0009 liveness_required. Reportado em 05/2026: usuário cassino.bet.br tomou#EP0710-EM0005no/api/wallet/refreshmid-game enquanto o log do CF mostravareason: "wallet_fetch_failed"(o envelope, não a causa). - Fluxos pre-auth (login, register, recovery, social, validate-document, logout) intencionalmente preservados — não disparam
handleUnauthorizede dependem demapBff*Error/FormErrorCodepra UX form-level. - 32 testes novos no helper, mais 5 ponta-a-ponta contra o classifier real. Suite total: 71 files / 685 tests passando.
Fix de hidratação dos widgets de Wins
- Eliminada a flash de skeleton no F5 dos widgets
BiggestWinsCarousel,LatestBetsCarousel,WinsCombinedWidget,BiggestWinsListWidgeteHomeWinsRightColumn. Duas causas combinadas:- Widgets liam
topWins/lastWinsda Zustand store gated em_isHomeHydrated, que só viravatruenumuseEffectde rota — SSR + primeiro render mostravam skeleton até o mount. - Todos estavam atrás de
React.lazy()+<Suspense fallback>— mesmo após corrigir #1 via props, o Suspense substituía brevemente o HTML SSR peloSectionSkeletonenquanto o chunk carregava.
- Widgets liam
- Fix espelha o pattern do
TopGamesSection: widgets recebemtopWins/lastWinscomo props threaded de cada loader →HubRowRenderer→renderWidget. Reads internos da store + gates_isHomeHydrated+ skeleton blocks removidos. Wins widgets convertidos delazy()pra static imports (custo de bundle ~4-6KB gzip — deps compartilhadas comoMarquee,WinCard,SectionTitlejá residentes).SectionSkeletonmantido pros outros lazy widgets (ProvidersCarousel,RecommendedBanners,FeaturedGame,Tournaments,Missions,MainLeagues) que ficam deep below the fold. - Casos resolvidos:
cassino-bet-brrow 2 da home (biggest-wins-today, above-the-fold), todas as 4 brands no row 3 do casino hub & casino live (biggest-wins-and-latest-bets, dentro deEAGER_CASINO_ROW_COUNT=4), e o aside desktop dovera-bet-br(HomeWinsRightColumn). - Slices
topWins/lastWinsda Zustand continuam populadas porhydrateHomepor backward compat — sem readers agora, safe pra cleanup em follow-up.
Brand Overrides
- fi-7k-bet (Finlândia): tradução completa pra finlandês das 17 entries de SEO page-descriptions (home, casino, casino.live, casino.category, casino.provider, casino.providers, blog, promotions, vip + 8 sub-páginas VIP, casino.category:todos-os-jogos), usando terminologia domain-aware (
kasino,verkkovedonlyönti,kolikkopelit,live-kasino,krash-pelit,talletus,nosto,pelaa vastuullisesti,VIP-ohjelma). Antes era cópia do EN — a brand já roda comBRAND_LANGUAGE=fi-fi, mas o copy above-the-footer ainda renderizava em inglês ("7K: Your trusted online sportsbook"). Recomendado um pass de revisão por nativo antes de qualquer copy regulada (responsible-gambling, KYC, payments) — o Finnish gambling regulation tem fraseologia específica que tradução automática não pega. Nomes de ligas nosidebar-sports.tspadronizados pra PT (Brasileirão Série A,Liga Europa,Copa do Brasil) — antes divergia dohome-leagues.tsem três casos. Removido o fallbacklabel: "Store"hardcoded no override de sidebar-buttons — agoraintent: "store"resolve pra "Kauppa" via i18n core. - 7k-bet-br: refactor dos 4 arquivos de analytics (
analytics.ts,appsflyer.ts,hotjar.ts,mixpanel.ts) pro patternisDev() ? DEV_* : PROD_*(consistente com o resto do arquivo). Webtrends desativado (webtrendsAccountId: undefinedem dev e prod) — token preservado comentado pra referência caso precise reativar. CSS anti-flicker do Webtrends removido (estava causando deadlock no initial page load). - 7k-bet-br + cassino-bet-br:
/twa.jsrestaurado empublic/twa.js+ registro<script src="/twa.js" defer>emapp/config/scripts/scripts.ts(parte do PR #786 — bridge AppsFlyer pro APK Android). Vera-bet-br já tinha sua variante e ficou intocada. - cl-bet7k-com, fi-7k-bet, ng-7k-bet: três brands ganharam override de
app/config/seo/hreflang.tscom o cluster recíproco da família 7k (auto-referência + outras 3 sister-domains, semx-default).
Ops
- Deploy configs (front-ops):
deploy.ymlatualizado pra mudar a default branch decl-bet7k-com,fi-7k-beteng-7k-betpramain-intere setarBRAND_LANGUAGEcorreto por env (es-cl,fi-fi,en-ngrespectivamente). Mesmas mudanças deBRAND_LANGUAGEaplicadas aos staging equivalentes (stagecl-bet7k-com,stagefi-7k-bet,stageng-7k-bet).stagecl-bet7k-comtambém trocou default branch deperformanceprastagepra alinhar com a strategy de branching atual. - Default branch do
prod-7k-bet-brajustada em deploy config separado.
Core / SDK
@cactus-agents/i18nbumped para^0.88.0— ship das novas keys:error_page.details_*(pt-br, pt, es, en) usadas pelo blocoErrorDetails,layout:sidebar.nomenclature_gamesconsumida pelouseCasinoNomenclaturerefatorado, elayout:sidebar-buttons.storeque destrava a remoção do fallback"Store"hardcoded nofi-7k-bet. Adicionar país novo agora é uma linha em cadalayout.jsondo core, sem mudança no front.@cactus-agents/api-clientbumped para^0.14.1.- Dependências do core estabilizadas (PR #774 + #775): vários
@cactus-agents/*saíram de tag beta pra stable empackage.jsondo front-web-base;@cactus-agents/i18ne@cactus-agents/paymentsbumpados pra stable em commit separado. - Suite de regressão no core (commit
68d3728mencionado no PR #779) com 11 cases pinned cobrindo o classifier real contra/api/wallet/refresh,/api/kyc/status,/api/user/login-historyetc. — garante que mexer noclassifyApiErrornão regrida a granularidade dos códigosEM*.
Documentação
docs-internal/endpoints/error-codes.md: nova seção "Envelope das rotas internas — preservar reason em 401/403" documentando a regra dura, o helperproxyErrorResponse/unauthorizedNoToken, as exceções legítimas (fluxos pre-auth: login, register, recovery, social, validate-document) e o procedimento pra adicionar rotas novas. Worked example baseado no caso realcassino.bet.br #EP0710-EM0005reportado em 05/2026.