Módulo: Portal (portal)¶
Portal web para busca e navegação de notícias governamentais.
Repositório: github.com/destaquesgovbr/portal
URL Produção: destaquesgovbr-portal-klvx64dufq-rj.a.run.app (URL provisória - Cloud Run/GCP)
Visão Geral¶
O portal é a interface principal do DestaquesGovbr, oferecendo:
- Busca full-text com Typesense
- Filtros por órgão, tema e data
- Navegação por temas e órgãos
- Páginas individuais de notícias
- Feeds RSS/Atom/JSON com filtros dinâmicos
flowchart LR
U[Usuário] --> P[Portal Next.js]
P --> T[(Typesense)]
T --> HF[(HuggingFace)]
P --> CF[Configurações YAML]
P --> F[Feeds API<br/>RSS/Atom/JSON]
F --> T
Stack Tecnológico¶
| Tecnologia | Versão | Uso |
|---|---|---|
| Next.js | 15 | Framework React (App Router) |
| TypeScript | 5 | Tipagem estática |
| Typesense | - | Busca full-text |
| shadcn/ui | - | Componentes UI |
| Tailwind CSS | 3 | Estilização |
| React Query | 5 | Data fetching |
| Biome | - | Lint + Format |
Acesso a dados via GraphQL (migração R1)¶
Além da busca de notícias (Typesense), o portal oferece funcionalidades dinâmicas
— clippings, marketplace, notificações push, widgets e o agente
de IA. Na migração R1 (2026) esse acesso a dados deixou de usar rotas REST
internas + Firebase Admin direto e passou a consumir a fachada
graphql-api:
- Cliente urql com auth-exchange que envia o
Authorization: Bearer <JWT>(token do Keycloak exposto na sessão NextAuth). - Roteamento por feature flag (GrowthBook
graphql.*): cada feature alterna entre a implementação GraphQL e o fallback REST legado, removível no cleanup. - O CSP do portal precisa incluir a origin do graphql-api em
connect-src(senão o browser bloqueia o fetch) — foi a causa-raiz nº 1 da migração. - O agente transmite via SSE em
/graphql/stream(não/graphql).
Gate de validação: E2E no browser, nunca só curl
A validação da migração é feita com Playwright dirigindo o browser real
contra portal + graphql-api (portal/e2e/graphql/). curl headless mascara
o CSP e o código TypeScript do cliente — foi assim que um drift de schema
passou despercebido. Ver ADR-002.
Estrutura do Repositório¶
portal/
├── src/
│ ├── app/ # App Router (Next.js 15)
│ │ ├── page.tsx # Homepage
│ │ ├── layout.tsx # Layout principal
│ │ ├── globals.css # Estilos globais
│ │ ├── temas/
│ │ │ └── [themeLabel]/ # Páginas por tema
│ │ │ └── page.tsx
│ │ ├── orgaos/
│ │ │ └── [agencyKey]/ # Páginas por órgão
│ │ │ └── page.tsx
│ │ ├── noticias/
│ │ │ └── [id]/ # Página de notícia
│ │ │ └── page.tsx
│ │ ├── feeds/
│ │ │ └── page.tsx # Página de descoberta de feeds
│ │ ├── feed.xml/route.ts # RSS 2.0 API route
│ │ ├── feed.atom/route.ts # Atom 1.0 API route
│ │ ├── feed.json/route.ts # JSON Feed 1.1 API route
│ │ └── api/ # API Routes
│ ├── components/
│ │ ├── ui/ # Componentes shadcn/ui
│ │ ├── search/ # Busca e filtros
│ │ ├── news/ # Cards e listas de notícias
│ │ ├── layout/ # Header, Footer, Nav
│ │ ├── filters/ # Filtros de busca
│ │ └── common/
│ │ └── FeedLink.tsx # Link RSS contextual
│ ├── lib/
│ │ ├── typesense-client.ts # Cliente Typesense
│ │ ├── feed.ts # Lógica core de feeds (parsing, validação, serialização)
│ │ ├── feed-handler.ts # Handler HTTP compartilhado (ETag, cache)
│ │ ├── markdown-to-html.ts # Conversor markdown → HTML para feeds
│ │ ├── themes.yaml # Árvore temática
│ │ ├── agencies.yaml # Catálogo de órgãos
│ │ ├── prioritization.yaml # Config de priorização
│ │ └── utils.ts # Utilitários
│ ├── hooks/ # React hooks customizados
│ └── types/ # Definições TypeScript
├── public/ # Assets estáticos
├── docs/
│ ├── FEEDS_API.md # Documentação completa da API de feeds
│ └── FEEDS_ARCHITECTURE.md # Arquitetura técnica dos feeds
├── .github/workflows/
│ └── deploy-production.yml # Deploy Cloud Run
├── package.json
├── tailwind.config.ts
├── next.config.ts
├── tsconfig.json
└── Dockerfile
Páginas Principais¶
Homepage (/)¶
- Lista de notícias mais recentes
- Busca com autocomplete
- Filtros rápidos por tema
- Notícias priorizadas (destaques)
Página de Tema (/temas/[themeLabel])¶
- Notícias filtradas por tema
- Breadcrumb de navegação
- Subtemas disponíveis
Página de Órgão (/orgaos/[agencyKey])¶
- Notícias do órgão específico
- Informações do órgão
- Órgãos relacionados (hierarquia)
Página de Notícia (/noticias/[id])¶
- Conteúdo completo
- Metadados (data, órgão, tema)
- Notícias relacionadas
Página de Feeds (/feeds)¶
- Construtor interativo: Multi-select de órgãos e temas, gera URLs dos 3 formatos (RSS/Atom/JSON) em tempo real
- Feeds por Ministério: Grid com links diretos RSS/Atom para cada ministério
- Feeds por Tema: Grid com links diretos RSS/Atom para cada tema de nível 1
- Instruções: Guia passo a passo para usar os feeds em leitores RSS
Feeds API¶
/feed.xml: RSS 2.0/feed.atom: Atom 1.0/feed.json: JSON Feed 1.1
Parâmetros suportados:
- agencias - Filtrar por órgãos (ex: agencias=mre,saude)
- temas - Filtrar por temas (ex: temas=01,03)
- tag - Filtrar por tag exata
- q - Busca textual em título e conteúdo
- limit - Quantidade de itens (default: 20, máx: 50)
→ Documentação completa em portal/docs/FEEDS_API.md
Componentes Principais¶
SearchBar¶
// Barra de busca com autocomplete
<SearchBar
placeholder="Buscar notícias..."
onSearch={(query) => handleSearch(query)}
suggestions={suggestions}
/>
NewsCard¶
// Card de notícia
<NewsCard
title={news.title}
summary={news.summary}
agency={news.agency}
publishedAt={news.published_at}
theme={news.theme_1_level_1_label}
imageUrl={news.image}
href={`/noticias/${news.unique_id}`}
/>
FilterPanel¶
// Painel de filtros
<FilterPanel
agencies={agencies}
themes={themes}
selectedAgencies={selected.agencies}
selectedThemes={selected.themes}
dateRange={dateRange}
onFilterChange={handleFilterChange}
/>
Cliente Typesense¶
Configuração (typesense-client.ts)¶
import Typesense from "typesense";
const client = new Typesense.Client({
nodes: [
{
host: process.env.TYPESENSE_HOST,
port: Number(process.env.TYPESENSE_PORT),
protocol: process.env.TYPESENSE_PROTOCOL,
},
],
apiKey: process.env.TYPESENSE_API_KEY,
connectionTimeoutSeconds: 2,
});
Função de Busca¶
interface SearchParams {
query: string;
filters?: {
agency?: string[];
theme_1_level_1_code?: string[];
dateFrom?: number;
dateTo?: number;
};
page?: number;
perPage?: number;
sortBy?: string;
}
async function searchNews(params: SearchParams): Promise<SearchResult> {
const { query, filters, page = 1, perPage = 20 } = params;
// Construir filter_by
const filterClauses: string[] = [];
if (filters?.agency?.length) {
filterClauses.push(`agency:[${filters.agency.join(",")}]`);
}
if (filters?.theme_1_level_1_code?.length) {
filterClauses.push(
`theme_1_level_1_code:[${filters.theme_1_level_1_code.join(",")}]`
);
}
if (filters?.dateFrom) {
filterClauses.push(`published_at:>=${filters.dateFrom}`);
}
const result = await client
.collections("news")
.documents()
.search({
q: query || "*",
query_by: "title,content,summary",
filter_by: filterClauses.join(" && ") || undefined,
sort_by: "published_at:desc",
page,
per_page: perPage,
highlight_fields: "title,summary",
});
return result;
}
Schema do Documento Typesense¶
interface NewsDocument {
id: string; // unique_id
unique_id: string;
agency: string; // ex: "gestao"
title: string;
url: string;
image?: string;
content: string; // Markdown
published_at: number; // Unix timestamp
category?: string;
tags?: string[];
// Campos enriquecidos
theme_1_level_1_code?: string; // ex: "01"
theme_1_level_1_label?: string; // ex: "Economia e Finanças"
theme_1_level_2_code?: string;
theme_1_level_2_label?: string;
theme_1_level_3_code?: string;
theme_1_level_3_label?: string;
most_specific_theme_code?: string;
most_specific_theme_label?: string;
summary?: string; // Resumo AI
}
Arquivos de Configuração¶
themes.yaml - Árvore Temática¶
themes:
- label: Economia e Finanças
code: "01"
children:
- label: Política Econômica
code: "01.01"
children:
- label: Política Fiscal
code: "01.01.01"
- label: Autonomia Econômica
code: "01.01.02"
agencies.yaml - Catálogo de Órgãos¶
sources:
gestao:
name: Ministério da Gestão e da Inovação em Serviços Públicos
parent: presidencia
type: Ministério
inpe:
name: Instituto Nacional de Pesquisas Espaciais
parent: mcti
type: Instituto
prioritization.yaml - Priorização¶
# Órgãos com notícias priorizadas no topo
priority_agencies:
- presidencia
- gestao
- fazenda
- saude
# Temas em destaque
priority_themes:
- "01" # Economia
- "03" # Saúde
- "20" # Políticas Públicas
Server Components vs Client Components¶
Server Components (padrão)¶
// app/temas/[themeLabel]/page.tsx
export default async function ThemePage({ params }: Props) {
// Busca no servidor
const news = await searchNews({
query: "*",
filters: { theme_1_level_1_label: [params.themeLabel] },
});
return <NewsList news={news.hits} />;
}
Client Components¶
"use client";
// components/search/SearchBar.tsx
export function SearchBar() {
const [query, setQuery] = useState("");
// Interatividade no cliente
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
Variáveis de Ambiente¶
# .env.local (desenvolvimento)
TYPESENSE_HOST=localhost
TYPESENSE_PORT=8108
TYPESENSE_PROTOCOL=http
TYPESENSE_API_KEY=xyz
TYPESENSE_COLLECTION_NAME=news
# Produção (via Secret Manager)
TYPESENSE_HOST=<ip-interno>
TYPESENSE_PORT=8108
TYPESENSE_PROTOCOL=http
TYPESENSE_API_KEY=<api-key-producao>
Comandos de Desenvolvimento¶
# Instalar dependências
pnpm install
# Desenvolvimento
pnpm dev
# Build de produção
pnpm build
# Rodar build local
pnpm start
# Lint
pnpm lint
# Formatar
pnpm format
# Type check
pnpm type-check
# ou
pnpm exec tsc --noEmit
Componentes shadcn/ui¶
Adicionar componente¶
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add input
npx shadcn@latest add select
Usar componente¶
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
export function Example() {
return (
<Card>
<CardHeader>
<CardTitle>Buscar</CardTitle>
</CardHeader>
<CardContent>
<Input placeholder="Digite sua busca..." />
<Button>Buscar</Button>
</CardContent>
</Card>
);
}
Deploy¶
Automático (GitHub Actions)¶
Push para main → Deploy automático via deploy-production.yml
Manual¶
# Build Docker
docker build -t portal .
# Push para Artifact Registry
docker tag portal gcr.io/PROJECT_ID/portal
docker push gcr.io/PROJECT_ID/portal
# Deploy Cloud Run
gcloud run deploy portal \
--image gcr.io/PROJECT_ID/portal \
--region us-east1
Feeds RSS/Atom/JSON¶
Características¶
- ✅ 3 formatos padrão: RSS 2.0, Atom 1.0, JSON Feed 1.1
- ✅ Filtros dinâmicos: Por órgão, tema, tag e busca textual
- ✅ Construtor interativo: Página
/feedscom multi-select de órgãos/temas - ✅ Links contextuais: Feeds automáticos em páginas de busca, tema e órgão
- ✅ Autodiscovery: Tags
<link rel="alternate">no<head> - ✅ Caching otimizado:
Cache-Controlcom 10min + ETag para 304 Not Modified - ✅ Markdown → HTML: Conversão server-side com mesmo pipeline do portal
Endpoints¶
GET /feed.xml?agencias=mre,saude&temas=01&q=reforma&limit=30
GET /feed.atom?agencias=gestao&temas=03
GET /feed.json?temas=01,20&limit=50
Implementação¶
Arquitetura:
Route Handler (/feed.xml/route.ts)
↓
handleFeedRequest() (feed-handler.ts)
↓
parseFeedParams() → validateFeedParams() → buildFeed()
↓
queryArticlesForFeed() → Typesense
↓
markdownToHtml() → serializeFeed(format)
↓
computeETag() → Response (200 ou 304)
Validação:
- agencias: Verifica existência em agencies.yaml
- temas: Verifica existência em themes.yaml
- q: Máximo 200 caracteres
- limit: Entre 1-50
Caching:
- Cache-Control: public, s-maxage=600, stale-while-revalidate=60
- ETag: MD5 do body
- If-None-Match → 304 Not Modified
Título dinâmico:
/feed.xml → "Destaques GOV.BR"
/feed.xml?agencias=mre → "Destaques GOV.BR — Ministério das Relações Exteriores"
/feed.xml?temas=03 → "Destaques GOV.BR — Saúde"
/feed.xml?q=reforma → "Destaques GOV.BR — Busca: reforma"
Testes¶
Cobertura: 43 testes unitários (32 em feed.test.ts + 11 em markdown-to-html.test.ts)
Áreas testadas: - Parsing e validação de parâmetros - Query Typesense com filtros combinados - Serialização nos 3 formatos - Conversão markdown → HTML - ETag computation e 304 responses - Tratamento de erros (400, 500)
Documentação Adicional¶
→ API completa: portal/docs/FEEDS_API.md (198 linhas)
→ Arquitetura técnica: portal/docs/FEEDS_ARCHITECTURE.md
Links Relacionados¶
- Visão Geral da Arquitetura
- Setup Frontend - Guia de desenvolvimento
- Typesense Local - Ambiente de desenvolvimento
- Deploy do Portal - GitHub Actions