Painel de administração

Nesta página

O painel de administração EmDash é uma aplicação de página única (SPA) em React incorporada ao seu site Astro. Oferece uma interface completa de gestão de conteúdo para editoras e administradoras.

Visão geral da arquitetura

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

O admin é uma «grande ilha» React. O Astro gere o shell e a autenticação; toda a navegação e renderização dentro do admin são no cliente.

Pilha tecnológica

CamadaTecnologiaFinalidade
EncaminhamentoTanStack RouterEncaminhamento no cliente com tipos seguros
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 alinhada ao esquema do servidor
ÍconesPhosphorIconografia coerente
EditorTipTapEdição de texto rico (Portable Text)

Estrutura de rotas

O admin monta-se em /_emdash/admin/ e usa encaminhamento no cliente:

RotaEcrã
/Painel principal
/content/:collectionLista de conteúdo
/content/:collection/:idEditor de conteúdo
/content/:collection/newNova entrada
/mediaBiblioteca de mídia
/content-typesConstrutor de esquema (só admin)
/menusMenus de navegação
/widgetsÁreas de widgets
/taxonomiesGestão de categorias/etiquetas
/settingsConfigurações do site
/plugins/:pluginId/*Páginas de plugins

UI baseada em manifesto

O admin não codifica manualmente coleções nem plugins. Obtém 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": "Audit Log",
			"adminPages": [{ "path": "history", "label": "Audit History" }],
			"widgets": [{ "id": "recent-activity", "title": "Recent Activity" }]
		}
	],
	"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
	"version": "abc123"
}

O admin constrói navegação, formulários e editores apenas a partir deste manifesto. Vantagens:

  • Alterações de esquema aparecem na hora — Não é preciso reconstruir o admin
  • A UI de plugins integra-se automaticamente — Páginas e widgets a partir do manifesto
  • Tipos na fronteira — Esquemas Zod permanecem no servidor

Fluxo de dados

  1. A SPA do admin carrega — TanStack Router inicializa 2. Obter manifesto — TanStack Query em cache os metadados de coleções/plugins 3. Construir navegação — Barra lateral gerada a partir do manifesto 4. A utilizadora navega — Encaminhamento no cliente, sem recarregar a página 5. Obter dados — TanStack Query pede conteúdo às APIs REST 6. Renderizar formulários — Editores de campo gerados a partir dos descritores do manifesto 7. Enviar alterações — Mutações via TanStack Query, atualizações otimistas 8. O servidor valida — Esquemas Zod no servidor, erros em JSON

Endpoints REST

O admin comunica exclusivamente via APIs REST:

API de conteúdo

MétodoEndpointFinalidade
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

API de esquema

MétodoEndpointFinalidade
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/:slugEliminar coleção
POST/api/schema/collections/:slug/fieldsAdicionar campo
PUT/api/schema/collections/:slug/fields/:fieldAtualizar campo
DELETE/api/schema/collections/:slug/fields/:fieldEliminar campo

API de mídia

MétodoEndpointFinalidade
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/:idEliminar item de mídia
GET/api/media/file/:keyServir ficheiro de mídia

Outras APIs

EndpointFinalidade
/api/settingsConfigurações do site (GET/POST)
/api/menus/*Menus de navegação
/api/widget-areas/*Gestão de widgets
/api/taxonomies/*Termos de taxonomia
/api/admin/plugins/*Estado do plugin

Paginação

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

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

Obter a página seguinte:

GET /api/content/posts?cursor=eyJpZCI6IjAxSjEyMzQ1NiJ9

UI de admin de plugins

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

// virtual:emdash/plugin-admins (gerado)
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,
};

Páginas de plugin

As páginas de plugin montam-se sob /_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

Widgets do painel

Os plugins podem adicionar widgets ao painel:

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

Autenticação

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

// Lógica simplificada do middleware
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();
}

A SPA do admin não trata do login: isso é uma página Astro que define o cookie de sessão.

Acesso por funções

Cada função vê partes diferentes do admin:

FunçãoSecções visíveis
EditorPainel, coleções atribuídas, mídia
Admin+ Tipos de conteúdo, todas as coleções, configurações
Developer+ Acesso CLI, tipos gerados

O endpoint do manifesto filtra coleções e recursos conforme a função da utilizadora.

Editor de conteúdo

O editor de conteúdo gera formulários dinamicamente a partir das definições de campo:

// Renderização simplificada do editor
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
stringCampo de texto
textÁrea de texto
numberCampo numérico
booleanInterruptor
datetimeSeletor de data/hora
selectLista pendente
multiSelectSeleção múltipla
portableTextEditor TipTap
imageSeletor de mídia
referenceSeletor de entradas

Editor de texto rico

Os campos Portable Text usam TipTap (ProseMirror):

A utilizadora escreve → TipTap (JSON ProseMirror) → Guardar → Portable Text (BD)
Carregar → Portable Text (BD) → TipTap (JSON ProseMirror) → Mostrar

A conversão ocorre nos limites de carregar/guardar com portableTextToProsemirror() e prosemirrorToPortableText().

Blocos suportados:

  • Parágrafos, títulos (H1–H6)
  • Listas com marcadores e numeradas
  • Citações, blocos de código
  • Imagens (da biblioteca de mídia)
  • Ligações

Blocos desconhecidos de plugins ou importações mantêm-se como marcadores só de leitura.

Biblioteca de mídia

A biblioteca de mídia oferece:

  • Vista em grelha e lista
  • Pesquisa e filtro por tipo e data
  • Upload por arrastar e largar
  • Pré-visualização de imagem com metadados
  • Seleção e eliminação em lote

Os uploads usam URLs assinadas para upload direto cliente → armazenamento:

  1. Pedir URL de uploadPOST /api/media/upload-url 2. Carregar diretamente — O cliente faz PUT do ficheiro para a URL assinada (R2/S3) 3. Confirmar uploadPOST /api/media/:id/confirm 4. O servidor extrai metadados — Dimensões, tipo MIME, etc.

Isto contorna limites de tamanho do corpo nos Workers e mostra progresso real de upload.

Próximos passos