Página de Blog — Site Público Abracaf
A página de blog do site público Abracaf vive em /abracaf/blog e exibe notícias e análises para a rede de concessionários Fiat. Todo o conteúdo é atualmente estático — os artigos são hardcoded em _lib/posts.data.ts.
Estrutura de arquivos
blog/
├── page.tsx ← RSC: monta hero + BlogContent
└── _components/
│ ├── blog-hero.tsx ← RSC: hero navy (eyebrow, h1, subtítulo)
│ ├── blog-content.tsx ← Client: estado de filtro, busca e paginação
│ ├── blog-filters.tsx ← Client: barra de filtros por categoria + busca
│ ├── blog-featured-post.tsx ← RSC: card artigo em destaque (navy)
│ ├── blog-post-card.tsx ← RSC: card artigo secundário (branco)
│ ├── blog-sidebar.tsx ← RSC: sidebar categorias + recentes
│ └── blog-pagination.tsx ← Client: paginação numérica
└── _lib/
└── posts.data.ts ← dados e tipos de artigos (estático)
page.tsx é RSC. Apenas blog-content.tsx, blog-filters.tsx e blog-pagination.tsx são 'use client', pois gerenciam estado interativo.
Dados e tipos
Os tipos e dados ficam em _lib/posts.data.ts:
export type PostTag = 'MERCADO' | 'LEGISLAÇÃO' | 'ECONOMIA' | 'REVISTA UNA+';
export interface Post {
tag: PostTag;
date: string; // ex: "25 Abr 2026"
read: string; // ex: "6 min"
title: string;
excerpt: string;
}
export const TAG_COLORS: Record<PostTag, string> = { ... };
export const POSTS: Post[] = [ /* 15 artigos, 3 páginas */ ];
export const POSTS_PER_PAGE = 6;
TAG_COLORS mapeia cada tag para sua cor no design system. O valor POSTS_PER_PAGE controla quantos artigos aparecem por página — o primeiro é promovido a featured, os demais vão para o grid de BlogPostCard.
Gerenciamento de estado
BlogContent é o único componente com estado. Ele centraliza filtro, busca e página, e passa os valores via props para os filhos:
const [filter, setFilter] = useState<FilterValue>('TODOS');
const [search, setSearch] = useState('');
const [page, setPage] = useState(1);
const filtered = posts.filter((p) => {
const matchTag = filter === 'TODOS' || p.tag === filter;
const matchSearch = !q || p.title.includes(q) || p.excerpt.includes(q);
return matchTag && matchSearch;
});
const paged = filtered.slice(start, start + POSTS_PER_PAGE);
const [featured, ...rest] = paged;
handleFilter e handleSearch sempre resetam page para 1 para evitar que o usuário fique em uma página que não existe após o filtro reduzir o total de resultados.
Filtros com Badge shadcn
Os botões de filtro usam o componente Badge de @kit/ui/badge. Por ser Base UI (não Radix), o padrão é render prop — não use asChild:
import { Badge } from '@kit/ui/badge';
<Badge
render={<button type="button" />}
onClick={() => onFilter(f.value)}
aria-pressed={active === f.value}
variant={active === f.value ? 'default' : 'secondary'}
className={
active === f.value
? 'bg-abc-navy-900 text-abc-white h-auto cursor-pointer rounded-full px-4 py-2 text-sm font-semibold'
: 'bg-abc-slate-100 text-abc-navy-700 h-auto cursor-pointer rounded-full px-4 py-2 text-sm font-semibold'
}
>
{f.label}
</Badge>
As classes h-auto, rounded-full, px-4 e py-2 sobrescrevem os defaults do Badge (h-5, rounded-4xl, px-2, py-0.5) via twMerge, que é chamado internamente pelo cn() do componente.
O componente BlogSidebar usa hidden lg:block em BlogContent — ele não aparece em dispositivos móveis. No mobile, a barra de filtros (BlogFilters) já cobre as categorias, tornando a sidebar redundante.
Adicionando novos artigos
Para adicionar um artigo, inclua um novo objeto no array POSTS em _lib/posts.data.ts:
{
tag: 'MERCADO', // um dos 4 valores de PostTag
date: '10 Jun 2026', // formato: "DD Mmm AAAA"
read: '5 min',
title: 'Título do artigo',
excerpt: 'Resumo de uma a duas frases exibido nos cards.',
},
O artigo aparecerá automaticamente nos filtros, na paginação e na sidebar de recentes (os 3 primeiros do array).
Os artigos são hardcoded. Quando o backend for integrado, substituir a constante POSTS em page.tsx por uma chamada ao servidor e remover os dados do arquivo posts.data.ts.