Pular para o conteúdo principal

SEO

Tudo o que o template emite pra buscadores: meta e JSON-LD por rota, hreflang multi-pais, canonical, sitemaps e robots.txt.

O dominio tem 8 arquivos de config em app/config/seo/ — todos brand-overridable, todos importados como ~/config/seo/<arquivo>:

ArquivoExportServer-onlyPapel
seo.tsseoConfigKnobs de brand: verificacao, OG fallback, sameAs, contatos, licencas, canonical overrides
hreflang.tshreflangConfig<link rel="alternate" hreflang> das brands-irmas
sibling-route-paths-7k.tsbet7kBrRoutePaths, cl7kRoutePaths, fi7kRoutePaths, ng7kRoutePathsMapas de rota das brands-irmas 7k (usados pelo hreflang)
sitemap.tssitemapConfigGrupos extras de sitemap + overrides de priority
page-descriptions.server.tspageDescriptionssimMeta e descriptions das paginas nao-jogo
games-seo.server.tsgamesSeoConfigsimMeta de <head> por jogo curado
sports-seo.tssportsSeoConfigCopy de SEO em torno do sportsbook
branded-home-links.tsbrandedHomeLinksConfigLinks internos pra home com anchor de marca

Utils e services correlatos: app/utils/seo.ts, app/utils/meta.ts, app/utils/games-seo.server.ts, app/utils/sports-seo.ts, app/utils/sitemap.server.ts, app/services/sitemap-config.server.ts, app/services/sitemap-content.server.ts.


seoConfig — knobs de brand

Default no base e um objeto vazio: campo ausente = zero markup. Cada brand popula o que usa em overrides/<brand>/app/config/seo/seo.ts.

Todos os valores terminam no HTML publico — nao ha segredo aqui.

CampoEmite
googleSiteVerification<meta name="google-site-verification">
facebookDomainVerification<meta name="facebook-domain-verification">
pinterestSiteVerification<meta name="p:domain_verify">
yandexVerification<meta name="yandex-verification">
fallbackOgImageog:image quando a rota nao define a propria
socialLinksOrganization.sameAs[] no JSON-LD
contactPointsOrganization.contactPoint[] (contactType, telephone, email, areaServed, availableLanguage)
identifiersOrganization.identifier[] como PropertyValue
disambiguatingDescriptionOrganization.disambiguatingDescription
foundingDateOrganization.foundingDate (ISO 8601)
alternateNamealternateName no Organization / WebSite
searchUrlTemplateTemplate do SearchAction (deve conter {search_term_string})
websiteSchemaScope"global" (default) ou "homepage-only"
canonicalOverrides<link rel="canonical"> por path

Licencas regulatorias

identifiers conecta a brand ao registro regulatorio no Knowledge Graph. No Brasil, quando a brand e licenciada pela SPA-MF, inclua o numero da licenca com propertyID: "SPA-MF" — o sinal ajuda a diferenciar operador licenciado de site de mercado cinza:

identifiers: [
{ propertyID: "SPA-MF", value: "12345/2024", name: "Licença SPA-MF" },
{ propertyID: "CNPJ", value: "12.345.678/0001-00" },
];

websiteSchemaScope

  • "global" (default) — WebSite JSON-LD emitido no root layout, em toda pagina. Compativel com brands que nao opt-in.
  • "homepage-only" — pulado no root layout, emitido apenas pela home. Recomendado pela spec do Google: WebSite + SearchAction descrevem o site como um todo e devem aparecer uma vez, na home.

canonicalOverrides

Mapeia um pathname normalizado (sem trailing slash, sem query/hash — ver normalizeHreflangPath) pro path que o canonical deve apontar. resolveCanonicalHref() (app/utils/seo.ts) aplica.

canonicalOverrides: { "/games/category/all": "/" }

:::caution Use com parcimonia O default e canonical auto-referente (cada URL canonicaliza pra si). Este override existe pra consolidar sinal de ranking quando uma subpagina canibaliza uma pagina mais importante em queries de marca — o caso classico e uma pagina de categoria de alto trafego superando a home pelo nome da marca. A pagina segue indexada e servindo as queries dela; so o canonical e reatribuido.

Isso muda apenas a tag canonical. O og:url continua auto-referente, pra que o card social aponte pra pagina em que o usuario esta. :::


