Pular para o conteúdo principal

Sistema de slugs — guia para replicar em outro frontend

Este documento explica como o default_crm-front usa slugs e como aplicar o mesmo modelo em outro frontend (React, Vue, Angular, mobile, etc.).

dois slugs diferentes no ecossistema. Não misture um com o outro.

ConceitoO que identificaDe onde vemPara que serve
Slug do tenant (empresa)A empresa dona do CRM (white-label)Subdomínio da URL (gevtech.gevtech.com.brgevtech)Branding, login do CRM, APIs GET /tenants/:slug/*
Slug do estabelecimento comercial (EC)O lojista / cliente finalCampo slug no cadastro do EC (API/metadata)URL do portal do cliente (https://meta.gevtech.com.br/customer/login)

1. Slug do tenant (multi-tenant por subdomínio)

Como funciona

O tenant é resolvido pelo hostname do browser:

https://{tenantSlug}.gevtech.com.br/manager/login
└─ tenantSlug = "gevtech"

Em desenvolvimento local:

http://horizonte-obras.localhost:3000/login
└─ tenantSlug = "horizonte-obras"

Algoritmo de resolução (ordem de prioridade)

Implementação de referência: src/common/helpers/company/tenant-subdomain.tsresolveTenantSlug().

  1. Subdomínio do hostname (getTenantSlugFromHostname)

    • Ignora www, app, api
    • localhost puro → sem subdomínio
    • *.localhost → primeiro segmento
    • a.b.dominio.coma (primeiro segmento)
    • dominio.com (2 partes) → sem subdomínio
  2. Session storage (tenant_slug) — persistido após login bem-sucedido

  3. Variável de ambiente NEXT_PUBLIC_TENANT_SLUG

  4. Fallback de dev (gevtech) — somente NODE_ENV === "development"

  5. Produção sem subdomínio → string vazia (não inventar slug)

Branding do tenant

Com o slug resolvido, o frontend busca nome, logo e banner:

GET /api/tenants/{tenantSlug}/nome
GET /api/tenants/{tenantSlug}/icon?json=true

No CRM, isso alimenta a tela de login (TenantBrandingProvideruseTenantBranding).

Login do CRM — atenção ao campo tenantSlug

O subdomínio identifica qual tenant carregar visualmente, mas o body do login envia outro valor:

POST /api/auth/login
{
"tenantSlug": "GEV TECNOLOGIA & INTELIGENCIA LTDA",
"email": "user@exemplo.com",
"password": "..."
}
  • No formulário, tenantSlug é preenchido com a razão social / nome da empresa (vindo de GET /tenants/:slug/nome), não com o slug do subdomínio.
  • O proxy (src/app/api/auth/login/route.ts) converte para razao_social no backend legado.

Resumo para outro frontend:

ContextoValor usado
Subdomínio da URLgevtech
GET /tenants/:slug/nome:slug = subdomínio
POST /auth/logintenantSlugrazão social retornada pelo branding

Variáveis de ambiente (tenant)

# Dev sem subdomínio
NEXT_PUBLIC_TENANT_SLUG=gevtech
NEXT_PUBLIC_TENANT_ID=uuid-do-tenant

# Opcional — UUID fixo do tenant em dev

Checklist — tenant em outro frontend

  • Parser de subdomínio compatível com *.localhost e produção
  • Lista de subdomínios reservados (www, app, api)
  • Cache de branding por slug (session/local storage, TTL ~24h)
  • Tela de login bloqueada se tenant inativo/suspenso
  • Não confundir slug do subdomínio com tenantSlug do POST de login
  • Em dev, fallback via env quando não há subdomínio

2. Slug do estabelecimento comercial (portal do cliente)

O que é

Cada estabelecimento comercial (loja/EC) tem um slug único usado como subdomínio do portal do cliente:

slug: "meta"
→ https://meta.gevtech.com.br/customer/login

Onde fica na API

O backend pode devolver o slug em:

  • data.slug (raiz do objeto), ou
  • data.metadata.slug (seção complementar)

Helper de referência: pickEstabelecimentoComercialSlugFromApiData() em
src/common/helpers/estabelecimento/estabelecimento-comercial-slug.ts.

Sempre use esse helper (ou equivalente) em vez de assumir um único caminho.

Regras de validação

RegraValor
Regex^[a-z0-9]+(?:-[a-z0-9]+)*$
Tamanho máximo100 caracteres
Vaziopermitido no formulário (backend pode exigir antes de criar painel)
Unicidadeconflito 409 → exibir erro no campo slug

Normalização (slugify)

Ao digitar nome fantasia, o CRM sugere slug automaticamente (paridade com backend):

function slugifyEstabelecimentoComercial(value: string): string {
return value
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "") // remove acentos
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, "-") // não-alfanumérico → hífen
.replace(/^-+|-+$/g, "") // trim hífens
.slice(0, 100);
}

Exemplo: "Padaria Central 24h""padaria-central-24h".

Montagem da URL do portal

function buildCustomerPortalLoginUrl(slug: string): string {
const clean = slug.trim();
if (!clean) return "";

// Opção A: template completo
const template = process.env.NEXT_PUBLIC_CUSTOMER_PORTAL_LOGIN_URL;
// ex.: "https://{slug}.gevtech.com.br/customer/login"
if (template?.includes("{slug}")) {
return template.replaceAll("{slug}", clean);
}

// Opção B: domínio base + path fixo
const baseDomain =
process.env.NEXT_PUBLIC_CUSTOMER_PORTAL_BASE_DOMAIN ?? "gevtech.com.br";
const protocol = baseDomain.includes("localhost") ? "http" : "https";

return `${protocol}://${clean}.${baseDomain}/customer/login`;
}

Formato correto:

https://{slug-ec}.gevtech.com.br/customer/login

Formato incorreto (não usar):

https://gevtech.com.br/customer/{slug}/login ❌
http://localhost:3002/customer/{slug}/login ❌

Cadastro e edição do EC

AçãoAPICampo
Criar ECPOST /estabelecimentos-comerciaisslug opcional no body
Editar ECPUT /estabelecimentos-comerciais/:uuidslug no diff parcial
ConflitoHTTP 409mapear mensagem → campo slug

Payload de criação (trecho):

{
"nomeFantasia": "META",
"slug": "meta",
"cpfCnpj": "..."
}

Criar painel do estabelecimento

Fluxo no CRM após selecionar contatos:

  1. GET /estabelecimentos-comerciais/:uuid — ler slug
  2. Exibir URL: buildCustomerPortalLoginUrl(slug)
  3. POST /estabelecimentos-comerciais/:uuid/painel — criar acessos
  4. POST /estabelecimentos-comerciais/:uuid/welcome-email — e-mail de boas-vindas por contato

Sem slug configurado → bloquear criação do painel e orientar edição do cadastro.

Variáveis de ambiente (portal EC)

NEXT_PUBLIC_CUSTOMER_PORTAL_BASE_DOMAIN=gevtech.com.br
NEXT_PUBLIC_CUSTOMER_PORTAL_LOGIN_URL=https://{slug}.gevtech.com.br/customer/login

Use uma das duas abordagens (template completo ou base domain + path no código).

Checklist — EC em outro frontend

  • Campo slug no formulário de cadastro/edição
  • Slugify ao alterar nome fantasia (opcional, mas recomendado)
  • Validação client-side com a mesma regex do backend
  • Tratar 409 como slug duplicado
  • Ler slug com fallback data.slug e data.metadata.slug
  • Montar URL do portal só com buildCustomerPortalLoginUrl
  • Exigir slug antes de fluxos que dependem do portal (painel, links públicos)

3. Portal do cliente (outro frontend) — recebendo o slug

Se você está construindo o frontend do portal (/customer/login), o slug do EC vem do subdomínio, de forma análoga ao tenant no CRM:

https://meta.gevtech.com.br/customer/login
└─ ecSlug = "meta"

Passos:

  1. Extrair meta do hostname (mesma lógica de getTenantSlugFromHostname, adaptando o domínio base).
  2. Opcional: GET /api/tenants/:slug/nome ou endpoint específico do EC para branding.
  3. No login do portal, enviar credenciais + identificador do estabelecimento conforme contrato da API de auth do portal.

O CRM e o portal podem compartilhar o mesmo padrão de subdomínio, mas o significado do slug muda: no CRM é tenant; no portal do cliente é o EC.


4. Estrutura mínima de código (portável)

Sugestão de módulos em qualquer stack:

lib/
tenant/
resolve-tenant-slug.ts # subdomínio → tenant
fetch-tenant-branding.ts # GET /tenants/:slug/nome
estabelecimento/
slugify.ts # normalização
validate-slug.ts # regex + tamanho
pick-slug-from-api.ts # data.slug | metadata.slug
build-portal-login-url.ts # URL pública do EC

Exemplo integrado (pseudo-código)

// Após salvar ou abrir detalhe do EC
const detail = await api.get(`/estabelecimentos-comerciais/${uuid}`);
const slug = pickEstabelecimentoComercialSlugFromApiData(detail.data);

if (!slug) {
showWarning("Configure o slug antes de gerar o painel.");
return;
}

const portalUrl = buildCustomerPortalLoginUrl(slug);
// "https://meta.gevtech.com.br/customer/login"
// Cadastro — ao blur do nome fantasia
onNomeFantasiaChange((nome) => {
setSlug(slugifyEstabelecimentoComercial(nome));
});
// Login CRM — após carregar branding
const tenantSlugFromHost = resolveTenantSlug(); // "gevtech"
const branding = await fetchTenantBranding(tenantSlugFromHost);
const razaoSocial = branding.nome; // para POST login

await api.post("/auth/login", {
tenantSlug: razaoSocial,
email,
password,
});

5. Fluxograma resumido

flowchart TB
subgraph CRM["Frontend CRM"]
A[Usuário acessa gevtech.gevtech.com.br] --> B[resolveTenantSlug → gevtech]
B --> C[GET /tenants/gevtech/nome]
C --> D[Tela de login com branding]
D --> E[POST /auth/login com razão social]

F[Cadastro EC slug meta] --> G[POST /estabelecimentos-comerciais]
G --> H[Criar painel]
H --> I[buildCustomerPortalLoginUrl meta]
I --> J[https://meta.gevtech.com.br/customer/login]
end

subgraph Portal["Frontend portal do cliente"]
K[Usuário acessa meta.gevtech.com.br] --> L[Extrai ecSlug meta]
L --> M[/customer/login]
end

6. Erros comuns

ErroCorreção
Usar slug do subdomínio no login do CRMEnviar razão social em tenantSlug
URL .../customer/{slug}/loginUsar subdomínio: {slug}.dominio.com/customer/login
Ler só data.slug e falhar quando API manda em metadataUsar pickEstabelecimentoComercialSlugFromApiData
Slug com maiúsculas ou acentosNormalizar com slugify antes de enviar
Criar painel sem slugBloquear UI e pedir edição do cadastro
Confundir tenant slug com EC slugSão entidades e URLs diferentes

7. Referências no repositório

ArquivoResponsabilidade
src/common/helpers/company/tenant-subdomain.tsResolução do slug do tenant
src/common/helpers/estabelecimento/estabelecimento-comercial-slug.tsSlug do EC, URL do portal
src/controllers/hooks/company/useTenantBranding.tsBranding por slug do tenant
src/views/components/LoginForm.tsxLogin com razão social
src/common/helpers/estabelecimento/estabelecimento-comercial-create-payload.tsEnvio do slug no POST
src/views/components/estabelecimento/lista/modals/CriarPainelEstabelecimentoModal.tsxURL do portal + painel
src/__tests__/common/helpers/estabelecimento-comercial-slug.test.tsTestes de slugify e URL
src/__tests__/common/helpers/tenant-subdomain.test.tsTestes de subdomínio
.env.exampleVariáveis do portal do cliente

8. Testes recomendados ao portar

Copie ou reimplemente os casos dos testes existentes:

Tenant

  • horizonte-obras.localhosthorizonte-obras
  • gevtech.gevtech.com.brgevtech
  • www.gevtech.com.br → sem slug (reservado)
  • Fallback NEXT_PUBLIC_TENANT_SLUG em dev

EC

  • slugify("Padaria Central 24h")padaria-central-24h
  • buildCustomerPortalLoginUrl("meta")https://meta.gevtech.com.br/customer/login
  • Template customizado via NEXT_PUBLIC_CUSTOMER_PORTAL_LOGIN_URL
  • pickEstabelecimentoComercialSlugFromApiData({ metadata: { slug: "x" } })"x"

9. Resumo executivo

  1. Tenant = subdomínio do CRM → branding + contexto; login usa razão social.
  2. EC = campo slug no cadastro → subdomínio do portal do cliente.
  3. Reutilize as mesmas funções de normalização, validação e montagem de URL para manter paridade com o backend.
  4. Configure envs de domínio do portal por ambiente (dev/staging/prod).
  5. Trate metadata.slug e slug na raiz ao ler respostas da API.

Com isso, qualquer frontend consegue integrar o mesmo comportamento sem depender do Next.js ou deste repositório diretamente.