Embeds e white-label
Qualquer jornal, ONG ou microsite governamental pode exibir um dado real e citado do Futuros, o valor mais um chip de fonte com deep link para a fonte primária, com um único <iframe>. O contrato de honestidade viaja com o embed: um embed nunca mostra um número sem sua procedência.
Três rotas de widget mais um builder:
| Rota | Renderiza |
|---|---|
/embed/stat | Uma estatística citada — valor grande + unidade, chip de delta com sinal, chip de fonte, marca |
/embed/chart | O mesmo indicador como um pequeno gráfico de linha (último ponto rotulado) |
/embed/board | Uma grade responsiva com vários quadros stat/chart — um dashboard inteiro em um iframe |
/incrustar | A UI do builder em ES — escolha as opções, veja a prévia ao vivo, copie o snippet |
Todas as rotas de embed são embutíveis cross-origin: o vercel.json serve /embed/(.*) com a mesma Content-Security-Policy aplicada ao resto do app (default-src 'self', script-src e connect-src restritos, object-src 'none', base-uri 'self', form-action 'self'), relaxando apenas frame-ancestors para *. O resto do site mantém frame-ancestors 'self' mais X-Frame-Options: SAMEORIGIN. O chrome do app (navegação, onboarding) é desativado sob /embed, então o widget pinta apenas a si mesmo.
/embed/stat e /embed/chart
Parâmetros de query compartilhados:
| Parâmetro | Valores | Padrão | Notas |
|---|---|---|---|
param | slug do pilar (salud, educacion, …) | salud | qual pilar |
iso | ISO3 (MEX, BRA, …) ou LATAM | MEX | convertido para maiúsculas automaticamente; LATAM renderiza o agregado regional ("América Latina y el Caribe") |
indicator | id do indicador | primeiro indicador da célula | escolha uma métrica específica |
theme | light | dark | dark | |
lang | es | en | pt | es | strings trilíngues completas + rótulos/unidades de métricas localizados |
accent | cor hex #RGB–#RRGGBBAA | azul Oxford (#11457e claro / #6496d5 escuro) | acento white-label (deve casar com /^#[0-9a-fA-F]{3,8}$/); sobrescreve tanto o acento quanto a cor de delta positivo |
O valor, o delta (vs. a média de 3 anos), a unidade e o chip de fonte vêm do mesmo payload estático de célula que o app usa; o chip de fonte aponta para a fonte primária do indicador e a marca aponta de volta para a célula viva do atlas.
Quatro mecânicas por trás desse parágrafo (todas em src/routes/_embed-shared.tsx):
- Gate somente-estático.
fetchParameter()carrega a célula gerada no build/data/parameter-cache/<param>__<ISO>.jsone reportasource: "static" | "mock"maisstatus: ready | not_generated | quarantined. Em um cache miss, o app principal pinta um mock determinístico para manter o layout vivo; o embed trata qualquer coisa que não sejasource:"static": ou que sejanot_generated: como sem dados, porque um valor mock dentro de um iframe de terceiro seria um número fabricado vestindo um chip de fonte real. - Ordem de resolução do chip de fonte. Três passos, o primeiro acerto vence: campos inline no indicador (
source,source_url,vintage_year) → a linhacitations[]do próprio payload casada porcitation_id→ o registro de fontes (inferSourceKey(citation_id)→ metadados da instituição → template de URLdeepLinkFor()). O passo do registro é a mesma maquinaria desyntheticCitation(): o chip pode ser reconstruído apenas a partir do id, e somente um id que não mapeia para nenhuma instituição conhecida não produz nada. - Rótulos. Os payloads gerados no build carregam apenas rótulos/unidades canônicos em ES;
?lang=en|ptos sobrepõe a partir do registro de métricas gerado no build, carregado de forma assíncrona e cacheado em nível de módulo. O texto ES pinta imediatamente e permanece se esse fetch nunca resolver, e o embed honra?lang=exatamente, ao contrário do app principal, nunca recai na preferência de idioma armazenada do visitante. - A cor do delta é sensível à direção. O chip colore pelo
good_directiondo indicador (up/down/neutral): um valor em queda de uma métrica onde cair é bom renderiza como bom;neutralrenderiza cinza.
/embed/chart exige adicionalmente uma series não vazia no indicador: um valor citado sem histórico renderiza o estado sem-dados, nunca um gráfico vazio. O último ponto é marcado com um ponto de referência e repetido como valor de destaque.
Embed mínimo de estatística:
<iframe
src="https://futuros.xyz/embed/stat?param=salud&iso=MEX&theme=light&lang=pt&et=ft_embed_SEU_TOKEN"
width="320" height="180" style="border:0" loading="lazy"
title="Futuros — Saúde, México"></iframe>Embed de gráfico white-label (acento customizado, português, indicador específico):
<iframe
src="https://futuros.xyz/embed/chart?param=educacion&iso=BRA&indicator=se_xpd_totl_gd_zs&theme=dark&lang=pt&accent=%23004b87&et=ft_embed_SEU_TOKEN"
width="360" height="240" style="border:0" loading="lazy"
title="Futuros — Gasto em educação, Brasil"></iframe>Note o # codificado na URL (%23). Os tamanhos sugeridos seguem o builder /incrustar: stat 320×180, chart 360×240.
/embed/board — dashboard multi-quadro
Um único iframe renderiza uma grade responsiva e tematizada de quadros stat/chart. Um parceiro (BID / CAF / CELAC) insere um único iframe e recebe um dashboard inteiro, white-label, sem mudança de código.
Parâmetros:
| Parâmetro | Valores | Padrão | Notas |
|---|---|---|---|
panels | kind:param:iso[:indicator] separados por vírgula | — | kind = stat | chart; até 12 quadros |
theme | light | dark | dark | tematiza o shell e cada quadro |
accent | cor hex | — | repassado a cada quadro |
brand | texto livre | Futuros | substitui a marca; com um brand customizado a atribuição no rodapé lê "powered by Futuros ↗", caso contrário lê "futuros.xyz ↗" |
lang | es | en | pt | es | encaminhado a cada quadro |
Cada spec de quadro é kind:param:iso[:indicator]: p. ex. stat:salud:MEX ou chart:educacion:BRA:se_xpd_totl_gd_zs. O board monta cada quadro como um iframe same-origin de /embed/stat ou /embed/chart, encaminhando theme, lang e accent.
O parsing degrada campo a campo, nunca o board inteiro: um kind desconhecido vira stat, um param vazio vira salud, um iso vazio vira MEX (sempre em maiúsculas), e qualquer coisa além de 12 quadros é truncada silenciosamente. Os quadros renderizam como iframes de altura fixa (stat 160 px, chart 220 px) em uma grade auto-fill com coluna mínima de 260 px, então o board reflui com a largura da página hospedeira.
Um dashboard de país white-label:
<iframe
src="https://futuros.xyz/embed/board?panels=stat:salud:MEX,chart:educacion:MEX,stat:seguridad:MEX,chart:trabajo-economia:MEX&theme=light&accent=%23004b87&brand=IDB&lang=pt&et=ft_embed_SEU_TOKEN"
width="100%" height="640" style="border:0" loading="lazy"
title="Dashboard Futuros — México"></iframe>Se panels estiver vazio, o board mostra uma dica (?panels=stat:salud:MEX,chart:educacion:BRA) em vez de um frame em branco.
Tokens de embed
Desde a release de contas, o futuros.xyz exige sessão em toda rota do app, e um iframe em um site de terceiros não pode carregar esse cookie. Um token de embed o substitui:
- Crie o token em
/cuenta/embeds: um rótulo, as rotas permitidas (/embed/stat,/embed/chart,/embed/board; o board inclui stat e chart porque os monta como filhos) e a validade (padrão 365 dias, máximo 730). Mostrado uma única vez; formatoft_embed_<43 caracteres>. - Acrescente
&et=<token>à URL do iframe (o builder/incrustarfaz isso por você se colar o token). O widget reenvia o mesmo token como cabeçalhoX-Embed-Tokennas leituras de dados, e o board o propaga a cada painel. - Revogue na mesma página. O edge guarda o veredito positivo por 60 s, então um iframe revogado deixa de carregar em menos de um minuto. Um token expirado, revogado, desconhecido ou de uma rota fora do seu escopo responde
401com uma linha de texto que aponta para/cuenta/embeds.
O token abre apenas as rotas /embed/* do seu escopo e os arquivos de dados que esses widgets leem (/data/parameter-cache/<parâmetro>__<país>.json e /data/metrics/registry.json), não o resto do corpus.
| Situação | Resposta do edge |
|---|---|
et válido em uma rota dentro do escopo | o widget, como antes |
Sem et | 401 (texto) |
et de outra rota (p. ex. um token só-stat em /embed/board) | 401 |
| Arquivo de dados fora dos dois acima | 401 |
| Verificador indisponível | 503 + Retry-After: 30 |
O token identifica a conta que o criou; cada criação e revogação fica no registro de auditoria, e os tokens de uma conta excluída deixam de valer. Só o hash é armazenado (SHA-256 com pimenta): perdê-lo obriga a criar outro. A CSP de /embed/* não muda: continua com frame-ancestors *.
/incrustar — o builder
https://futuros.xyz/incrustar?lang=pt é o builder sem código para os embeds de widget único. Escolha pilar, país, indicador, tipo (stat/chart) e tema; ele mostra uma prévia ao vivo em iframe do valor citado real e entrega um snippet de uma linha para copiar e colar. O dropdown de indicadores é populado com os indicadores reais da célula selecionada, então você só embute uma métrica que existe.
Tematização — semântica de override de themeTokens
Cada widget aplica inline um conjunto completo de variáveis CSS na própria raiz (themeTokens() em _embed-shared.tsx), não há classe de tema global, então os estilos da página hospedeira não vazam para dentro e o widget não vaza para fora:
| Token | Papel | ?accent= sobrescreve? |
|---|---|---|
--e-bg / --e-panel / --e-line | Fundo, cartão, borda | Não |
--e-ink / --e-ink-2 / --e-ink-3 | Hierarquia de texto | Não |
--e-accent | Marca, traço da linha do gráfico | Sim |
--e-good | Delta na direção boa | Sim — o mesmo hex do acento |
--e-bad | Delta na direção ruim, ressalva de quarentena | Não — permanece vermelho |
--e-cite | Chip de citação | Não |
O override é deliberadamente parcial: accent substitui exatamente --e-accent e --e-good, nunca --e-bad ou --e-cite. Um parceiro pode aplicar sua marca ao widget e aos deltas positivos; a cor de aviso e o chip de citação não são personalizáveis, você pode fazer white-label do número, não da ressalva.
Garantias de honestidade
O contrato de procedência tem dentes dentro do próprio widget:
- Nenhum número fabricado. Um embed se recusa a renderizar payloads mock/de fallback, se a fonte de dados da célula não for o corpus estático, ele mostra "Dados não disponíveis" em vez de um valor inventado.
- A quarentena é visível. Uma célula sinalizada pelos validadores renderiza com uma ressalva explícita de "em revisão" em vez de apresentar silenciosamente um número posto em dúvida.
- A citação viaja junto. O chip de fonte é reconstruído apenas a partir do
citation_id(veja Procedência), então o deep link funciona mesmo que o embed nunca carregue o registro completo de citações.
Framing e segurança
- As rotas de embed enviam a mesma CSP completa do app, com
frame-ancestors *como única diferença:default-src 'self',script-srceconnect-srclimitados a self mais as origens de PostHog, jsDelivr e Google Fonts,object-src 'none',base-uri 'self',form-action 'self'. Nada muda para quem embute (o iframe continua funcionando em qualquer página hospedeira), mas a superfície de embed não roda mais sem restrições de conteúdo. - Todas as outras rotas enviam
frame-ancestors 'self'eX-Frame-Options: SAMEORIGIN, então o app principal não é embutível fora da origem. - Embeds exigem um token de embed (
?et=ou o cabeçalhoX-Embed-Token); ver Tokens de embed. Continuam sendo GETs de dados estáticos: não há nada a submeter. - Os widgets são autocontidos: o tema é aplicado via overrides inline de variáveis CSS na raiz do embed (não uma classe global), então o mesmo número lê corretamente dentro de qualquer página hospedeira.
Os valores por trás de cada embed são as mesmas observações citadas expostas pela API Pública v1: o embed é apenas uma vista renderizada e linkável de uma célula.