Hreflang

<link rel="alternate" hreflang="…" href="…"> injetado no <head> de todas as paginas (via _layout.tsx), sinalizando ao Google as variantes da brand em outros idiomas/regioes.

Default no base: array vazio → nenhuma tag emitida. Brands com sister-sites sobrescrevem overrides/<brand>/app/config/seo/hreflang.ts listando todos os pares, incluindo o proprio site e x-default.

HreflangEntry

export interface HreflangEntry {
/** `language[-REGION]` ou `x-default`. Ex: `pt-BR`, `es-CL`, `x-default`. */
hreflang: string;
/** Origin da variante — sem trailing slash, sem path, sem query. */
origin: string;
/** Mapa de rotas da brand-irma (mesmo shape do `~/config/routes/paths` dela). */
routePaths?: RoutePathMap;
/** Regex de paths que nao existem nesse locale — href cai na home da variante. */
fallbackToHome?: RegExp[];
}

O reverse-match de rota

Este e o mecanismo central e o menos obvio. Quando o entry traz routePaths, o _layout.tsx traduz o pathname atual pra URL equivalente no idioma da variante — /cassino/ao-vivo em BR vira /casino/en-vivo em CL. A traducao e feita por translatePathToSibling() (app/utils/seo.ts):

  1. Itera as rotas locais (routePaths) com matchPath pra descobrir a routeKey + params da URL atual.
  2. Re-resolve com o pattern da irma via generatePath.
  3. Se nenhuma rota local bate (conteudo dinamico fora do registry, por exemplo), retorna null — e o caller cai pro pathname local cru.

