Ir al contenido

Modelo de Contenido

EmDash utiliza un modelo de contenido basado en base de datos donde las definiciones del esquema residen en la base de datos, no en el código. Esta es una decisión de diseño fundamental que permite la modificación del esquema en tiempo de ejecución y una configuración amigable para no desarrolladores.

Los CMS tradicionales como Strapi o Keystatic requieren que definas el esquema en el código:

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

EmDash almacena esta misma información en tablas de la base de datos:

-- _emdash_collections table
INSERT INTO _emdash_collections (slug, label)
VALUES ('posts', 'Blog Posts');
-- Tabla _emdash_fields
INSERT INTO _emdash_fields (collection_id, slug, type, required)
VALUES
('coll_abc', 'title', 'string', true),
('coll_abc', 'content', 'portableText', false);

Ambos enfoques definen la misma estructura de contenido. La diferencia está en dónde reside esa estructura y cómo puede modificarse.

Modificación en Tiempo de Ejecución

Crea y edita tipos de contenido sin cambios de código o reconstrucciones. Los no desarrolladores pueden diseñar su modelo de contenido a través de la interfaz de administración.

Columnas SQL Reales

A diferencia del modelo EAV (Entidad-Atributo-Valor) de WordPress, cada campo obtiene una columna real. Indexación adecuada, claves foráneas y optimización de consultas.

Auto-documentado

Las herramientas de base de datos pueden inspeccionar el esquema directamente. No es necesario analizar código para entender el modelo de contenido.

Ruta de Migración

Exporta el esquema como JSON para control de versiones. Importa el esquema en nuevos entornos.

Dos tablas del sistema definen tu estructura de contenido:

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
);

El campo source rastrea cómo se creó la colección:

OrigenDescripción
manualCreada a través de la interfaz de administración
template:blogCreada por el archivo de semilla de una plantilla
import:wordpressImportada desde WordPress
discoveredDescubierta automáticamente a partir de datos 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 colección obtiene su propia tabla con el prefijo ec_. Cuando creas una colección “products” con campos title y 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
-- Columnas de contenido (de las definiciones de campo)
title TEXT NOT NULL,
price REAL
);

Cuando agregas un campo a través de la interfaz de administración, EmDash:

  1. Inserta un registro en _emdash_fields 2. Ejecuta ALTER TABLE ec_collection ADD COLUMN nombre_columna TIPO 3. Regenera el esquema Zod para validación

SQLite admite estas operaciones ALTER TABLE en tiempo de ejecución:

OperaciónSoportada
Agregar columnaSí
Renombrar columnaSí
Eliminar columnaSí (SQLite 3.35+)
Cambiar tipo de columnaNo (requiere reconstrucción de tabla)

Para cambios de tipo, EmDash maneja la reconstrucción de la tabla de forma transparente: crear nueva tabla → copiar datos → eliminar tabla antigua → renombrar nueva tabla.

EmDash mantiene una separación clara:

ÁmbitoUbicaciónTablas
EsquemaTablas del sistema_emdash_collections, _emdash_fields
ContenidoTablas por colecciónec_posts, ec_products, etc.
MediosTabla separada + almacenamientoTabla media + R2/S3
ConfiguracionesTabla de opcionesoptions con prefijo site:

Esta separación significa:

  • El esquema se puede exportar sin contenido
  • El contenido se puede migrar entre esquemas
  • Las tablas del sistema nunca se saturan con datos de usuario

EmDash construye esquemas Zod a partir de las definiciones de campo de la base de datos al iniciar:

// 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);
}

El contenido se valida contra estos esquemas en tiempo de ejecución en cada operación de creación y actualización.

Genera tipos de TypeScript desde el esquema de tu base de datos:

Ventana de terminal
# Fetch schema from database, generate types
npx emdash types

Esto genera .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 funciones 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 }>;
}

Flujo de Trabajo para Desarrolladores vs. No Desarrolladores

Sección titulada «Flujo de Trabajo para Desarrolladores vs. No Desarrolladores»

Los desarrolladores pueden usar la CLI:

Ventana de terminal
# Fetch schema, generate types
npx emdash types
# Exportar esquema como JSON
npx emdash export-seed > seed.json

Los no desarrolladores usan exclusivamente la interfaz de administración:

  1. Abre Tipos de Contenido en el panel de administración
  2. Haz clic en Agregar Colección
  3. Define campos a través del constructor visual
  4. Comienza a crear contenido inmediatamente

Ambos enfoques modifican las mismas tablas de base de datos subyacentes.

Las plantillas y exportaciones usan archivos JSON de semilla para definiciones de esquema portátiles:

{
"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": "Categorias", "hierarchical": true }],
"menus": [{ "name": "primary", "label": "Navegacion principal" }]
}

Aplica archivos de semilla de forma programática:

import { applySeed, validateSeed } from "emdash/seed";
import seedData from "../../concepts/.emdash/seed.json";
// Valida primero
const { valid, errors } = validateSeed(seedData);
// Aplica (idempotente - seguro de volver a ejecutar)
await applySeed(db, seedData, {
includeContent: true,
onConflict: "skip", // 'skip' | 'update' | 'error'
});
EnfoqueUbicación del EsquemaModificación en Tiempo de EjecuciónSeguridad de Tipos
EmDashBase de datosSí (completa)Generada desde la BD
WordPressCódigo PHP + EAVLimitada (campos meta)Ninguna
StrapiArchivos de códigoNo (se requiere reconstrucción)Generada en la compilación
SanityArchivos de códigoNo (el esquema debe desplegarse)Integrada
DirectusBase de datosSí (completa)Generada desde la BD

EmDash sigue el modelo de Directus: primero la base de datos con generación opcional de tipos. Esto proporciona la máxima flexibilidad mientras aún admite el desarrollo con seguridad de tipos cuando se desea.