EmDash est un CMS conçu spécifiquement pour Astro. Il étend votre site Astro avec du contenu stocké en base de données, une interface d’administration soignée et des fonctionnalités de type WordPress (menus, widgets, taxonomies) tout en préservant l’expérience développeur que vous attendez.
Tout ce que vous savez sur Astro reste valable. EmDash ajoute la gestion de contenu par-dessus votre workflow Astro existant.
Ce qu’EmDash apporte
EmDash fournit les fonctionnalités de gestion de contenu qui manquent aux sites Astro basés sur des fichiers :
| Fonctionnalité | Description |
|---|---|
| Interface admin | Interface d’édition WYSIWYG complète sur /_emdash/admin |
| Stockage en BD | Contenu stocké dans SQLite, libSQL, Cloudflare D1 ou PostgreSQL |
| Médiathèque | Télécharger, organiser et servir des images et fichiers |
| Menus de navigation | Gestion de menus par glisser-déposer avec imbrication |
| Zones de widgets | Barres latérales dynamiques et zones de pied de page |
| Paramètres du site | Configuration globale (titre, logo, liens sociaux) |
| Taxonomies | Catégories, tags et taxonomies personnalisées |
| Système d’aperçu | URLs d’aperçu signées pour le contenu en brouillon |
| Révisions | Historique des versions du contenu |
Astro Collections vs EmDash
Les collections astro:content d’Astro sont basées sur des fichiers et résolues au moment du build. Les collections EmDash sont stockées en base de données et résolues au moment de l’exécution.
| Collections Astro | Collections EmDash | |
|---|---|---|
| Stockage | Fichiers Markdown/MDX dans src/content/ | Base de données SQL (SQLite, libSQL, D1 ou Postgres) |
| Édition | Éditeur de code | Interface admin |
| Format de contenu | Markdown avec frontmatter | Portable Text (JSON structuré) |
| Mises à jour | Nécessite une reconstruction | Instantané (SSR) |
| Schéma | Zod dans content.config.ts | Défini dans l’admin, stocké en BD |
| Idéal pour | Contenu géré par les développeurs | Contenu géré par les rédacteurs |
Utiliser les deux ensemble
Les collections Astro et EmDash peuvent coexister. Utilisez les collections Astro pour le contenu développeur (docs, changelogs) et EmDash pour le contenu éditorial (articles de blog, pages) :
---
import { getCollection } from "astro:content";
import { getEmDashCollection } from "emdash";
// Docs gérés par les développeurs depuis les fichiers
const docs = await getCollection("docs");
// Articles gérés par les rédacteurs depuis la base de données
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
limit: 5,
});
---
Configuration
EmDash nécessite deux fichiers de configuration.
Intégration Astro
La configuration suivante enregistre EmDash comme intégration Astro en mode sortie serveur :
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";
export default defineConfig({
output: "server", // Requis pour EmDash
integrations: [
react(), // Requis — l'interface admin est une app React
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
}),
],
});
Live Collections Loader
Le fichier suivant enregistre EmDash comme source de contenu en direct :
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({
loader: emdashLoader(),
}),
};
La collection _emdash redirige en interne vers vos types de contenu (articles, pages, produits).
Interroger le contenu
EmDash fournit des fonctions de requête qui suivent le modèle des Live Content Collections d’Astro, retournant { entries, error } ou { entry, error } :
EmDash
import { getEmDashCollection, getEmDashEntry } from "emdash";
// Obtenir tous les articles publiés - retourne { entries, error }
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
});
// Obtenir un article par slug - retourne { entry, error, isPreview }
const { entry: post } = await getEmDashEntry("posts", "my-post");
Astro
import { getCollection, getEntry } from "astro:content";
// Obtenir toutes les entrées du blog
const posts = await getCollection("blog");
// Obtenir une entrée par slug
const post = await getEntry("blog", "my-post"); Options de filtrage
getEmDashCollection supporte le filtrage que getCollection d’Astro ne propose pas :
const { entries: posts } = await getEmDashCollection("posts", {
status: "published", // draft | published | archived
limit: 10, // résultats maximum
where: { category: "news" }, // filtre de taxonomie
});
Rendre le contenu
EmDash stocke le texte enrichi sous forme de Portable Text, un format JSON structuré. Rendez-le avec le composant PortableText :
EmDash
---
import { getEmDashEntry } from "emdash";
import { PortableText } from "emdash/ui";
const { slug } = Astro.params;
const { entry: post } = await getEmDashEntry("posts", slug);
if (!post) {
return Astro.redirect("/404");
}
---
<article>
<h1>{post.data.title}</h1>
<PortableText value={post.data.content} />
</article> Astro
---
import { getEntry, render } from "astro:content";
const { slug } = Astro.params;
const post = await getEntry("blog", slug);
const { Content } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article> Fonctionnalités dynamiques
EmDash fournit des APIs pour des fonctionnalités de type WordPress qui n’existent pas dans la couche de contenu d’Astro.
Menus de navigation
Le layout suivant récupère un menu par emplacement et le rend avec des éléments imbriqués :
---
import { getMenu } from "emdash";
const primaryMenu = await getMenu("primary");
---
{primaryMenu && (
<nav>
<ul>
{primaryMenu.items.map(item => (
<li>
<a href={item.url}>{item.label}</a>
{item.children.length > 0 && (
<ul>
{item.children.map(child => (
<li><a href={child.url}>{child.label}</a></li>
))}
</ul>
)}
</li>
))}
</ul>
</nav>
)}
Zones de widgets
Le layout suivant récupère une zone de widgets et rend chaque widget :
---
import { getWidgetArea } from "emdash";
import { PortableText } from "emdash/ui";
const sidebar = await getWidgetArea("sidebar");
---
{sidebar && sidebar.widgets.length > 0 && (
<aside>
{sidebar.widgets.map(widget => (
<div class="widget">
{widget.title && <h3>{widget.title}</h3>}
{widget.type === "content" && widget.content && (
<PortableText value={widget.content} />
)}
</div>
))}
</aside>
)}
Paramètres du site
Le composant suivant lit les paramètres globaux du site et affiche un logo ou un titre :
---
import { getSiteSettings, getSiteSetting } from "emdash";
const settings = await getSiteSettings();
// Ou récupérer des valeurs individuelles :
const title = await getSiteSetting("title");
---
<header>
{settings.logo ? (
<img src={settings.logo.url} alt={settings.title} />
) : (
<span>{settings.title}</span>
)}
{settings.tagline && <p>{settings.tagline}</p>}
</header>
Plugins
Étendez EmDash avec des plugins qui ajoutent des hooks, du stockage, des paramètres et une interface admin :
import react from "@astrojs/react";
import emdash from "emdash/astro";
import seoPlugin from "@emdash-cms/plugin-seo";
export default defineConfig({
integrations: [
react(),
emdash({
// ...
plugins: [seoPlugin({ generateSitemap: true })],
}),
],
});
Créez des plugins personnalisés avec definePlugin :
import { definePlugin } from "emdash";
export default definePlugin({
id: "analytics",
version: "1.0.0",
capabilities: ["content:read"],
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved", { id: event.content.id });
},
},
admin: {
settingsSchema: {
trackingId: { type: "string", label: "ID de suivi" },
},
},
});
Rendu côté serveur
Les sites EmDash fonctionnent en mode SSR, de sorte que le contenu est servi au moment de l’exécution et les modifications apparaissent immédiatement.
Pour les pages statiques avec getStaticPaths, le contenu est récupéré au moment du build :
---
import { getEmDashCollection, getEmDashEntry } from "emdash";
export async function getStaticPaths() {
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
});
return posts.map((post) => ({
params: { slug: post.data.slug },
}));
}
const { slug } = Astro.params;
const { entry: post } = await getEmDashEntry("posts", slug);
---
Pour les pages dynamiques, définissez prerender = false pour récupérer le contenu à chaque requête :
---
export const prerender = false;
import { getEmDashEntry } from "emdash";
const { slug } = Astro.params;
const { entry: post, error } = await getEmDashEntry("posts", slug);
if (error) {
return new Response("Server error", { status: 500 });
}
if (!post) {
return new Response(null, { status: 404 });
}
---
Prochaines étapes
Premiers pas
Créez votre premier site EmDash en moins de 5 minutes.
Interroger le contenu
Découvrez l’API de requêtes en détail.
Créer un blog
Construisez un blog complet avec catégories et tags.
Déployer sur Cloudflare
Mettez votre site en production sur Workers.