Zum Inhalt springen

Inhaltsmodell

EmDash verwendet ein datenbank-zuerst Inhaltsmodell, bei dem Schema-Definitionen in der Datenbank und nicht im Code leben. Dies ist eine grundlegende Designentscheidung, die Laufzeit-Schemaänderungen und eine entwicklerfreundliche Einrichtung ermöglicht.

Traditionelle CMS wie Strapi oder Keystatic erfordern, dass Sie das Schema im Code definieren:

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

EmDash speichert dieselben Informationen in Datenbanktabellen:

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

Beide Ansätze definieren dieselbe Inhaltsstruktur. Der Unterschied liegt darin, wo diese Struktur lebt und wie sie geändert werden kann.

Laufzeitänderung

Erstellen und bearbeiten Sie Inhaltstypen ohne Codeänderungen oder Neubuilds. Nicht-Entwickler können ihr Inhaltsmodell über die Admin-Oberfläche gestalten.

Echte SQL-Spalten

Im Gegensatz zum EAV-Modell (Entity-Attribute-Value) von WordPress erhält jedes Feld eine echte Spalte. Richtige Indizierung, Fremdschlüssel und Abfrageoptimierung.

Selbstdokumentierend

Datenbank-Tools können das Schema direkt inspizieren. Kein Parsen von Code nötig, um das Inhaltsmodell zu verstehen.

Migrationspfad

Exportieren Sie das Schema als JSON für die Versionskontrolle. Importieren Sie das Schema in neuen Umgebungen.

Zwei Systemtabellen definieren Ihre Inhaltsstruktur:

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

Das Feld source verfolgt, wie die Collection erstellt wurde:

QuelleBeschreibung
manualÜber Admin-Oberfläche erstellt
template:blogDurch eine Template-Seed-Datei erstellt
import:wordpressVon WordPress importiert
discoveredAutomatisch aus vorhandenen Daten erkannt
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)
);

Jede Collection erhält ihre eigene Tabelle mit dem Präfix ec_. Wenn Sie eine “products”-Collection mit Titel- und Preis-Feldern erstellen:

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
-- Inhalts-Spalten (aus Felddefinitionen)
title TEXT NOT NULL,
price REAL
);

Wenn Sie ein Feld über die Admin-Oberfläche hinzufügen, macht EmDash:

  1. Fügt einen Datensatz in _emdash_fields ein 2. Führt ALTER TABLE ec_collection ADD COLUMN column_name TYPE aus 3. Generiert das Zod-Schema für die Validierung neu

SQLite unterstützt diese ALTER TABLE-Operationen zur Laufzeit:

OperationUnterstützt
Spalte hinzufügenJa
Spalte umbenennenJa
Spalte löschenJa (SQLite 3.35+)
Spaltentyp ändernNein (erfordert Tabellenneubau)

Für Typänderungen handhabt EmDash den Tabellenneubau transparent: neue Tabelle erstellen → Daten kopieren → alte Tabelle löschen → neue Tabelle umbenennen.

EmDash hält eine klare Trennung aufrecht:

AnliegenOrtTabellen
SchemaSystemtabellen_emdash_collections, _emdash_fields
InhaltPro-Collection-Tabellenec_posts, ec_products, etc.
MedienSeparate Tabelle + Speichermedia Tabelle + R2/S3
EinstellungenOptions-Tabelleoptions mit site: Präfix

Diese Trennung bedeutet:

  • Schema kann ohne Inhalt exportiert werden
  • Inhalt kann zwischen Schemata migriert werden
  • Systemtabellen werden nie mit Benutzerdaten überladen

EmDash baut Zod-Schemata aus Datenbank-Felddefinitionen beim Start auf:

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

Inhalt wird bei jedem Erstellungs- und Aktualisierungsvorgang gegen diese Laufzeit-Schemata validiert.

Generieren Sie TypeScript-Typen aus Ihrem Datenbankschema:

Terminal-Fenster
# Fetch schema from database, generate types
npx emdash types

Dies generiert .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;
}
// Typisierte Überladungen für Abfragefunktionen
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 }>;
}

Entwickler können die CLI verwenden:

Terminal-Fenster
# Fetch schema, generate types
npx emdash types
# Exportieren Sie das Schema als JSON
npx emdash export-seed > seed.json

Nicht-Entwickler verwenden ausschließlich die Admin-Oberfläche:

  1. Öffnen Sie Inhaltstypen im Admin-Bereich
  2. Klicken Sie auf Collection hinzufügen
  3. Definieren Sie Felder über den visuellen Builder
  4. Beginnen Sie sofort mit der Inhaltserstellung

Beide Ansätze ändern dieselben zugrundeliegenden Datenbanktabellen.

Templates und Exporte verwenden JSON-Seed-Dateien für portable Schema-Definitionen:

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

Wenden Sie Seed-Dateien programmatisch an:

import { applySeed, validateSeed } from "emdash/seed";
import seedData from "../../concepts/.emdash/seed.json";
// Zuerst validieren
const { valid, errors } = validateSeed(seedData);
// Anwenden (idempotent - sicher erneut auszuführen)
await applySeed(db, seedData, {
includeContent: true,
onConflict: "skip", // 'skip' | 'update' | 'error'
});
AnsatzSchema-OrtLaufzeitänderungTypsicherheit
EmDashDatenbankJa (vollständig)Aus DB generiert
WordPressPHP-Code + EAVEingeschränkt (Meta-Felder)Keine
StrapiCode-DateienNein (Neubuild erforderlich)Beim Build generiert
SanityCode-DateienNein (Schema muss deployed werden)Eingebaut
DirectusDatenbankJa (vollständig)Aus DB generiert

EmDash folgt dem Directus-Modell: datenbank-zuerst mit optionaler Typgenerierung. Dies bietet maximale Flexibilität und unterstützt dennoch typsichere Entwicklung, wenn gewünscht.