A ordem de tentativa e por especificidade, nao pela ordem do objeto:

  1. mais segmentos literais primeiro (sem :param nem *);
  2. em empate, patterns sem * antes dos com * (/sports antes de /sports/*);
  3. em empate final, mais segmentos totais.

Sem isso, /games/category/slots casaria casino.play (/games/:provider/:game — 1 literal, 2 params) em vez de casino.category (/games/category/:slug — 2 literais, 1 param), porque ambos tem 3 segmentos.

Precedencia no _layout.tsx

Pra cada entry, na ordem:

  1. fallbackToHome matcha o pathname? → href = origin + "/"
  2. o entry tem routePaths? → translatePathToSibling(), com fallback pro pathname local
  3. senao → concatena o pathname local cru

O caso 3 e o que garante a auto-referencia (o entry do proprio dominio nao deve declarar routePaths: o routePaths local ja produz o path correto).

fallbackToHome

Lista de regex pra paths que nao existem naquele locale. Quando o pathname atual matcha, o href emitido aponta pra home (/) da variante em vez de concatenar um path quebrado. E a "Opcao B" do parecer de SEO: manter hreflang valido apontando pra raiz da brand alternativa.

Entries de auto-referencia devem deixar undefined — o path sempre existe no proprio site.

Exemplo real (overrides/7k-bet-br/app/config/seo/hreflang.ts)

const BR_ONLY: RegExp[] = [/^\/blog(\/|$)/, /^\/promocoes(\/|$)/, /^\/legal(\/|$)/];

export const hreflangConfig: HreflangEntry[] = [
{ hreflang: "pt-BR", origin: "https://7k.bet.br" },
{
hreflang: "es-CL",
origin: "https://bet7k.cl",
routePaths: cl7kRoutePaths,
fallbackToHome: BR_ONLY,
},
// en-NG, fi-FI …
{ hreflang: "x-default", origin: "https://7k.bet.br" },
];

x-default aponta pro mercado-mae: usuarios em locales nao mapeados recebem a URL canonica em vez de uma variante geo aleatoria.

sibling-route-paths-7k.ts — e por que ele vive no base

Os mapas de rota das quatro brands 7k irmas ficam no base, nao em cada override:

:::warning Por que nao importar o routePaths da irma direto O brandOverridesPlugin e file-replacement: um override de ~/config/routes/paths resolve so pro build da propria brand. Um build de 7k-bet-br nao consegue importar o ~/config/routes/paths de cl-bet7k-com. A saida e espelhar os mapas a mao aqui, uma vez, e deixar cada override 7k importar as irmas de que precisa.

Manter em sincronia com overrides/<brand>/app/config/routes/paths.ts. O quality gate do pre-push nao pega drift entre este arquivo e os overrides das brands — o unico sintoma e uma URL de hreflang errada em producao. A ordem dos segmentos no arquivo segue a das fontes justamente pra tornar o diff linha-a-linha trivial. :::


Descriptions por pagina

page-descriptions.server.ts

Meta e texto visivel das paginas nao-jogo. Server-only.

Estrutura em duas camadas:

  • defaults — por routeKey, aplicado quando nao ha override especifico
  • pages — override por routeKey:slug (rotas dinamicas) ou por routeKey (rotas estaticas). Campos omitidos herdam do default correspondente
CampoUsoFormato
meta_title<title> e og:titletexto puro
meta_description<meta description> e JSON-LDtexto puro
front_descriptionVisivel na pagina (expansivel)HTML

Template tags resolvidas em runtime: {brand_name}, {category_name}, {provider_name}.

Override: overrides/<brand>/app/config/seo/page-descriptions.server.ts — hoje todas as brands do base tem o proprio.

games-seo.server.ts

Overrides de <head> por jogo curado. Complementa dois vizinhos, e a divisao importa:

ArquivoCobre
app/config/content/game-details.server.tsConteudo rico de corpo (ver Games)
app/config/content/faq.server.tsQ&A + FAQPage JSON-LD
app/config/seo/games-seo.server.tsSomente os knobs de meta

GameSeoEntry (keyed pelo slug completo "provider/game"):

CampoEfeito
titleSobrescreve <title> e og:title
descriptionSobrescreve <meta description> e og:description
alternateNamealternateName no Schema.org
ogTitle / ogDescription / ogImageOG dedicado (cai em title/description)
h1H1 visivel na pagina do jogo (cai no nome do jogo)
h3Subtitulo entre o H1 e o iframe
ratingValue / ratingCountAggregateRating no JSON-LD de VideoGame
emitJsonLdGameGraphEmite um grafo Game completo ao lado do VideoGame base

O base envia copy templatizada pros 10 jogos mais populares no BR. {brand_name} e resolvido em request time por getGameSeo(slug, brandName) (app/utils/games-seo.server.ts) — a copy do base fica generica e qualquer brand sem override ainda recebe meta correta pra marca dela.

sports-seo.ts

Copy de SEO renderizada em torno da pagina de sportsbook, pra crawlers. Os blocos HTML sao serializados numa unica string por getSportsSeoHtml() e renderizados pelo componente compartilhado PageDescription (colapsado por default com "Ver mais", igual cassino/promocoes/VIP). resolveSportsSeoMeta(pathname, brandName) resolve o meta por rota.

:::note Quando o conteudo nao renderiza Mesmo populado, o bloco fica escondido quando a brand usa o sportsbook First com o footer delegado ao iframe. O gate esta em app/routes/sports/_layout.tsx (shouldRenderSeoContent). Ver Sports. :::

branded-home-links.ts

Links internos apontando pra home com anchor de marca, renderizados no HTML de servidor de paginas hub de alto trafego.

Racional (plano de recuperacao de query de marca): a home pode perder a posicao no SERP do proprio nome da marca pra uma subpagina de alto trafego. Links internos daquelas subpaginas pra /, com anchors contendo o nome da marca, reforcam que a home e o destino relevante pra queries de marca.

:::warning Tem que estar no HTML de servidor Um link client-only e invisivel pro Googlebot. :::

export interface BrandedHomeLinkEntry {
/** Anchor visivel — deve conter o nome da marca. */
anchor: string;
/** Frase envolvente opcional com placeholder `{link}`. */
sentence?: string;
}

export type BrandedHomeLinksConfig = Record<string, BrandedHomeLinkEntry>;

A chave e um intent key estavel, usado na pagina via <BrandedHomeLink intent="…">. Chave desconhecida ou ausente nao renderiza nada. Default no base: {} — nenhuma brand opt-in sem override.


Sitemaps

Existem 9 arquivos de rota de sitemap em app/routes/ — cada um um handler proprio:

