Cette page est destinée aux personnes travaillant sur EmDash, pas à celles construisant un site avec. Elle documente les mécanismes internes — les dispositions de tables, l’intégration Astro, le chemin de requête, la génération de code. Rien de tout cela n’est nécessaire pour utiliser EmDash. Si vous construisez un site, lisez plutôt Architecture et le Modèle de contenu.
L’intégration Astro
EmDash s’exécute comme une intégration Astro depuis le paquet emdash. Au moment du build :
-
Injecte les routes de la SPA admin et de l’API REST avec l’API
injectRouted’Astro. Rien n’est copié dans le projet de l’utilisateur. Les chemins injectés sont :Modèle de chemin Objectif /_emdash/admin/[...path]SPA du panneau admin /_emdash/api/manifestManifeste admin (collections, plugins) /_emdash/api/content/[collection]CRUD des entrées de contenu /_emdash/api/media/*Opérations de la bibliothèque de médias /_emdash/api/schema/*Gestion du schéma /_emdash/api/settingsParamètres du site /_emdash/api/menus/*Menus de navigation /_emdash/api/taxonomies/*Catégories, tags, taxonomies personnalisées -
Génère des modules virtuels pour que le bundler puisse résoudre et tree-shaker le code de configuration et des plugins :
Module Objectif virtual:emdash/configConfiguration de base de données et stockage virtual:emdash/dialectFabrique de dialecte de base de données virtual:emdash/plugin-adminsImports statiques pour les UIs admin des plugins -
Fournit le chargeur Live Collections, gère les migrations et ouvre la connexion de stockage.
Schéma database-first
Les définitions de schéma vivent dans la base de données, pas dans le code. Deux tables système suivent la structure.
_emdash_collections contient une ligne par collection :
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,
supports JSON, -- ["drafts", "revisions", "preview"]
source TEXT, -- comment elle a été créée
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT
);
La colonne source enregistre la provenance : manual (UI admin), template:<name> (fichier seed), import:wordpress (importateur) ou discovered (auto-détectée depuis les tables existantes).
_emdash_fields contient une ligne par champ, liée à sa collection :
CREATE TABLE _emdash_fields (
id TEXT PRIMARY KEY,
collection_id TEXT REFERENCES _emdash_collections(id),
slug TEXT NOT NULL, -- nom de colonne
label TEXT NOT NULL,
type TEXT NOT NULL, -- type de champ
column_type TEXT NOT NULL, -- TEXT, REAL, INTEGER, JSON
required INTEGER DEFAULT 0,
unique_field INTEGER DEFAULT 0,
default_value TEXT,
validation JSON,
widget TEXT,
options JSON,
sort_order INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(collection_id, slug)
);
Tables de contenu par collection
Chaque collection obtient sa propre table, préfixée ec_. Une collection products avec des champs title et price produit :
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 douce
version INTEGER DEFAULT 1, -- verrouillage optimiste
-- Colonnes de contenu, depuis les définitions de champs
title TEXT NOT NULL,
price REAL
);
De vraies colonnes (plutôt qu’une table avec un blob JSON) permettent une indexation correcte, des clés étrangères fonctionnelles, un schéma que les outils de base de données peuvent inspecter et aucun parsing JSON par champ.
Les responsabilités restent séparées :
| Responsabilité | Emplacement | Tables |
|---|---|---|
| Schéma | Tables système | _emdash_collections, _emdash_fields |
| Contenu | Tables par collection | ec_posts, ec_products, … |
| Médias | Table séparée + stockage | Table media + R2/S3 |
| Paramètres | Table d’options | options avec préfixe site: |
Modifications de schéma à l’exécution
L’ajout d’un champ via l’UI admin exécute trois étapes :
- Insérer un enregistrement dans
_emdash_fields. - Exécuter
ALTER TABLE ec_<collection> ADD COLUMN <name> <TYPE>. - Régénérer le schéma Zod utilisé pour la validation.
SQLite supporte l’ajout, le renommage et la suppression de colonnes (la suppression nécessite SQLite 3.35+) à l’exécution. Changer le type d’une colonne n’est pas supporté directement, donc EmDash reconstruit la table de manière transparente : créer une nouvelle table, copier les lignes, supprimer l’ancienne table, renommer la nouvelle.
Validation à l’exécution
EmDash construit des schémas Zod à partir des définitions de champs au démarrage et valide chaque création et mise à jour contre eux :
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);
}
Couche de données
EmDash utilise Kysely pour du SQL type-safe sur toutes les bases de données supportées (SQLite, libSQL, Cloudflare D1 et PostgreSQL). Le dialecte est sélectionné par virtual:emdash/dialect depuis la configuration que le site passe à l’intégration.
Chargeur Live Collections
Le contenu est servi à l’exécution via les Live Collections d’Astro. emdashLoader() implémente l’interface LiveLoader d’Astro et est enregistré comme une unique collection _emdash :
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({ loader: emdashLoader() }),
};
L’unique collection _emdash englobe chaque type de contenu ; le chargeur filtre par type quand getEmDashCollection("posts") est appelé.
Chemins de requête
Une requête de contenu depuis une page :
- Astro reçoit la requête et exécute le composant de page.
getEmDashCollection()appellegetLiveCollection()d’Astro.emdashLoaderinterroge la tableec_*pertinente via Kysely.- Les lignes sont mappées au format d’entrée d’Astro (
id,slug,data). - Le composant rend.
Une requête admin :
- Le middleware valide le token de session.
- La route API exécute le CRUD via un repository.
- Les hooks de cycle de vie se déclenchent (par exemple
content:beforeSave). - Kysely exécute le SQL.
- La route retourne du JSON à la SPA admin.
Internes du panneau admin
L’admin est une île React. Astro sert la coquille et applique l’authentification dans le middleware ; tout à l’intérieur est côté client, construit sur TanStack Router, TanStack Query, TanStack Table, React Hook Form + Zod, TipTap et Kumo (le système de design Base UI + Tailwind de Cloudflare).
La route coquille contrôle l’accès dans le middleware :
export async function onRequest({ request, locals }, next) {
const session = await getSession(request);
if (request.url.includes("/_emdash/admin")) {
if (!session?.user) return redirect("/_emdash/admin/login");
locals.user = session.user;
}
return next();
}
UI pilotée par le manifeste
L’admin ne code en dur rien sur les collections ou plugins. Il récupère GET /_emdash/api/manifest, qui retourne les collections, plugins et taxonomies auxquels l’utilisateur demandeur peut accéder, filtrés par rôle :
{
"collections": [
{
"slug": "posts",
"label": "Blog Posts",
"icon": "file-text",
"supports": ["drafts", "revisions", "preview"],
"fields": [{ "slug": "title", "type": "string", "required": true }]
}
],
"plugins": [{ "id": "audit-log", "label": "Audit Log" }],
"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
"version": "abc123"
}
La navigation, les formulaires et les éditeurs de champs sont générés à partir de ce manifeste, de sorte que les changements de schéma et de plugins apparaissent sans reconstruction de l’admin, et les schémas Zod restent côté serveur.
UIs admin des plugins
Les points d’entrée admin des plugins sont rassemblés dans un module virtuel généré d’imports statiques pour que le bundler puisse les résoudre et les tree-shaker :
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
export const pluginAdmins = { seo: pluginAdmin0 };
Conversion de texte riche
Les champs Portable Text s’éditent dans TipTap (ProseMirror). Le contenu est converti aux frontières de chargement et de sauvegarde par portableTextToProsemirror() et prosemirrorToPortableText(). Les blocs inconnus provenant de plugins ou d’importations sont préservés comme des espaces réservés en lecture seule.
Téléchargements signés
Les téléchargements de médias utilisent des URL signées directes vers le stockage quand l’adaptateur les supporte et un endpoint de streaming same-origin sinon :
- Le client demande une URL de téléchargement (
POST /api/media/upload-url). - Le client télécharge vers la cible retournée. Les adaptateurs compatibles S3 peuvent retourner une URL signée qui contourne les limites de taille de body du Worker ; les bindings R2 natifs et le stockage local retournent un endpoint de streaming EmDash.
- Le client confirme (
POST /api/media/:id/confirm). - Le serveur extrait les métadonnées (dimensions, type MIME).
Étendre l’importateur de contenu
L’importateur WordPress est construit sur une interface ImportSource connectable. Une source personnalisée implémente probe, analyze et fetch :
interface ImportSource {
probe(input: ImportInput): Promise<ProbeResult>;
analyze(input: ImportInput): Promise<AnalysisResult>;
fetchContent(input: ImportInput): AsyncIterable<NormalizedEntry>;
}
probe valide l’entrée et rapporte ce qui a été trouvé, analyze mappe les types de post source aux collections EmDash et signale les lacunes de schéma, et fetchContent diffuse des entrées normalisées que le pipeline d’importation écrit via les mêmes repositories que l’admin utilise. Les sources intégrées couvrent WordPress WXR, WordPress.com et l’API REST WordPress ; enregistrez une source personnalisée pour importer depuis un autre système.