Que fait EmDash CMS ?
Compétence par ara.so — Collection Daily 2026 Skills.
EmDash est un CMS TypeScript full-stack construit sur Astro et Cloudflare. C'est le successeur spirituel de WordPress : extensible, convivial pour les développeurs et alimenté par un système de plugins qui exécute les plugins dans des isolates Worker sandboxés plutôt qu'avec un accès complet au système de fichiers/à la base de données. EmDash stocke le texte enrichi en Portable Text (JSON structuré) plutôt qu'en HTML, prend en charge l'authentification prioritaire par clé d'accès et fonctionne sur Cloudflare (D1 + R2 + Workers) ou sur tout serveur Node.js avec SQLite.
Installation
Créer un nouveau projet
npm create emdash@latest
Suivez les invites pour choisir un modèle (blog, marketing, portfolio, starter, vide) et une plateforme (Cloudflare ou Node.js/SQLite).
Déployer directement sur Cloudflare
Utilisez le bouton de déploiement en un clic depuis le README, ou :
npm create emdash@latest -- --template blog-cloudflare
cd my-site
npm run deploy
Ajouter EmDash à un projet Astro existant
npm install emdash
// astro.config.mjs
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { d1 } from "emdash/db";
export default defineConfig({
integrations: [
emdash({
database: d1(), // Cloudflare D1
}),
],
});
Pour Node.js + SQLite (aucun compte Cloudflare requis) :
// astro.config.mjs
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { sqlite } from "emdash/db";
export default defineConfig({
integrations: [
emdash({
database: sqlite({ path: "./content.db" }),
}),
],
});
Commandes CLI clés
# Créer un nouveau projet EmDash
npm create emdash@latest
# Générer les types TypeScript à partir de votre schéma en direct
npx emdash types
# Alimenter le site de démonstration avec du contenu d'exemple
npx emdash seed
# Exécuter les migrations de base de données
npx emdash migrate
# Démarrer le serveur de développement (commande Astro standard)
npx astro dev
# Construire pour la production
npx astro build
# Ouvrir le panneau d'administration (après le démarrage du serveur de développement)
open http://localhost:4321/_emdash/admin
Commandes pour monorepo / contributeurs
pnpm install
pnpm build
pnpm test # exécuter tous les tests
pnpm typecheck # vérification TypeScript
pnpm lint:quick # lint rapide (< 1s)
pnpm format # formater avec oxfmt
# Exécuter la démo (Node.js + SQLite, aucun Cloudflare requis)
pnpm --filter emdash-demo seed
pnpm --filter emdash-demo dev
Configuration
Cloudflare (D1 + R2 + KV + Worker Loaders)
// wrangler.jsonc
{
"name": "my-emdash-site",
"compatibility_date": "2025-01-01",
"d1_databases": [
{
"binding": "DB",
"database_name": "emdash-content",
"database_id": "$DATABASE_ID"
}
],
"r2_buckets": [
{
"binding": "MEDIA",
"bucket_name": "emdash-media"
}
],
"kv_namespaces": [
{
"binding": "SESSIONS",
"id": "$KV_NAMESPACE_ID"
}
],
// Supprimer ce bloc pour désactiver les plugins sandboxés (comptes gratuits)
"worker_loaders": [
{
"binding": "PLUGIN_LOADER"
}
]
}
// astro.config.mjs
import emdash from "emdash/astro";
import { d1 } from "emdash/db";
import { r2 } from "emdash/storage";
import { kv } from "emdash/sessions";
export default defineConfig({
integrations: [
emdash({
database: d1({ binding: "DB" }),
storage: r2({ binding: "MEDIA" }),
sessions: kv({ binding: "SESSIONS" }),
}),
],
});
Node.js + SQLite
// astro.config.mjs
import emdash from "emdash/astro";
import { sqlite } from "emdash/db";
import { localFiles } from "emdash/storage";
export default defineConfig({
integrations: [
emdash({
database: sqlite({ path: "./content.db" }),
storage: localFiles({ dir: "./public/uploads" }),
}),
],
});
PostgreSQL / Turso
import { postgres } from "emdash/db";
import { turso } from "emdash/db";
// PostgreSQL
database: postgres({ url: process.env.DATABASE_URL })
// Turso/libSQL
database: turso({
url: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
})
Interrogation du contenu
Les types de contenu sont définis dans l'interface d'administration (aucun code requis). Après avoir créé une collection, générez les types :
npx emdash types
Cela écrit les définitions de type dans src/emdash.d.ts.
Récupérer une collection dans une page Astro
---
// src/pages/blog/index.astro
import { getEmDashCollection } from "emdash";
const { entries: posts } = await getEmDashCollection("posts", {
filter: { status: "published" },
sort: { field: "publishedAt", direction: "desc" },
limit: 10,
});
---
<ul>
{posts.map((post) => (
<li>
<a href={`/blog/${post.data.slug}`}>{post.data.title}</a>
<time datetime={post.data.publishedAt}>{post.data.publishedAt}</time>
</li>
))}
</ul>
Récupérer une entrée unique
---
// src/pages/blog/[slug].astro
import { getEmDashEntry, renderPortableText } from "emdash";
const { slug } = Astro.params;
const post = await getEmDashEntry("posts", { slug });
if (!post) return Astro.redirect("/404");
const { Content } = await renderPortableText(post.data.body);
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
Pagination
---
import { getEmDashCollection } from "emdash";
const page = Number(Astro.params.page ?? 1);
const { entries: posts, total } = await getEmDashCollection("posts", {
filter: { status: "published" },
sort: { field: "publishedAt", direction: "desc" },
limit: 10,
offset: (page - 1) * 10,
});
---
Filtrage par taxonomie
---
import { getEmDashCollection } from "emdash";
const { entries: posts } = await getEmDashCollection("posts", {
filter: {
status: "published",
tags: { contains: "typescript" },
},
});
---
Rendu Portable Text
EmDash stocke le texte enrichi en Portable Text (JSON structuré), pas en HTML.
---
import { renderPortableText } from "emdash";
const post = await getEmDashEntry("posts", { slug: Astro.params.slug });
const { Content } = await renderPortableText(post.data.body);
---
<Content />
Rendu de blocs personnalisés
// src/portable-text.ts
import { definePortableTextComponents } from "emdash/blocks";
export const components = definePortableTextComponents({
types: {
callout: ({ value }) => `
<div class="callout callout--${value.type}">
${value.text}
</div>
`,
image: ({ value }) => `
<figure>
<img src="${value.url}" alt="${value.alt ?? ""}" />
${value.caption ? `<figcaption>${value.caption}</figcaption>` : ""}
</figure>
`,
},
marks: {
highlight: ({ children }) => `<mark>${children}</mark>`,
},
});
---
import { renderPortableText } from "emdash";
import { components } from "../portable-text";
const { Content } = await renderPortableText(post.data.body, { components });
---
<Content />
Développement de plugins
Les plugins sont le mécanisme d'extension principal. Sur Cloudflare, ils s'exécutent dans des isolates Worker sandboxés avec un manifeste de capacités déclaré. Sur Node.js, ils s'exécutent en processus (mode sécurisé).
Plugin minimal
// plugins/my-plugin/index.ts
import { definePlugin } from "emdash/plugin";
export default () =>
definePlugin({
id: "my-plugin",
name: "My Plugin",
version: "1.0.0",
capabilities: [],
hooks: {},
});
Plugin avec hooks de contenu et email
import { definePlugin } from "emdash/plugin";
export default () =>
definePlugin({
id: "notify-on-publish",
name: "Notify on Publish",
version: "1.0.0",
capabilities: ["read:content", "email:send"],
hooks: {
"content:afterSave": async (event, ctx) => {
if (event.content.status !== "published") return;
await ctx.email.send({
to: "[email protected]",
subject: `Nouvelle publication : ${event.content.title}`,
text: `"${event.content.title}" est maintenant en ligne.`,
});
},
},
});
Plugin avec stockage KV et paramètres d'administration
import { definePlugin } from "emdash/plugin";
export default () =>
definePlugin({
id: "view-counter",
name: "View Counter",
version: "1.0.0",
capabilities: ["read:content", "kv:read", "kv:write"],
settings: {
schema: {
resetDaily: { type: "boolean", default: false, label: "Réinitialiser les compteurs quotidiennement" },
},
},
hooks: {
"content:beforeRender": async (event, ctx) => {
const key = `views:${event.content.id}`;
const current = Number(await ctx.kv.get(key) ?? 0);
await ctx.kv.set(key, String(current + 1));
event.content.meta.views = current + 1;
},
},
adminPages: [
{
path: "/analytics",
title: "Analytiques des vues",
component: "ViewAnalytics",
},
],
});
Plugin avec type de bloc personnalisé
import { definePlugin } from "emdash/plugin";
import { defineBlock } from "emdash/blocks";
export default () =>
definePlugin({
id: "callout-block",
name: "Bloc d'appel",
version: "1.0.0",
capabilities: [],
blocks: [
defineBlock({
name: "callout",
title: "Appel",
fields: [
{ name: "type", type: "select", options: ["info", "warning", "danger"], default: "info" },
{ name: "text", type: "text", label: "Message" },
],
}),
],
hooks: {},
});
Plugin avec route API personnalisée
import { definePlugin } from "emdash/plugin";
export default () =>
definePlugin({
id: "newsletter",
name: "Newsletter",
version: "1.0.0",
capabilities: ["kv:write"],
apiRoutes: [
{
method: "POST",
path: "/subscribe",
handler: async (request, ctx) => {
const { email } = await request.json();
await ctx.kv.set(`subscriber:${email}`, "true");
return new Response(JSON.stringify({ ok: true }), {
headers: { "Content-Type": "application/json" },
});
},
},
],
hooks: {},
});
Capacités disponibles
| Capacité | Ce qu'elle permet |
|---|---|
read:content | Lire le contenu publié |
write:content | Créer et mettre à jour du contenu |
read:users | Lire les profils utilisateurs |
email:send | Envoyer un e-mail via le fournisseur configuré |
kv:read | Lire depuis l'espace de noms KV du plugin |
kv:write | Écrire dans l'espace de noms KV du plugin |
http:fetch | Effectuer des requêtes HTTP sortantes |
storage:read | Lire depuis le stockage multimédia |
storage:write | Écrire dans le stockage multimédia |
Hooks disponibles
// Cycle de vie du contenu
"content:beforeSave"
"content:afterSave"
"content:beforeDelete"
"content:afterDelete"
"content:beforeRender"
// Cycle de vie de l'authentification
"auth:afterLogin"
"auth:afterLogout"
"auth:afterRegister"
// Cycle de vie des médias
"media:beforeUpload"
"media:afterUpload"
"media:beforeDelete"
// Cycle de vie de l'administration
"admin:init"
Authentification
EmDash utilise une authentification prioritaire par clé d'accès (WebAuthn) avec des solutions de secours OAuth et lien magique.
Configurer les fournisseurs OAuth
// astro.config.mjs
import emdash from "emdash/astro";
import { github, google } from "emdash/auth";
export default defineConfig({
integrations: [
emdash({
database: d1(),
auth: {
providers: [
github({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
}),
google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
},
}),
],
});
Configurer l'e-mail de lien magique
auth: {
providers: [
magicLink({
from: "[email protected]",
// utilise l'adaptateur e-mail configuré
}),
],
}
Protéger une page
---
// src/pages/dashboard.astro
import { requireAuth } from "emdash/auth";
const user = await requireAuth(Astro);
// Redirige vers /login si non authentifié
---
<p>Bienvenue, {user.name} !</p>
Obtenir l'utilisateur actuel (optionnel)
---
import { getUser } from "emdash/auth";
const user = await getUser(Astro); // null si non connecté
---
{user ? <p>Bonjour {user.name}</p> : <a href="/login">Connexion</a>}
Rôles
import { requireRole } from "emdash/auth";
// Dans une route API ou une page
const user = await requireRole(Astro, "editor");
// Rôles : "administrator" | "editor" | "author" | "contributor"
Migration depuis WordPress
Importer depuis un fichier d'exportation WXR
npx emdash import:wordpress ./export.xml
Importer depuis l'API REST WordPress
npx emdash import:wordpress --url https://yoursite.wordpress.com --api-key $WP_API_KEY
Importer depuis WordPress.com
npx emdash import:wordpress --wpcom --site yoursite.wordpress.com
L'outil d'importation migre les articles, pages, pièces jointes multimédia, catégories, tags, auteurs et commentaires. Les blocs Gutenberg sont convertis en Portable Text via le package gutenberg-to-portable-text.
Schéma de contenu (interface d'administration)
Les types de contenu sont créés dans le panneau d'administration à /_emdash/admin — pas dans le code. Après avoir créé ou modifié une collection, régénérez les types TypeScript :
npx emdash types
# Écrit dans src/emdash.d.ts
Types de champs disponibles dans le constructeur de schéma
text— chaîne courterichText— Portable Text (éditeur TipTap)number— entier ou flottantboolean— bascule vrai/fauxdate/datetime— sélecteurs de dateselect— liste déroulante à choix uniquemultiSelect— choix multiplesimage— sélecteur de bibliothèque multimédiafile— pièce jointerelation— lien vers une autre entrée de collectionslug— chaîne sécurisée pour les URL, générée automatiquement à partir d'un champ sourcejson— JSON brut
Serveur MCP (intégration d'outils IA)
EmDash inclut un serveur MCP intégré
…
[truncated for translation]