Pular para o conteúdo

Painel de Administração

O painel de administração do EmDash é uma aplicação React de página única incorporada no seu site Astro. Ele fornece uma interface completa de gerenciamento de conteúdo para editores e administradores.

┌────────────────────────────────────────────────────────────────┐
│ Astro Shell │
│ /_emdash/admin/[...path].astro │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ React SPA │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ TanStack │ │ TanStack │ │ Kumo │ │ │
│ │ │ Router │ │ Query │ │ Components │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ REST API Client │ │ │
│ │ │ /_emdash/api/* │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘

O admin é um aplicativo React de “ilha grande”. O Astro cuida do shell e da autenticação; toda a navegação e renderização dentro do admin é feita no lado do cliente.

CamadaTecnologiaPropósito
RoteamentoTanStack RouterRoteamento no lado do cliente com segurança de tipos
DadosTanStack QueryEstado do servidor, cache, mutações
UIKumoComponentes acessíveis (Base UI + Tailwind)
TabelasTanStack TableOrdenação, filtragem, paginação
FormuláriosReact Hook Form + ZodValidação correspondente ao esquema do servidor
ÍconesPhosphorIconografia consistente
EditorTipTapEdição de texto rico (Portable Text)

O admin é montado em /_emdash/admin/ e usa roteamento no lado do cliente:

CaminhoTela
/Dashboard
/content/:collectionLista de conteúdo
/content/:collection/:idEditor de conteúdo
/content/:collection/newNova entrada
/mediaBiblioteca de mídia
/content-typesConstrutor de esquema (somente admin)
/menusMenus de navegação
/widgetsÁreas de widgets
/taxonomiesGerenciamento de categorias/tags
/settingsConfigurações do site
/plugins/:pluginId/*Páginas de plugins

O admin não codifica conhecimento de coleções ou plugins. Em vez disso, ele busca um manifesto do servidor:

GET /_emdash/api/manifest

Resposta:

{
"collections": [
{
"slug": "posts",
"label": "Blog Posts",
"labelSingular": "Post",
"icon": "file-text",
"supports": ["drafts", "revisions", "preview"],
"fields": [
{ "slug": "title", "type": "string", "required": true },
{ "slug": "content", "type": "portableText" }
]
}
],
"plugins": [
{
"id": "audit-log",
"label": "Log de auditoria",
"adminPages": [{ "path": "history", "label": "Histórico de auditoria" }],
"widgets": [{ "id": "recent-activity", "title": "Atividade recente" }]
}
],
"taxonomies": [{ "name": "category", "label": "Categorias", "hierarchical": true }],
"version": "abc123"
}

O admin constrói sua navegação, formulários e editores inteiramente a partir deste manifesto. Benefícios:

  • Mudanças no esquema aparecem imediatamente — Nenhuma reconstrução do admin necessária
  • UI de plugins se integra automaticamente — Páginas e widgets do manifesto
  • Segurança de tipos no limite — Esquemas Zod permanecem no servidor
  1. SPA do admin carrega — TanStack Router inicializa 2. Buscar manifesto — TanStack Query armazena em cache metadados de coleções/plugins 3. Construir navegação — Barra lateral gerada a partir do manifesto 4. Usuário navega — Roteamento no lado do cliente, sem recarregar a página 5. Buscar dados — TanStack Query solicita conteúdo das APIs REST 6. Renderizar formulários — Editores de campo gerados a partir de descritores de campo do manifesto 7. Enviar alterações — Mutações via TanStack Query, atualizações otimistas 8. Servidor valida — Esquemas Zod no servidor, erros retornados como JSON

O admin se comunica exclusivamente através de APIs REST:

MétodoEndpointPropósito
GET/api/content/:collectionListar entradas
POST/api/content/:collectionCriar entrada
GET/api/content/:collection/:idObter entrada
PUT/api/content/:collection/:idAtualizar entrada
DELETE/api/content/:collection/:idExclusão lógica da entrada
GET/api/content/:collection/:id/revisionsListar revisões
POST/api/content/:collection/:id/preview-urlGerar URL de pré-visualização
MétodoEndpointPropósito
GET/api/schemaExportar esquema completo
GET/api/schema/collectionsListar coleções
POST/api/schema/collectionsCriar coleção
PUT/api/schema/collections/:slugAtualizar coleção
DELETE/api/schema/collections/:slugExcluir coleção
POST/api/schema/collections/:slug/fieldsAdicionar campo
PUT/api/schema/collections/:slug/fields/:fieldAtualizar campo
DELETE/api/schema/collections/:slug/fields/:fieldExcluir campo
MétodoEndpointPropósito
GET/api/mediaListar itens de mídia
POST/api/media/upload-urlObter URL de upload assinada
POST/api/media/:id/confirmConfirmar upload concluído
DELETE/api/media/:idExcluir item de mídia
GET/api/media/file/:keyServir arquivo de mídia
EndpointPropósito
/api/settingsConfigurações do site (GET/POST)
/api/menus/*Menus de navegação
/api/widget-areas/*Gerenciamento de widgets
/api/taxonomies/*Termos de taxonomia
/api/admin/plugins/*Estado do plugin

Todos os endpoints de lista usam paginação baseada em cursor:

{
"items": [...],
"nextCursor": "eyJpZCI6IjAxSjEyMzQ1NiJ9"
}

Buscar a próxima página:

GET /api/content/posts?cursor=eyJpZCI6IjAxSjEyMzQ1NiJ9

Os plugins podem estender o admin com páginas e widgets do painel. A integração gera um módulo virtual com importações estáticas:

// virtual:emdash/plugin-admins (generated)
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
import * as pluginAdmin1 from "@emdash-cms/plugin-analytics/admin";
export const pluginAdmins = {
seo: pluginAdmin0,
analytics: pluginAdmin1,
};

As páginas de plugin são montadas em /_emdash/admin/plugins/:pluginId/*:

// @emdash-cms/plugin-seo/src/admin.tsx
export const pages = [
{
path: "settings",
component: SEOSettingsPage,
label: "SEO Settings",
},
];

Renderiza em: /_emdash/admin/plugins/seo/settings

Os plugins podem adicionar widgets ao painel:

export const widgets = [
{
id: "seo-overview",
component: SEOWidget,
title: "SEO Overview",
size: "half", // "full" | "half" | "third"
},
];

A rota do shell do admin aplica autenticação via middleware do Astro:

// Simplified middleware logic
export async function onRequest({ request, locals }, next) {
const session = await getSession(request);
if (request.url.includes("/_emdash/admin")) {
if (!session?.user) {
return redirect("/_emdash/admin/login");
}
locals.user = session.user;
}
return next();
}

O próprio SPA do admin não lida com login — isso é uma página do Astro que define um cookie de sessão.

Funções diferentes veem partes diferentes do admin:

FunçãoSeções Visíveis
EditorPainel, coleções atribuídas, mídia
Admin+ Tipos de Conteúdo, todas as coleções, configurações
Desenvolvedor+ Acesso CLI, tipos gerados

O endpoint do manifesto filtra coleções e funcionalidades com base na função do usuário solicitante.

O editor de conteúdo gera formulários dinamicamente com base nas definições de campo:

// Simplified editor rendering
function ContentEditor({ collection, fields }) {
return (
<form>
{fields.map((field) => (
<FieldWidget
key={field.slug}
type={field.type}
label={field.label}
required={field.required}
options={field.options}
/>
))}
</form>
);
}

Cada tipo de campo tem um widget correspondente:

Tipo de CampoWidget
stringEntrada de texto
textÁrea de texto
numberEntrada numérica
booleanInterruptor de alternância
datetimeSeletor de data/hora
selectMenu suspenso
multiSelectSeleção múltipla
portableTextEditor TipTap
imageSeletor de mídia
referenceSeletor de entrada

Campos de Texto Portátil usam TipTap (ProseMirror) para edição:

User types → TipTap (ProseMirror JSON) → Save → Portable Text (DB)
Load → Portable Text (DB) → TipTap (ProseMirror JSON) → Display

A conversão ocorre nos limites de carregamento/salvamento via portableTextToProsemirror() e prosemirrorToPortableText().

Blocos suportados:

  • Parágrafos, cabeçalhos (H1-H6)
  • Listas com marcadores e numeradas
  • Citações em bloco, blocos de código
  • Imagens (da biblioteca de mídia)
  • Links

Blocos desconhecidos de plugins ou importações são preservados como espaços reservados somente leitura.

A biblioteca de mídia fornece:

  • Visualizações em grade e lista
  • Busca e filtro por tipo, data
  • Upload por arrastar e soltar
  • Pré-visualização de imagem com metadados
  • Seleção em massa e exclusão

Os uploads usam URLs assinadas para upload direto do cliente para o armazenamento:

  1. Solicitar URL de upload — POST /api/media/upload-url 2. Upload direto — Cliente faz PUT do arquivo para a URL assinada (R2/S3) 3. Confirmar upload — POST /api/media/:id/confirm 4. Servidor extrai metadados — Dimensões, tipo MIME, etc.

Esta abordagem ignora os limites de tamanho do corpo dos Workers e fornece progresso de upload em tempo real.