Esta página é para pessoas que trabalham no EmDash, não para quem constrói um site com ele. Documenta mecânicas internas — layouts de tabelas, a integração Astro, o caminho de requisição, geração de código. Nada disso é necessário para usar o EmDash. Se você está construindo um site, leia Arquitetura e o Modelo de conteúdo em vez disso.
A integração Astro
O EmDash roda como uma integração Astro do pacote emdash. No momento do build:
-
Injeta as rotas da SPA admin e da API REST com a API
injectRoutedo Astro. Nada é copiado para o projeto do usuário. Os caminhos injetados são:Padrão de caminho Propósito /_emdash/admin/[...path]SPA do painel admin /_emdash/api/manifestManifesto admin (coleções, plugins) /_emdash/api/content/[collection]CRUD de entradas de conteúdo /_emdash/api/media/*Operações da biblioteca de mídia /_emdash/api/schema/*Gerenciamento de schema /_emdash/api/settingsConfigurações do site /_emdash/api/menus/*Menus de navegação /_emdash/api/taxonomies/*Categorias, tags, taxonomias personalizadas -
Gera módulos virtuais para que o bundler possa resolver e fazer tree-shake do código de configuração e plugins:
Módulo Propósito virtual:emdash/configConfiguração de banco de dados e armazenamento virtual:emdash/dialectFactory de dialeto de banco de dados virtual:emdash/plugin-adminsImports estáticos para UIs admin de plugins -
Fornece o loader de Live Collections, gerencia migrações e abre a conexão de armazenamento.
Schema database-first
Definições de schema vivem no banco de dados, não no código. Duas tabelas do sistema rastreiam a estrutura.
_emdash_collections contém uma linha por coleção:
CREATE TABLE _emdash_collections (
id TEXT PRIMARY KEY,
slug TEXT UNIQUE NOT NULL, -- "posts", "products"
label TEXT NOT NULL, -- "Blog Posts"
label_singular TEXT, -- "Post"
description TEXT,
icon TEXT,
supports JSON, -- ["drafts", "revisions", "preview"]
source TEXT, -- como foi criada
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT
);
A coluna source registra a proveniência: manual (UI admin), template:<name> (arquivo seed), import:wordpress (importador), ou discovered (auto-detectada de tabelas existentes).
_emdash_fields contém uma linha por campo, vinculada à sua coleção:
CREATE TABLE _emdash_fields (
id TEXT PRIMARY KEY,
collection_id TEXT REFERENCES _emdash_collections(id),
slug TEXT NOT NULL, -- nome da coluna
label TEXT NOT NULL,
type TEXT NOT NULL, -- tipo de campo
column_type TEXT NOT NULL, -- TEXT, REAL, INTEGER, JSON
required INTEGER DEFAULT 0,
unique_field INTEGER DEFAULT 0,
default_value TEXT,
validation JSON,
widget TEXT,
options JSON,
sort_order INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(collection_id, slug)
);
Tabelas de conteúdo por coleção
Cada coleção obtém sua própria tabela, com prefixo ec_. Uma coleção products com campos title e price produz:
CREATE TABLE ec_products (
-- Colunas do sistema, sempre presentes
id TEXT PRIMARY KEY,
slug TEXT UNIQUE,
status TEXT DEFAULT 'draft',
author_id TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
published_at TEXT,
deleted_at TEXT, -- exclusão suave
version INTEGER DEFAULT 1, -- bloqueio otimista
-- Colunas de conteúdo, das definições de campo
title TEXT NOT NULL,
price REAL
);
Colunas reais (em vez de uma tabela com um blob JSON) fornecem indexação adequada, chaves estrangeiras funcionais, um schema que ferramentas de banco de dados podem inspecionar e sem parsing JSON por campo.
As responsabilidades permanecem separadas:
| Responsabilidade | Localização | Tabelas |
|---|---|---|
| Schema | Tabelas do sistema | _emdash_collections, _emdash_fields |
| Conteúdo | Tabelas por coleção | ec_posts, ec_products, … |
| Mídia | Tabela separada + armazenamento | Tabela media + R2/S3 |
| Configurações | Tabela de opções | options com prefixo site: |
Alterações de schema em runtime
Adicionar um campo pela UI admin executa três passos:
- Inserir um registro em
_emdash_fields. - Executar
ALTER TABLE ec_<collection> ADD COLUMN <name> <TYPE>. - Regenerar o schema Zod usado para validação.
O SQLite suporta adicionar, renomear e remover colunas (remover requer SQLite 3.35+) em runtime. Alterar o tipo de uma coluna não é suportado diretamente, então o EmDash reconstrói a tabela de forma transparente: cria uma nova tabela, copia as linhas, remove a tabela antiga, renomeia a nova.
Validação em runtime
O EmDash constrói schemas Zod das definições de campo na inicialização e valida cada criação e atualização contra eles:
function buildSchema(fields: Field[]): ZodSchema {
const shape: Record<string, ZodType> = {};
for (const field of fields) {
let zodType = fieldTypeToZod(field.type);
if (field.required) zodType = zodType.required();
if (field.validation?.min !== undefined) zodType = zodType.min(field.validation.min);
shape[field.slug] = zodType;
}
return z.object(shape);
}
Camada de dados
O EmDash usa Kysely para SQL type-safe em todos os bancos de dados suportados (SQLite, libSQL, Cloudflare D1 e PostgreSQL). O dialeto é selecionado por virtual:emdash/dialect da configuração que o site passa para a integração.
Loader de Live Collections
O conteúdo é servido em runtime através das Live Collections do Astro. emdashLoader() implementa a interface LiveLoader do Astro e é registrado como uma única coleção _emdash:
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({ loader: emdashLoader() }),
};
A única coleção _emdash envolve cada tipo de conteúdo; o loader filtra por tipo quando getEmDashCollection("posts") é chamado.
Caminhos de requisição
Uma requisição de conteúdo de uma página:
- O Astro recebe a requisição e executa o componente da página.
getEmDashCollection()chamagetLiveCollection()do Astro.emdashLoaderconsulta a tabelaec_*relevante através do Kysely.- As linhas são mapeadas para o formato de entrada do Astro (
id,slug,data). - O componente renderiza.
Uma requisição admin:
- O middleware valida o token de sessão.
- A rota da API executa CRUD através de um repositório.
- Hooks de ciclo de vida disparam (por exemplo
content:beforeSave). - O Kysely executa o SQL.
- A rota retorna JSON para a SPA admin.
Internos do painel admin
O admin é uma ilha React. O Astro serve a shell e aplica autenticação no middleware; tudo dentro é do lado do cliente, construído sobre TanStack Router, TanStack Query, TanStack Table, React Hook Form + Zod, TipTap e Kumo (o sistema de design Base UI + Tailwind da Cloudflare).
A rota da shell controla o acesso no 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();
}
UI dirigida por manifesto
O admin não tem nada hardcoded sobre coleções ou plugins. Ele busca GET /_emdash/api/manifest, que retorna as coleções, plugins e taxonomias que o usuário solicitante pode acessar, filtradas por papel:
{
"collections": [
{
"slug": "posts",
"label": "Blog Posts",
"icon": "file-text",
"supports": ["drafts", "revisions", "preview"],
"fields": [{ "slug": "title", "type": "string", "required": true }]
}
],
"plugins": [{ "id": "audit-log", "label": "Audit Log" }],
"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
"version": "abc123"
}
Navegação, formulários e editores de campo são gerados a partir deste manifesto, então mudanças de schema e plugins aparecem sem uma reconstrução do admin, e schemas Zod permanecem do lado do servidor.
UIs admin de plugins
Os pontos de entrada admin de plugins são coletados em um módulo virtual gerado de imports estáticos para que o bundler possa resolver e fazer tree-shake deles:
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
export const pluginAdmins = { seo: pluginAdmin0 };
Conversão de rich text
Campos Portable Text são editados no TipTap (ProseMirror). O conteúdo é convertido nas fronteiras de carregamento e salvamento por portableTextToProsemirror() e prosemirrorToPortableText(). Blocos desconhecidos de plugins ou importações são preservados como placeholders somente leitura.
Uploads assinados
Uploads de mídia usam URLs assinadas diretas para o armazenamento quando o adaptador as suporta e um endpoint de streaming same-origin caso contrário:
- O cliente solicita uma URL de upload (
POST /api/media/upload-url). - O cliente faz upload para o destino retornado. Adaptadores compatíveis com S3 podem retornar uma URL assinada que contorna os limites de tamanho de body do Worker; bindings R2 nativos e armazenamento local retornam um endpoint de streaming EmDash.
- O cliente confirma (
POST /api/media/:id/confirm). - O servidor extrai metadados (dimensões, tipo MIME).
Estendendo o importador de conteúdo
O importador WordPress é construído sobre uma interface ImportSource plugável. Uma fonte personalizada implementa probe, analyze e fetch:
interface ImportSource {
probe(input: ImportInput): Promise<ProbeResult>;
analyze(input: ImportInput): Promise<AnalysisResult>;
fetchContent(input: ImportInput): AsyncIterable<NormalizedEntry>;
}
probe valida a entrada e reporta o que encontrou, analyze mapeia os tipos de post fonte para coleções EmDash e sinaliza lacunas no schema, e fetchContent transmite entradas normalizadas que o pipeline de importação escreve através dos mesmos repositórios que o admin usa. Fontes integradas cobrem WordPress WXR, WordPress.com e a API REST do WordPress; registre uma fonte personalizada para importar de outro sistema.