Cette page est destinée aux personnes travaillant sur EmDash, pas à celles qui construisent un site avec. Elle explique la disposition de la base de données, l’intégration Astro, les chemins de requête, l’application d’administration, le flux de médias et le système d’importation. 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 de la compilation :
-
Il injecte l’application d’administration et les routes API REST avec l’API
injectRouted’Astro. Rien n’est copié dans le projet de l’utilisateur. Les principales familles de routes sont :Modèle de chemin Objectif /_emdash/admin/[...path]SPA du panneau d’administration /_emdash/api/manifestManifeste admin (collections, plugins) /_emdash/api/content/[collection]/...Opérations sur les entrées de contenu /_emdash/api/media/...Opérations de la bibliothèque média /_emdash/api/schema/...Gestion du schéma /_emdash/api/settings/...Paramètres du site /_emdash/api/menus/...Menus de navigation /_emdash/api/taxonomies/...Catégories, tags, taxonomies personnalisées /_emdash/api/plugins/[pluginId]/[...path]Routes API définies par les plugins L’injecteur de routes est l’inventaire complet, incluant l’authentification, les commentaires, la recherche, les importations, les widgets et les autres familles de routes.
-
Génère des modules virtuels pour que le bundler puisse résoudre le code de configuration et d’extension :
Module Objectif virtual:emdash/configConfiguration de la base de données, du stockage et du site virtual:emdash/dialectFactory du dialecte de base de données virtual:emdash/admin-registryImportations statiques pour les interfaces admin des plugins virtual:emdash/pluginsImplémentations de plugins configurés virtual:emdash/media-providersFournisseurs de médias externes configurés virtual-modules.tsdéfinit les helpers runtime restants et les contenus des modules générés. -
Fournit le loader Live Content Collections et enregistre le middleware runtime. Au moment de la requête, le middleware ouvre les connexions de base de données et de stockage configurées et applique les migrations en attente avant que les routes les utilisent.
Schéma database-first
Les définitions de schéma résident dans la base de données, pas dans un fichier de configuration statique. _emdash_collections stocke une ligne par collection. Ses colonnes principales décrivent la collection et les fonctionnalités que le runtime et l’admin exposent :
| Colonnes | Objectif |
|---|---|
id, slug | Identité stable de la collection |
label, label_singular, description, icon | Noms et orientations montrés aux rédacteurs |
supports, has_seo, comments_enabled, edit_locking | Capacités optionnelles de la collection |
title_field, date_field, admin_config, hidden, sort_order | Comportement de la liste admin et de la navigation |
url_pattern, routable | URL publique et comportement du slug |
source | Comment la collection a été créée |
La valeur source enregistre la provenance comme manual, seed, template:<name>, import:<name> ou discovered. Les paramètres supplémentaires proviennent des migrations enregistrées, donc database/types.ts et les migrations sont l’inventaire actuel des colonnes.
_emdash_fields stocke les champs liés à chaque collection :
| Colonnes | Objectif |
|---|---|
id, collection_id, slug | Identité du champ et collection propriétaire |
label, type, column_type | Libellé de l’éditeur, type de champ EmDash et type de stockage SQL |
required, unique, default_value, validation | Contraintes de contenu et valeurs par défaut |
widget, options, sort_order | Contrôle de l’éditeur et ordre d’affichage |
searchable, indexed, translatable | Comportement de recherche, requête et localisation |
collection_id référence _emdash_collections.id, et chaque slug de champ est unique au sein de sa collection.
Tables de contenu par collection
Chaque collection obtient sa propre table, préfixée ec_. Une collection products avec les champs title et price produit une table de cette forme :
CREATE TABLE ec_products (
-- Colonnes système, présentes dans chaque table de contenu
id TEXT PRIMARY KEY,
slug TEXT,
status TEXT DEFAULT 'draft',
author_id TEXT,
primary_byline_id TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
published_at TEXT,
scheduled_at TEXT,
deleted_at TEXT,
version INTEGER DEFAULT 1,
live_revision_id TEXT,
draft_revision_id TEXT,
locale TEXT NOT NULL DEFAULT 'en',
translation_group TEXT,
-- Colonnes de contenu, créées à partir des définitions de champs
title TEXT NOT NULL,
price REAL,
UNIQUE (slug, locale)
);
Les vraies colonnes donnent à chaque champ un type de base de données, permettent les index et les clés étrangères, et laissent les outils de base de données inspecter le schéma sans décoder un blob JSON de contenu. La contrainte unique permet aux traductions de partager un slug tout en gardant chaque slug unique au sein d’une langue. Toutes les variantes linguistiques de la même entrée partagent une valeur translation_group, qui permet à EmDash de trouver les lignes qui sont des traductions les unes des autres.
Les principaux aspects des données restent séparés :
| Aspect | 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 + stockage configuré |
| Paramètres | Table d’options | options avec préfixe site: |
Changements de schéma au runtime
L’ajout d’un champ via l’UI d’administration exécute ces étapes :
- Insérer la définition du champ dans
_emdash_fields. - Ajouter la colonne correspondante à la table
ec_*de la collection et créer un index lorsque le champ est configuré comme indexé. - Rafraîchir les types de développement générés pour que le nouveau champ apparaisse dans les outils de l’éditeur.
La validation de contenu lit les définitions de champs actuelles et construit un schéma Zod lorsque du contenu est créé ou mis à jour. Changer le type SQL sous-jacent, la contrainte required ou unique, ou le comportement de localisation d’un champ peut nécessiter une migration de contenu manuelle ; SchemaRegistry rejette les changements sur place non supportés au lieu de reconstruire la table implicitement.
Validation au runtime
EmDash dérive un schéma Zod des champs actuels de la collection. Le générateur délègue les détails de type et de contrainte à generateFieldSchema() :
export function generateZodSchema(
collection: CollectionWithFields,
): z.ZodObject<Record<string, ZodType>> {
const shape: Record<string, ZodType> = {};
for (const field of collection.fields) {
shape[field.slug] = generateFieldSchema(field);
}
return z.object(shape);
}
Le gestionnaire de contenu rejette également les champs inconnus, vérifie les valeurs de chaîne requises et vérifie les références à d’autres collections.
Couche de données
EmDash utilise Kysely pour du SQL typé à travers SQLite, libSQL, Cloudflare D1 et PostgreSQL. La configuration du site sélectionne l’adaptateur de base de données ; l’intégration expose sa factory de dialecte via virtual:emdash/dialect.
Loader Live Content Collections
Le contenu est servi au runtime via les Live Content Collections d’Astro. emdashLoader() implémente l’interface LiveLoader d’Astro et est enregistré comme une seule collection _emdash :
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({ loader: emdashLoader() }),
};
La collection unique _emdash englobe chaque collection EmDash. getEmDashCollection("posts") fournit le filtre de type posts, et le loader le mappe à la table ec_posts.
Chemins de requête
Une requête de contenu depuis une page Astro suit ce chemin :
- La page appelle
getEmDashCollection()ougetEmDashEntry(). - Le wrapper de requête appelle
getLiveCollection()ougetLiveEntry()d’Astro avec la collection interne_emdashet le type de collection EmDash demandé. emdashLoader()interroge la tableec_*pertinente via Kysely, appliquant les règles de publication, de langue, de filtre, de tri et de pagination.- Le wrapper de requête mappe les lignes aux entrées Astro et charge leurs signatures et termes de taxonomie.
- Le composant Astro rend les entrées retournées.
L’état de prévisualisation et du mode édition voyage à travers le contexte de requête, de sorte que les mêmes fonctions de requête peuvent retourner du contenu brouillon après que le middleware a vérifié la requête.
Une requête API d’administration suit un chemin séparé :
- Le middleware authentifie la requête et stocke l’utilisateur résolu dans
Astro.locals. - La route API analyse la requête et vérifie la permission nécessaire pour cette opération.
- La route délègue la logique métier à un gestionnaire ou un dépôt.
- Le gestionnaire exécute les hooks de cycle de vie des plugins autour de l’opération de base de données lorsque cette opération expose des hooks.
- La route retourne une réponse JSON standard de succès ou d’erreur à l’application d’administration.
Fonctionnement interne du panneau d’administration
L’admin est une application monopage React. Astro sert son shell et le middleware d’authentification protège les routes d’administration. À l’intérieur de l’application, TanStack Router gère la navigation, TanStack Query charge l’état du serveur, TanStack Table rend les grilles de données, React Hook Form et Zod gèrent les formulaires, TipTap édite le Portable Text et Kumo fournit le système de design.
Pour l’authentification de session, le middleware redirige une requête de navigateur non authentifiée vers la page de connexion et retourne une erreur JSON pour une requête API non authentifiée. Après avoir chargé un utilisateur actif, il le place sur Astro.locals pour la route :
const sessionUser = await resolveSessionUser(session);
if (!sessionUser?.id) {
if (isApiRoute) {
return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
}
const loginUrl = new URL("/_emdash/admin/login", getPublicOrigin(url, emdash?.config));
loginUrl.searchParams.set("redirect", url.pathname);
return context.redirect(loginUrl.toString());
}
Après cette branche, le middleware charge l’utilisateur, rejette les comptes manquants ou désactivés, place l’utilisateur actif sur Astro.locals et continue vers la route.
UI pilotée par le manifeste
L’admin ne code pas en dur les schémas de collection ou les contributions des plugins. Il récupère GET /_emdash/api/manifest, qui décrit les collections actuelles, champs, plugins, taxonomies, mode d’authentification et autres capacités configurées. Un manifeste abrégé ressemble à ceci :
{
"collections": {
"posts": {
"label": "Blog Posts",
"labelSingular": "Post",
"supports": ["drafts", "revisions", "preview"],
"fields": {
"title": { "kind": "string", "label": "Title", "required": true }
}
}
},
"plugins": {
"audit-log": { "version": "0.2.1", "enabled": true }
},
"taxonomies": [
{ "name": "category", "label": "Categories", "hierarchical": true }
],
"version": "0.37.0"
}
L’admin utilise le manifeste pour construire la navigation des collections et les éditeurs de champs. Comme le endpoint lit le schéma en direct, les changements de collections et de champs apparaissent sans reconstruire l’application d’administration.
UIs admin des plugins
Les points d’entrée admin des plugins configurés sont rassemblés dans virtual:emdash/admin-registry. Le module généré utilise des importations statiques pour que le bundler puisse inclure les composants React :
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
export const pluginAdmins = { seo: pluginAdmin0 };
Conversion de texte riche
Les champs Portable Text utilisent TipTap, qui est basé sur ProseMirror. EmDash convertit le Portable Text en ProseMirror lorsque l’éditeur charge et le reconvertit en Portable Text lorsque l’entrée est enregistrée. Les blocs inconnus provenant de plugins ou d’importations sont préservés comme des espaces réservés en lecture seule au lieu d’être supprimés.
Uploads signés
Les uploads de médias utilisent des URLs signées directes vers le stockage lorsque l’adaptateur de stockage les supporte et un endpoint de streaming same-origin sinon :
- Le client demande une cible d’upload depuis
POST /_emdash/api/media/upload-url. EmDash crée un élément média en attente. - Le client upload vers la cible retournée. Les adaptateurs compatibles S3 peuvent retourner une URL signée qui contourne les limites de taille du corps de l’application ; les bindings natifs R2 et le stockage local retournent un endpoint de streaming EmDash.
- Le client confirme l’upload avec
POST /_emdash/api/media/:id/confirm. - EmDash valide le fichier stocké et marque l’élément média comme prêt.
Extension de l’importateur de contenu
L’importateur WordPress utilise une interface ImportSource extensible. Une source peut sonder une URL, analyser le contenu disponible par rapport au schéma actuel et diffuser des éléments de contenu normalisés :
interface ImportSource {
id: string;
name: string;
description: string;
icon: "upload" | "globe" | "wordpress" | "plug";
requiresFile?: boolean;
canProbe?: boolean;
probe?(url: string): Promise<SourceProbeResult | null>;
analyze(input: SourceInput, context: ImportContext): Promise<ImportAnalysis>;
fetchContent(input: SourceInput, options: FetchOptions): AsyncGenerator<NormalizedItem>;
fetchMedia?(url: string, input: SourceInput): Promise<Blob>;
}
La source WXR importe les fichiers d’exportation WordPress. La source connecteur importe directement depuis des sites avec le plugin EmDash WordPress. Une source REST séparée détecte les sites WordPress publics, mais dirige l’utilisateur vers un export WXR car l’import REST direct n’est pas implémenté. Enregistrez une autre source lorsqu’un importateur peut produire les mêmes formes d’analyse normalisée et d’éléments de contenu.