Pular para o conteúdo principal

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.

aviso

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:

  1. robots.txt só vale na raiz do app — existe um robots por projeto, não dá para criar um por produto dentro de (extranet_public).
  2. O sitemap.xml cai na raiz — o matcher do middleware exclui sitemap.xml/robots.txt do rewrite multi-domínio (seção acima), então abracaf.com.br/sitemap.xml resolve direto no app/sitemap.ts raiz, 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

apps/web/app/sitemap.ts
const host = (await headers()).get('host');
if (isAbracafHost(host)) return getAbracafSitemap(`https://${host}`);
// senão: sitemap default do app (CMS)
apps/web/app/robots.ts
// Á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

Hostsitemap.xmlrobots.txt
abracaf.com.br / wwwRotas do site + posts do blogAllow: / + Disallow privados + Sitemap
preview. / preview-hml.abracaf.com.brIgual produçãoDisallow: / (não indexa homologação)
Domínio principal / intranet (default)Sitemap default do app (CMS)Allow: / + Disallow das áreas privadas
Por que o robots default carrega o 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.

Posts do blog são mock

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

  1. Criar <produto>/_lib/<produto>-seo.ts com os hosts do produto + getXSitemap/getXRobots.
  2. Adicionar mais um if (isXHost(host)) nos despachantes app/sitemap.ts e app/robots.ts.

Adicionando nova página

  1. Criar a pasta da página dentro do produto:

    app/[locale]/(extranet_public)/abracaf/nova-pagina/
    └── page.tsx
  2. Criar os componentes de seção em _components/:

    app/[locale]/(extranet_public)/abracaf/_components/
    └── nova-pagina-secao.tsx
  3. 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):

  1. Criar app/[locale]/(extranet_public)/assobens/layout.tsx seguindo o modelo do Abracaf
  2. Criar styles/theme.assobens.css com cores e fontes específicas do cliente
  3. Importar o tema em styles/globals.css
  4. Criar as páginas e componentes dentro de assobens/

Ver Design System Extranets para o guia completo de criação de tema por cliente.