Arquitetura do site público — Extranets Associação Digital
O site público das extranets usa um route group separado do app principal, permitindo layouts, autenticação e configurações CSS completamente independentes. O padrão foi criado para o Abracaf e se repete para cada novo produto da Associação Digital.
Route group
O site público vive em apps/web/app/[locale]/(extranet_public)/. Os parênteses indicam um route group do Next.js — o segmento não aparece na URL e não afeta o roteamento.
app/[locale]/
├── (auth)/ ← autenticação do app principal
├── (marketing)/ ← marketing do app principal
├── home/ ← app autenticado
└── (extranet_public)/ ← site público (sem autenticação obrigatória)
└── abracaf/ ← produto Abracaf
├── layout.tsx ← carrega fontes, aplica Header + Footer
├── page.tsx ← home page
└── contato/
└── page.tsx ← página de contato
O middleware (proxy.ts) não exige autenticação para rotas dentro de (extranet_public) — não há handler de padrão para /abracaf/*. Qualquer visitante pode acessar o site público sem estar logado.
Layout e fontes
abracaf/layout.tsx é o único ponto onde as fontes do produto são carregadas e os componentes de chrome (Header + Footer) são montados:
import { Sora, Space_Grotesk } from 'next/font/google';
import { Footer } from './_components/abracaf-footer';
import { Header } from './_components/abracaf-header';
const sora = Sora({ subsets: ['latin'], variable: '--font-abc-sora', display: 'swap' });
const spaceGrotesk = Space_Grotesk({ subsets: ['latin'], variable: '--font-abc-space-grotesk', display: 'swap' });
export default function Layout({ children }: React.PropsWithChildren) {
return (
<div className={`${sora.variable} ${spaceGrotesk.variable} bg-abc-slate-50 font-abc-body flex min-h-screen flex-col`}>
<Header />
<main className="pt-extranet-header flex-1">{children}</main>
<Footer />
</div>
);
}
O pt-extranet-header (72px) compensa o header fixo para que o conteúdo de cada página não fique coberto. Todas as novas páginas dentro de abracaf/ herdam esse layout automaticamente.
Middleware — exclusão de assets estáticos
apps/web/proxy.ts define o matcher do middleware Next.js. Assets servidos diretamente precisam ser excluídos para não passar pelo processamento de i18n e autenticação:
export const config = {
matcher: [
'/((?!_next/static|_next/image|images|Video|locales|assets|sitemap.xml|robots.txt|api/*).*)',
],
};
O segmento Video foi adicionado para excluir arquivos em apps/web/public/Video/ — onde ficam os vídeos do hero das extranets. Sem essa exclusão o middleware interceptaria as requisições de vídeo e causaria redirecionamentos incorretos.
Se você adicionar uma nova pasta em public/ com assets grandes (vídeos, fontes pesadas), adicione o nome da pasta ao matcher acima para evitar que o middleware intercepte essas requisições.
Sitemap e robots (multi-domínio)
Cada extranet roda em domínio próprio (ex.: abracaf.com.br), mas o sitemap.xml e o robots.txt precisam de tratamento especial por duas restrições do Next.js:
robots.txtsó vale na raiz doapp— existe um robots por projeto, não dá para criar um por produto dentro de(extranet_public).- O
sitemap.xmlcai na raiz — o matcher do middleware excluisitemap.xml/robots.txtdo rewrite multi-domínio (seção acima), entãoabracaf.com.br/sitemap.xmlresolve direto noapp/sitemap.tsraiz, sem o prefixo do produto.
Por isso o padrão é despachante por host: os arquivos raiz app/sitemap.ts e app/robots.ts leem o header host e, quando é o domínio de uma extranet, delegam para um módulo de SEO no escopo do próprio produto. Toda a lógica do produto fica no produto; a raiz só desvia.
Módulo de SEO do produto
app/[locale]/(extranet_public)/abracaf/_lib/abracaf-seo.ts concentra tudo do Abracaf:
const ABRACAF_HOSTS = [
'abracaf.com.br',
'www.abracaf.com.br',
'preview.abracaf.com.br',
'preview-hml.abracaf.com.br',
];
export function isAbracafHost(host: string | null): boolean { /* normaliza porta + inclui em ABRACAF_HOSTS */ }
export function isAbracafPreviewHost(host: string | null): boolean { /* preview. / preview-hml. */ }
// Rotas estáticas públicas + posts (POSTS de blog/_lib/posts.data) → /blog/{slug}
export function getAbracafSitemap(baseUrl: string): MetadataRoute.Sitemap { /* ... */ }
// preview → Disallow: / | produção → Allow: / + Disallow privados + Sitemap + host
export function getAbracafRobots(host: string, privatePaths: string[]): MetadataRoute.Robots { /* ... */ }
Despachantes na raiz
const host = (await headers()).get('host');
if (isAbracafHost(host)) return getAbracafSitemap(`https://${host}`);
// senão: sitemap default do app (CMS)
// Áreas pós-login / auth — nunca indexáveis (a intranet roda nos hosts default).
const PRIVATE_PATHS = ['/home', '/admin', '/auth', '/login', '/join', '/identities', '/update-password', '/api'];
const host = (await headers()).get('host');
if (isAbracafHost(host)) return getAbracafRobots(host!, PRIVATE_PATHS);
// senão (domínio principal / intranet): Allow: / + Disallow PRIVATE_PATHS
Comportamento por host
| Host | sitemap.xml | robots.txt |
|---|---|---|
abracaf.com.br / www | Rotas do site + posts do blog | Allow: / + Disallow privados + Sitemap |
preview. / preview-hml.abracaf.com.br | Igual produção | Disallow: / (não indexa homologação) |
| Domínio principal / intranet (default) | Sitemap default do app (CMS) | Allow: / + Disallow das áreas privadas |
A intranet e o fluxo de login não rodam no host do site público — caem no branch default. Por isso é o robots.txt default que precisa do Disallow de /home, /auth, /admin etc., garantindo que a tela de login e a intranet fiquem fora do índice dos buscadores.
Hoje os artigos do sitemap vêm de blog/_lib/posts.data.ts (mock). Há um TODO em abracaf-seo.ts para trocar pela fonte real (Supabase) quando o endpoint existir.
Adicionar SEO para um produto novo
- Criar
<produto>/_lib/<produto>-seo.tscom os hosts do produto +getXSitemap/getXRobots. - Adicionar mais um
if (isXHost(host))nos despachantesapp/sitemap.tseapp/robots.ts.
Adicionando nova página
-
Criar a pasta da página dentro do produto:
app/[locale]/(extranet_public)/abracaf/nova-pagina/└── page.tsx -
Criar os componentes de seção em
_components/:app/[locale]/(extranet_public)/abracaf/_components/└── nova-pagina-secao.tsx -
O layout com Header e Footer é aplicado automaticamente — nenhuma configuração adicional necessária.
Padrão de page.tsx:
import { NovaPaginaHero } from './_components/nova-pagina-hero';
import { NovaPaginaConteudo } from './_components/nova-pagina-conteudo';
export const generateMetadata = async () => ({
title: 'Nova Página | Abracaf',
description: 'Descrição da página.',
});
export default function Page() {
return (
<>
<NovaPaginaHero />
<NovaPaginaConteudo />
</>
);
}
Adicionando novo produto
Para adicionar um produto novo (ex: Assobens):
- Criar
app/[locale]/(extranet_public)/assobens/layout.tsxseguindo o modelo do Abracaf - Criar
styles/theme.assobens.csscom cores e fontes específicas do cliente - Importar o tema em
styles/globals.css - Criar as páginas e componentes dentro de
assobens/
Ver Design System Extranets para o guia completo de criação de tema por cliente.