RotaArquivoConteudo
/sitemap.xmlsitemap[.]xml.tsIndex — aponta pros filhos
/sitemap-static.xmlsitemap-static[.]xml.tsPaginas estaticas do registry
/sitemap-games/categories.xmlsitemap-games.categories[.]xml.tsCategorias de jogo
/sitemap-games/providers.xmlsitemap-games.providers[.]xml.tsProviders
/sitemap-games/:provider.xmlsitemap-games.$provider[.]xml.tsJogos de um provider (com sharding)
/sitemap/:area/:filenamesitemap-content.$area.$filename.tsConteudo dinamico (blog, promotions, news, faq, help, responsibleGaming)
/sitemap-faq.xmlsitemap-faq[.]xml.tsFAQ
/sitemap-profiles.xmlsitemap-profiles[.]xml.tsPerfis oficiais
sitemap-brand-group[.]xml.ts(handler compartilhado)Grupos declarados pela brand

:::info sitemap-legal.xml foi removido Removido em 2026-07: paginas legais sao noindex por decisao de produto, e anunciar URL noindexada num sitemap e contraditorio. :::

Slugs reservados

/sitemap-games/{categories,providers}.xml sao registrados antes da rota dinamica /sitemap-games/:provider.xml. A ordenacao por especificidade do React Router ja resolveria, mas a ordem explicita documenta que categories e providers sao slugs reservados que um provider real nunca pode reivindicar. Se um provider com um desses slugs aparecer no catalogo, a rota dinamica nunca o ve e aqueles jogos saem do SEO.

Formatos do sitemap de conteudo

Uma rota (/sitemap/:area/:filename) cataloga todos os formatos:

/sitemap/blog/dicas.xml → posts da categoria "dicas" (shard 1)
/sitemap/blog/dicas-2.xml → shard 2 (>= 500 posts)
/sitemap/blog/categorias.xml → URLs das paginas de categoria
/sitemap/blog/post.xml → posts sem categoria (canonical = null)

:param no RR7 so funciona depois de /, entao o primeiro segmento (/sitemap) e literal e :area + :filename sao os dois params.

Grupos por brand — sitemapConfig

export const sitemapConfig: SitemapConfig = {
groups: [], // default: nenhum extra
};
CampoDescricao
groups: SitemapGroupDef[]Grupos extras. Cada path vira um XML separado
routeOverrides?: { rules }Override de priority/changeFreq nas rotas shared existentes

SitemapGroupDef:

CampoDescricao
path"sitemap-blog.xml" (dash-prefixed) ou "sitemap/promocoes/promocoes.xml" (nested)
source{ kind: "static-list", entries } ou { kind: "wp-content", area, categorySlug? }
rules?SitemapPriorityRule[] — a primeira que match aplica; ponha as especificas primeiro
defaultRule?Aplicado quando nenhuma rule match

As areas aceitas pelo wp-content: blog, promotions, faq, help, news, responsibleGaming.

