Pular para o conteúdo

Modelo de Conteúdo

O EmDash utiliza um modelo de conteúdo baseado em banco de dados onde as definições de esquema residem no banco de dados, não no código. Esta é uma escolha de design fundamental que permite modificação de esquema em tempo de execução e configuração amigável para não-desenvolvedores.

CMSs tradicionais como Strapi ou Keystatic exigem que você defina o esquema em código:

// Traditional approach - schema in code
const posts = collection({
fields: {
title: text({ required: true }),
content: richText(),
},
});

O EmDash armazena essa mesma informação em tabelas do banco de dados:

-- _emdash_collections table
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 de conteúdo. A diferença está em onde essa estrutura reside e como pode ser modificada.

Modificação em Tempo de Execução

Crie e edite tipos de conteúdo sem alterações de código ou rebuilds. Não-desenvolvedores podem projetar seu modelo de conteúdo através da interface administrativa.

Colunas SQL Reais

Diferente do modelo EAV (Entidade-Atributo-Valor) do WordPress, cada campo obtém uma coluna real. Indexação adequada, chaves estrangeiras e otimização de consultas.

Auto-documentado

Ferramentas de banco de dados podem inspecionar o esquema diretamente. Não é necessário analisar código para entender o modelo de conteúdo.

Caminho de Migração

Exporte o esquema como JSON para controle de versão. Importe o esquema em novos ambientes.

Duas tabelas do sistema definem sua estrutura de conteúdo:

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, -- Lucide icon name
supports JSON, -- ["drafts", "revisions", "preview"]
source TEXT, -- How it was created
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT
);

O campo source rastreia como a coleção foi criada:

FonteDescrição
manualCriada via interface administrativa
template:blogCriada por um arquivo de seed de template
import:wordpressImportada do WordPress
discoveredDescoberta automaticamente a partir de dados existentes
CREATE TABLE _emdash_fields (
id TEXT PRIMARY KEY,
collection_id TEXT REFERENCES _emdash_collections(id),
slug TEXT NOT NULL, -- Column name: "title", "price"
label TEXT NOT NULL, -- Display label
type TEXT NOT NULL, -- Field type
column_type TEXT NOT NULL, -- SQLite type: TEXT, REAL, INTEGER, JSON
required INTEGER DEFAULT 0,
unique_field INTEGER DEFAULT 0,
default_value TEXT, -- JSON-encoded default
validation JSON, -- Validation rules
widget TEXT, -- Custom widget identifier
options JSON, -- Widget options
sort_order INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(collection_id, slug)
);

Cada coleção obtém sua própria tabela com o prefixo ec_. Quando você cria uma coleção “products” com campos title e price:

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
-- Colunas de conteúdo (das definições de campo)
title TEXT NOT NULL,
price REAL
);

Quando você adiciona um campo via interface administrativa, o EmDash:

  1. Insere um registro em _emdash_fields 2. Executa ALTER TABLE ec_collection ADD COLUMN nome_da_coluna TIPO 3. Regenera o esquema Zod para validação

O SQLite suporta estas operações ALTER TABLE em tempo de execução:

OperaçãoSuportada
Adicionar colunaSim
Renomear colunaSim
Remover colunaSim (SQLite 3.35+)
Alterar tipo de colunaNão (requer reconstrução da tabela)

Para alterações de tipo, o EmDash lida com a reconstrução da tabela de forma transparente: cria nova tabela → copia dados → remove tabela antiga → renomeia nova tabela.

O EmDash mantém uma separação clara:

PreocupaçãoLocalizaçãoTabelas
EsquemaTabelas do sistema_emdash_collections, _emdash_fields
ConteúdoTabelas por coleçãoec_posts, ec_products, etc.
MídiaTabela separada + armazenamentoTabela media + R2/S3
ConfiguraçõesTabela de opçõesoptions com prefixo site:

Esta separação significa:

  • O esquema pode ser exportado sem conteúdo
  • O conteúdo pode ser migrado entre esquemas
  • As tabelas do sistema nunca ficam poluídas com dados do usuário

O EmDash constrói esquemas Zod a partir das definições de campo do banco de dados na inicialização:

// Simplified example
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 esses esquemas de tempo de execução em cada operação de criação e atualização.

Gere tipos TypeScript a partir do esquema do seu banco de dados:

Terminal window
# Fetch schema from database, generate types
npx emdash types

Isso gera .emdash/types.ts:

// .emdash/types.ts (generated)
export interface Post {
title: string;
content: PortableTextBlock[];
excerpt?: string;
featuredImage?: string;
}
export interface Product {
title: string;
price: number;
quantity: number;
}
// Sobrecargas tipadas 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 }>;
}

Fluxo de Trabalho para Desenvolvedores vs. Não-Desenvolvedores

Seção intitulada “Fluxo de Trabalho para Desenvolvedores vs. Não-Desenvolvedores”

Desenvolvedores podem usar a CLI:

Terminal window
# Fetch schema, generate types
npx emdash types
# Exportar esquema como JSON
npx emdash export-seed > seed.json

Não-desenvolvedores usam exclusivamente a interface administrativa:

  1. Abra Tipos de Conteúdo no painel administrativo
  2. Clique em Adicionar Coleção
  3. Defina campos através do construtor visual
  4. Comece a criar conteúdo imediatamente

Ambas as abordagens modificam as mesmas tabelas de banco de dados subjacentes.

Templates e exportações usam arquivos JSON de seed para definições de esquema portáteis:

{
"version": "1",
"collections": [
{
"slug": "posts",
"label": "Posts do blog",
"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": "Categorias", "hierarchical": true }],
"menus": [{ "name": "primary", "label": "Navegação principal" }]
}

Aplique arquivos de seed programaticamente:

import { applySeed, validateSeed } from "emdash/seed";
import seedData from "../../concepts/.emdash/seed.json";
// Valide primeiro
const { valid, errors } = validateSeed(seedData);
// Aplique (idempotente - seguro para reexecutar)
await applySeed(db, seedData, {
includeContent: true,
onConflict: "skip", // 'skip' | 'update' | 'error'
});
AbordagemLocalização do EsquemaModificação em Tempo de ExecuçãoSegurança de Tipos
EmDashBanco de DadosSim (completa)Gerada a partir do BD
WordPressCódigo PHP + EAVLimitada (campos meta)Nenhuma
StrapiArquivos de códigoNão (rebuild necessário)Gerada no build
SanityArquivos de códigoNão (esquema deve ser implantado)Integrada
DirectusBanco de DadosSim (completa)Gerada a partir do BD

O EmDash segue o modelo do Directus: banco de dados primeiro com geração opcional de tipos. Isso oferece máxima flexibilidade, enquanto ainda suporta desenvolvimento com tipagem segura quando desejado.