EmDash utilise un modèle de contenu orienté base de données où les définitions de schéma vivent dans la base, pas dans le code. C’est un choix de conception fondamental qui permet de modifier le schéma à l’exécution et un parcours utilisable sans rôle développeur.
Schéma comme données
Les CMS traditionnels comme Strapi ou Keystatic exigent de définir le schéma dans le code :
// Approche classique : schéma dans le code
const posts = collection({
fields: {
title: text({ required: true }),
content: richText(),
},
});
EmDash stocke les mêmes informations dans des tables :
-- Table _emdash_collections
INSERT INTO _emdash_collections (slug, label)
VALUES ('posts', 'Blog Posts');
-- Table _emdash_fields
INSERT INTO _emdash_fields (collection_id, slug, type, required)
VALUES
('coll_abc', 'title', 'string', true),
('coll_abc', 'content', 'portableText', false);
Les deux approches décrivent la même structure. La différence est l’emplacement de cette structure et la façon de la modifier.
Pourquoi la base d’abord ?
Modification à l’exécution
Créer et modifier des types de contenu sans changement de code ni rebuild. Sans rôle développeur, le modèle de données se conçoit depuis l’admin.
Vraies colonnes SQL
Contrairement au modèle EAV de WordPress, chaque champ a une vraie colonne. Index, clés étrangères et requêtes optimisées.
Auto-documenté
Les outils SQL inspectent le schéma directement. Pas besoin d’analyser le code pour comprendre le modèle.
Chemin de migration
Exporter le schéma en JSON pour le contrôle de version. L’importer dans de nouveaux environnements.
Tables de schéma
Deux tables système définissent la structure :
Table des collections
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, -- nom d’icône Lucide
supports JSON, -- ["drafts", "revisions", "preview"]
source TEXT, -- origine de création
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT
);
Le champ source indique comment la collection a été créée :
| Source | Description |
|---|---|
manual | Créée depuis l’admin |
template:blog | Créée par le seed d’un modèle |
import:wordpress | Importée depuis WordPress |
discovered | Détectée automatiquement |
Table des champs
CREATE TABLE _emdash_fields (
id TEXT PRIMARY KEY,
collection_id TEXT REFERENCES _emdash_collections(id),
slug TEXT NOT NULL, -- nom de colonne : "title", "price"
label TEXT NOT NULL, -- libellé affiché
type TEXT NOT NULL, -- type de champ
column_type TEXT NOT NULL, -- type SQLite : TEXT, REAL, INTEGER, JSON
required INTEGER DEFAULT 0,
unique_field INTEGER DEFAULT 0,
default_value TEXT, -- défaut encodé JSON
validation JSON, -- règles de validation
widget TEXT, -- identifiant de widget personnalisé
options JSON, -- options du widget
sort_order INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(collection_id, slug)
);
Tables de contenu
Chaque collection a sa table avec le préfixe ec_. Pour une collection « products » avec titre et prix :
CREATE TABLE ec_products (
-- Colonnes système (toujours présentes)
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, -- suppression logique
version INTEGER DEFAULT 1, -- verrouillage optimiste
-- Colonnes de contenu (définitions de champs)
title TEXT NOT NULL,
price REAL
);
Changements de schéma à l’exécution
Lorsque vous ajoutez un champ depuis l’admin, EmDash :
- Insère une ligne dans
_emdash_fields2. ExécuteALTER TABLE ec_<collection> ADD COLUMN <nom> <type>3. Régénère le schéma Zod pour la validation
SQLite autorise ces opérations ALTER TABLE à l’exécution :
| Opération | Prise en charge |
|---|---|
| Ajouter une colonne | Oui |
| Renommer une colonne | Oui |
| Supprimer une colonne | Oui (SQLite 3.35+) |
| Changer le type | Non (reconstruction de table) |
Pour un changement de type, EmDash reconstruit la table de façon transparente : nouvelle table → copie → suppression de l’ancienne → renommage.
Séparation schéma / contenu
EmDash sépare clairement :
| Préoccupation | Emplacement | Tables |
|---|---|---|
| Schéma | Tables système | _emdash_collections, _emdash_fields |
| Contenu | Tables par collection | ec_posts, ec_products, etc. |
| Médias | Table + stockage | table media + R2/S3 |
| Réglages | Table d’options | options avec préfixe site: |
Conséquences :
- Export du schéma sans contenu
- Migration de contenu entre schémas
- Tables système sans données utilisateur mélangées
Validation à l’exécution
EmDash construit des schémas Zod à partir des champs en base au démarrage :
// Exemple simplifié
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);
}
Chaque création et mise à jour est validée contre ces schémas.
Intégration TypeScript
Générez des types depuis le schéma de base :
# Récupérer le schéma et générer les types
npx emdash types
Cela produit .emdash/types.ts :
// .emdash/types.ts (généré)
export interface Post {
title: string;
content: PortableTextBlock[];
excerpt?: string;
featuredImage?: string;
}
export interface Product {
title: string;
price: number;
quantity: number;
}
// Surcharges pour les fonctions de requête
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 }>;
}
Parcours développeur / sans développeur
Développeur·se : CLI :
# Schéma et types
npx emdash types
# Exporter le schéma JSON
npx emdash export-seed > seed.json
Sans rôle développeur : admin uniquement :
- Ouvrir Types de contenu
- Cliquer Ajouter une collection
- Définir les champs dans le constructeur visuel
- Créer du contenu tout de suite
Les deux chemins modifient les mêmes tables.
Fichiers seed
Modèles et exports utilisent des fichiers seed JSON portables :
{
"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" }]
}
Application programmatique :
import { applySeed, validateSeed } from "emdash/seed";
import seedData from "./.emdash/seed.json";
// Valider d’abord
const { valid, errors } = validateSeed(seedData);
// Appliquer (idempotent)
await applySeed(db, seedData, {
includeContent: true,
onConflict: "skip", // 'skip' | 'update' | 'error'
});
Comparaison
| Approche | Schéma | Modif. runtime | Types |
|---|---|---|---|
| EmDash | Base | Oui (complète) | Générés depuis la BD |
| WordPress | PHP + EAV | Limitée (meta) | Aucun |
| Strapi | Fichiers code | Non (rebuild) | Au build |
| Sanity | Fichiers code | Non (déploiement schéma) | Intégré |
| Directus | Base | Oui (complète) | Générés depuis la BD |
EmDash suit le modèle Directus : base d’abord, types optionnels. Flexibilité maximale avec développement typé si besoin.
Étapes suivantes
Collections
En savoir plus sur les types de champs et la validation.
Panneau d’administration
Explorer l’architecture de l’admin.
Seeding
Configurer des sites avec des fichiers seed.