O EmDash usa um modelo de conteúdo orientado ao banco de dados, em que as definições de esquema ficam no banco, não no código. É uma decisão de desenho fundamental que permite alterar o esquema em tempo de execução e um fluxo acessível sem papel de desenvolvedor.
Esquema como dados
CMSs tradicionais como Strapi ou Keystatic exigem definir o esquema em código:
// Abordagem tradicional: esquema no código
const posts = collection({
fields: {
title: text({ required: true }),
content: richText(),
},
});
O EmDash guarda a mesma informação em tabelas:
-- tabela _emdash_collections
INSERT INTO _emdash_collections (slug, label)
VALUES ('posts', 'Blog Posts');
-- tabela _emdash_fields
INSERT INTO _emdash_fields (collection_id, slug, type, required)
VALUES
('coll_abc', 'title', 'string', true),
('coll_abc', 'content', 'portableText', false);
Ambas as abordagens definem a mesma estrutura. A diferença é onde essa estrutura vive e como pode ser alterada.
Por que o banco primeiro?
Alteração em tempo de execução
Criar e editar tipos de conteúdo sem mudanças de código nem rebuilds. Sem papel de desenvolvedor, o modelo de dados pode ser desenhado pela UI de administração.
Colunas SQL reais
Ao contrário do modelo EAV do WordPress, cada campo tem uma coluna real. Índices, chaves estrangeiras e otimização de consultas.
Autodocumentado
Ferramentas de banco podem inspecionar o esquema diretamente. Não é preciso analisar código.
Caminho de migração
Exportar o esquema como JSON para controlo de versões. Importar em novos ambientes.
Tabelas de esquema
Duas tabelas de sistema definem a estrutura:
Tabela de coleções
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, -- Nome do ícone Lucide
supports JSON, -- ["drafts", "revisions", "preview"]
source TEXT, -- Como foi criada
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT
);
O campo source indica como a coleção foi criada:
| Valor | Descrição |
|---|---|
manual | Criada pelo admin |
template:blog | Pelo seed de um template |
import:wordpress | Importada do WordPress |
discovered | Detetada automaticamente |
Tabela de campos
CREATE TABLE _emdash_fields (
id TEXT PRIMARY KEY,
collection_id TEXT REFERENCES _emdash_collections(id),
slug TEXT NOT NULL, -- Nome da coluna: "title", "price"
label TEXT NOT NULL, -- Rótulo de exibição
type TEXT NOT NULL, -- Tipo de campo
column_type TEXT NOT NULL, -- Tipo SQLite: TEXT, REAL, INTEGER, JSON
required INTEGER DEFAULT 0,
unique_field INTEGER DEFAULT 0,
default_value TEXT, -- Valor padrão codificado em JSON
validation JSON, -- Regras de validação
widget TEXT, -- Identificador de widget personalizado
options JSON, -- Opções do widget
sort_order INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(collection_id, slug)
);
Tabelas de conteúdo
Cada coleção tem a sua própria tabela com o prefixo ec_. Ao criar uma coleção «products» com campos title e price:
CREATE TABLE ec_products (
-- Colunas de 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 lógica
version INTEGER DEFAULT 1, -- Bloqueio otimista
-- Colunas de conteúdo (das definições de campos)
title TEXT NOT NULL,
price REAL
);
Alterações de esquema em tempo de execução
Ao adicionar um campo pelo admin, o EmDash:
- Insere um registo em
_emdash_fields2. ExecutaALTER TABLE ec_collection ADD COLUMN column_name TYPE3. Regenera o esquema Zod para validação
Operações ALTER TABLE suportadas em tempo de execução no SQLite:
| Operação | Suportado |
|---|---|
| Adicionar coluna | Sim |
| Renomear coluna | Sim |
| Remover coluna | Sim (SQLite 3.35+) |
| Alterar tipo | Não (reconstrução da tabela) |
Para mudanças de tipo, o EmDash trata a reconstrução da tabela de forma transparente: cria nova tabela → copia dados → elimina tabela antiga → renomeia nova tabela.
Separação esquema / conteúdo
O EmDash mantém uma separação clara:
| Preocupação | Local | Tabelas |
|---|---|---|
| Esquema | Tabelas de sistema | _emdash_collections, _emdash_fields |
| Conteúdo | Tabelas por coleção | ec_posts, ec_products, etc. |
| Mídia | Tabela separada + armazenamento | media + R2/S3 |
| Configurações | Tabela de opções | options com prefixo site: |
Esta separação implica:
- O esquema pode ser exportado sem o conteúdo
- O conteúdo pode ser migrado entre esquemas
- As tabelas de sistema nunca ficam poluídas com dados de utilizador
Validação em tempo de execução
O EmDash constrói esquemas Zod a partir das definições de campos no arranque:
// Exemplo simplificado
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);
}
O conteúdo é validado contra estes esquemas em tempo de execução em cada operação de criação e atualização.
Integração TypeScript
Gere tipos TypeScript a partir do esquema no banco:
# Obter esquema do banco, gerar tipos
npx emdash types
Isto gera .emdash/types.ts:
// .emdash/types.ts (gerado)
export interface Post {
title: string;
content: PortableTextBlock[];
excerpt?: string;
featuredImage?: string;
}
export interface Product {
title: string;
price: number;
quantity: number;
}
// Overloads tipados para funções de consulta
declare module "emdash" {
export function getEmDashCollection(
type: "posts",
): Promise<{ entries: ContentEntry<Post>[]; error?: Error }>;
export function getEmDashEntry(
type: "products",
id: string,
): Promise<{ entry: ContentEntry<Product> | null; error?: Error; isPreview: boolean }>;
}
Fluxos desenvolvedor / não desenvolvedor
Desenvolvedores podem usar o CLI:
# Obter esquema, gerar tipos
npx emdash types
# Exportar esquema como JSON
npx emdash export-seed > seed.json
Não desenvolvedores usam exclusivamente o admin:
- Abrir Tipos de conteúdo no painel de administração
- Clicar em Adicionar coleção
- Definir campos pelo construtor visual
- Começar a criar conteúdo imediatamente
Ambos alteram as mesmas tabelas no banco.
Ficheiros seed
Templates e exportações usam ficheiros seed JSON para definições de esquema portáveis:
{
"version": "1",
"collections": [
{
"slug": "posts",
"label": "Blog Posts",
"labelSingular": "Post",
"supports": ["drafts", "revisions", "preview"],
"fields": [
{ "slug": "title", "type": "string", "required": true },
{ "slug": "content", "type": "portableText" },
{ "slug": "featuredImage", "type": "image" }
]
}
],
"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
"menus": [{ "name": "primary", "label": "Primary Navigation" }]
}
Aplicar ficheiros seed programaticamente:
import { applySeed, validateSeed } from "emdash/seed";
import seedData from "./.emdash/seed.json";
// Validar primeiro
const { valid, errors } = validateSeed(seedData);
// Aplicar (idempotente — seguro reexecutar)
await applySeed(db, seedData, {
includeContent: true,
onConflict: "skip", // 'skip' | 'update' | 'error'
});
Comparação com outras abordagens
| Abordagem | Local do esquema | Alteração em runtime | Segurança de tipos |
|---|---|---|---|
| EmDash | Banco de dados | Sim (completa) | Gerada da BD |
| WordPress | PHP + EAV | Limitada (meta fields) | Nenhuma |
| Strapi | Ficheiros de código | Não (rebuild necessário) | Gerada no build |
| Sanity | Ficheiros de código | Não (esquema precisa deploy) | Integrada |
| Directus | Banco de dados | Sim (completa) | Gerada da BD |
O EmDash segue o modelo Directus: banco primeiro com geração de tipos opcional. Isto proporciona máxima flexibilidade mantendo o suporte a desenvolvimento tipado quando desejado.
Próximos passos
Coleções
Saiba mais sobre tipos de campo e validação.
Painel de administração
Explore a arquitetura do admin.
Seeding
Configurar sites com ficheiros seed.