Esta página é para pessoas trabalhando no EmDash, não para quem está construindo 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 EmDash. Se você está construindo um site, leia Architecture e o Content Model em vez disso.
A integração Astro
EmDash executa como uma integração Astro do pacote emdash. No momento da compilação:
-
Injeta o SPA admin e rotas REST API 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 de biblioteca de mídia /_emdash/api/schema/*Gerenciamento de esquema /_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 tree-shake o código de configuração e plugin:
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-adminsImportações estáticas para UIs admin de plugins -
Fornece o carregador Live Collections, gerencia migrações e abre a conexão de armazenamento.
Esquema database-first
Definições de esquema 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, -- how it was created
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-detectado 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, -- column name
label TEXT NOT NULL,
type TEXT NOT NULL, -- field type
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, prefixada com ec_. Uma coleção products com campos title e price produz:
CREATE TABLE ec_products (
-- System columns, always present
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, -- soft delete
version INTEGER DEFAULT 1, -- optimistic locking
-- Content columns, from field definitions
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 esquema que ferramentas de banco de dados podem inspecionar e sem análise JSON campo por campo.
As preocupações permanecem separadas:
| Preocupação | Localização | Tabelas |
|---|---|---|
| Esquema | 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 esquema em tempo de execução
Adicionar um campo através da UI admin executa três etapas:
- Insere um registro em
_emdash_fields. - Executa
ALTER TABLE ec_<collection> ADD COLUMN <name> <TYPE>. - Regenera o esquema Zod usado para validação.
SQLite suporta adicionar, renomear e remover colunas (remover requer SQLite 3.35+) em tempo de execução. Alterar o tipo de uma coluna não é suportado no local, então EmDash reconstrói a tabela de forma transparente: criar uma nova tabela, copiar linhas, remover a tabela antiga, renomear a nova.
Validação em tempo de execução
EmDash constrói esquemas Zod a partir 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
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 a partir da configuração que o site passa para a integração.
Carregador Live Collections
O conteúdo é servido em tempo de execução 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 coleção única _emdash envolve todos os tipos de conteúdo; o carregador filtra por tipo quando getEmDashCollection("posts") é chamado.
Caminhos de requisição
Uma requisição de conteúdo de uma página:
- 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 API executa CRUD através de um repositório.
- Hooks de ciclo de vida disparam (por exemplo
content:beforeSave). - Kysely executa o SQL.
- A rota retorna JSON para o SPA admin.
Internals do painel admin
O admin é uma ilha React. Astro serve o 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 (Base UI da Cloudflare + sistema de design Tailwind).
A rota 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 codifica nada 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, filtrados 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 esquema e plugin aparecem sem reconstrução do admin, e esquemas Zod permanecem do lado do servidor.
UIs admin de plugins
Pontos de entrada admin de plugins são coletados em um módulo virtual gerado de importações estáticas para que o bundler possa resolvê-los e tree-shake:
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
export const pluginAdmins = { seo: pluginAdmin0 };
Conversão de rich text
Campos Portable Text editam no TipTap (ProseMirror). O conteúdo é convertido nos limites 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 contornam limites de tamanho de corpo do Worker com URLs assinadas diretas para armazenamento:
- O cliente solicita uma URL de upload (
POST /api/media/upload-url). - O cliente faz upload diretamente para a URL assinada (R2 ou S3).
- 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 plugável ImportSource. 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 relata o que encontrou, analyze mapeia tipos de post de origem para coleções EmDash e sinaliza lacunas de esquema, 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.