SitemapPriorityRule.pattern casa contra o path relativo e aceita exact match, prefixo com wildcard (/blog/*), wildcard de segmento (/cassino/categoria/*) e glob com multiplos coringas.

:::warning Cada path de grupo precisa de um route(...) explicito O RR7 nao aceita :param depois de - (so depois de /), entao cada path dash-prefixed e registrado individualmente em app/router/routes.ts — hoje sao ~29 registros apontando pro mesmo handler (sitemap-brand-group[.]xml.ts), que le o pathname do request e procura o group correspondente na config da brand. 404 quando a brand nao declara o group.

Nenhuma brand do base declara app/config/seo/sitemap.ts hoje, entao todos esses paths respondem 404 no base. Pra adicionar um grupo novo: declare em sitemapConfig.groups e adicione o route(...) correspondente. :::

routeOverrides cobre as rotas shared (sitemap-static.xml, sitemap-games/*, sitemap-faq.xml, sitemap/{area}/{filename}): cada rota gera as URLs como sempre e applyRouteOverrides() (app/services/sitemap-config.server.ts) faz lookup do loc nas rules. Sem opt-in, o comportamento default e preservado.

Helpers

app/utils/sitemap.server.ts concentra a mecanica: renderUrlset(), renderSitemapIndex(), sitemapResponse(), sitemapNotFoundResponse(), publicOrigin(), resolveSitemapOrigin(), formatLastmod(), dayRoundedNow(), resolveImageDeliveryUrl(), mais o sharding (SITEMAP_GAMES_SHARD_THRESHOLD, hashSlug(), computeShardCount(), shardGames(), groupGamesByProvider()).


robots.txt

app/routes/robots[.]txt.ts tem comportamento duplo, decidido pelo host da request.

Host canonico

PUBLIC_DOMAIN quando setado, senao ORIGIN_DOMAIN. PUBLIC_DOMAIN cobre ambientes em que o dominio servido ao publico difere do ORIGIN_DOMAIN (o caso bet7k.cl: publico em bet7k.cl, plataforma/BFF e override de brand keyed em cl.bet7k.com, que hoje e so um 301 pro dominio publico).

:::caution PUBLIC_DOMAIN substitui o canonico, nao adiciona um segundo Um hit que chega pelo ORIGIN_DOMAIN nesses ambientes cai no ramo non-prod — o que e o comportamento seguro pra um dominio que redireciona. :::

Prod (host da request === host canonico)

Allow: / + a lista de disallow + ponteiro pro sitemap index no mesmo host. Cache public, max-age=86400.

Paths bloqueados e por que:

PathMotivo
/api/Endpoints server-only, sem conteudo pro usuario
/debugPaineis de debug in-app
/user/Area autenticada
/signupFormulario de cadastro, sem valor organico
/recoverRecuperacao de senha
/__/Internals do framework (resource routes)
/game-errorSuperficie de erro de launch, so pra iframe
/redirectRota de redirect
routePaths.search, routePaths["search.casino"], routePaths["search.sports"]Busca interna — paginas finas, explicitamente citadas como conteudo de baixa qualidade pelas Search Quality Guidelines

Os tres paths de busca sao resolvidos via routePaths, entao overrides de brand (/buscar, por exemplo) sao respeitados automaticamente.

Non-prod (stage, preview, dev, ou qualquer mismatch)

Disallow: / e header X-Robots-Tag: noindex, nofollow, cache 300s. Sem linha Sitemap: — se um crawler ignorar o Disallow, ao menos nao entregamos o sitemap de bandeja.

Em dev local nao ha o que ajustar: o dev server roda em localhost enquanto o .dev.vars carrega o dominio real da brand, entao localhost cai automaticamente no ramo non-prod — que e o comportamento desejado.

:::note Scheme forcado publicOrigin() forca https em host nao-local. Uma zona com SSL "Flexible" (caso bet7k.cl) entrega request.url com scheme http pro worker, e a linha Sitemap: saia com http:// em producao. :::


Utils de meta

UtilPapel
buildMeta({ title, description, url, image, type })Conjunto padrao de descriptors de SEO
composeFullTitle(title, brandName)Sufixa - ${brandName} quando o titulo ainda nao menciona a marca; titulo vazio cai pra marca sozinha
normalizeHreflangPath(pathname)Normaliza pathname pra canonical e hreflang
resolveCanonicalHref(url)Aplica seoConfig.canonicalOverrides
translatePathToSibling(pathname, siblingRoutePaths)Reverse-match de rota pro hreflang
buildBreadcrumbsMeta() / buildBreadcrumbJsonLd() / homeCrumb()Breadcrumbs
buildOrganizationJsonLd() / buildWebSiteJsonLd()JSON-LD de entidade
getOrigin(matches) / getBrandName(matches)Leitura do loaderData do layout

app/utils/meta.ts faz o merge com o meta do layout pai. Os hreflang alternates sao chaveados por (rel, hrefLang) de proposito: sem isso, um <link rel="alternate"> emitido por uma rota filha apagaria a lista inteira de hreflangs do layout.

:::warning composeFullTitle precisa do guard de titulo vazio Sem ele, um seoTitle que resolve null/"" (config de SEO sem entry pra rota, sessao restaurada sem os campos) comporia um sufixo orfao — " - 7K". :::


Override por brand

Todo arquivo de app/config/seo/ e brand-overridable em overrides/<brand-key>/app/config/seo/<arquivo>.ts, com a semantica padrao de substituicao de arquivo inteiro (sem deep-merge). Ver Override Files.

Cobertura atual (consulte overrides/*/app/config/seo/ pra estado corrente): page-descriptions.server.ts em todas as brands; hreflang.ts na familia 7k (7k-bet-br, cl-bet7k-com, fi-7k-bet, ng-7k-bet); games-seo.server.ts, seo.ts e sports-seo.ts em subconjuntos.