Modèle de contenu

Sur cette page

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 :

SourceDescription
manualCréée depuis l’admin
template:blogCréée par le seed d’un modèle
import:wordpressImportée depuis WordPress
discoveredDé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 :

  1. Insère une ligne dans _emdash_fields 2. Exécute ALTER 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érationPrise en charge
Ajouter une colonneOui
Renommer une colonneOui
Supprimer une colonneOui (SQLite 3.35+)
Changer le typeNon (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éoccupationEmplacementTables
SchémaTables système_emdash_collections, _emdash_fields
ContenuTables par collectionec_posts, ec_products, etc.
MédiasTable + stockagetable media + R2/S3
RéglagesTable d’optionsoptions 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 :

  1. Ouvrir Types de contenu
  2. Cliquer Ajouter une collection
  3. Définir les champs dans le constructeur visuel
  4. 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

ApprocheSchémaModif. runtimeTypes
EmDashBaseOui (complète)Générés depuis la BD
WordPressPHP + EAVLimitée (meta)Aucun
StrapiFichiers codeNon (rebuild)Au build
SanityFichiers codeNon (déploiement schéma)Intégré
DirectusBaseOui (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