Communitygithub.com

EmDash CMS

Compétence d'agent de codage IA pour construire avec EmDash, le CMS TypeScript full-stack basé sur Astro et Cloudflare.

Qu'est-ce que EmDash CMS ?

EmDash CMS est un système de gestion de contenu moderne, full-stack en TypeScript, construit sur Astro et Cloudflare. Il se positionne comme le successeur spirituel de WordPress, offrant une extensibilité via un système de plugins exécutés dans des Workers isolés et sandboxés, sans accès direct au système de fichiers ou à la base de données. Cette compétence d'agent IA permet de générer rapidement un nouveau projet EmDash, d'ajouter EmDash à un projet Astro existant, et d'utiliser des commandes CLI clés pour la gestion de contenu (migrations, typage, seed). Elle prend en charge les déploiements sur Cloudflare (D1, R2, KV) ou tout serveur Node.js avec SQLite, et offre une interface d'administration sans code pour la création de schémas de contenu. Le contenu riche est stocké au format Portable Text (JSON structuré) plutôt qu'en HTML, et l'authentification est basée sur les clés d'accès (WebAuthn) avec des options OAuth et de lien magique en secours. Les plugins peuvent étendre les fonctionnalités via des hooks, des blocs personnalisés, des routes API et des pages d'administration, le tout déclaré avec un manifeste de capacités. La migration depuis WordPress est facilitée par des commandes intégrées, et un serveur MCP (Model Context Protocol) permet une intégration directe avec les outils d'IA.

Compatible avec✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/reason-machines/trending-skills/tree/HEAD/skills/emdash-cms

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Aperçu

Depuis le README du skill

EmDash CMS preview

Documentation

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:contentLire le contenu publié
write:contentCréer et mettre à jour du contenu
read:usersLire les profils utilisateurs
email:sendEnvoyer un e-mail via le fournisseur configuré
kv:readLire depuis l'espace de noms KV du plugin
kv:writeÉcrire dans l'espace de noms KV du plugin
http:fetchEffectuer des requêtes HTTP sortantes
storage:readLire 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 courte
  • richText — Portable Text (éditeur TipTap)
  • number — entier ou flottant
  • boolean — bascule vrai/faux
  • date / datetime — sélecteurs de date
  • select — liste déroulante à choix unique
  • multiSelect — choix multiples
  • image — sélecteur de bibliothèque multimédia
  • file — pièce jointe
  • relation — lien vers une autre entrée de collection
  • slug — chaîne sécurisée pour les URL, générée automatiquement à partir d'un champ source
  • json — JSON brut

Serveur MCP (intégration d'outils IA)

EmDash inclut un serveur MCP intégré

…

[truncated for translation]

Skills associés