EmDash est un CMS conçu spécifiquement pour Astro—pas un CMS headless générique avec un adaptateur Astro. Il enrichit votre site Astro avec du contenu stocké en base de données, une admin UI soignée et des fonctionnalités façon 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 améliore votre site ; il ne remplace pas votre flux de travail.
Ce qu’EmDash ajoute
EmDash fournit les fonctionnalités de gestion de contenu que les sites Astro purement fichiers n’ont pas :
| Fonctionnalité | Description |
|---|---|
| Admin UI | Interface d’édition WYSIWYG complète sur /_emdash/admin |
| Database storage | Contenu dans SQLite, libSQL ou Cloudflare D1 |
| Media library | Téléverser, organiser et servir images et fichiers |
| Navigation menus | Gestion des menus par glisser-déposer avec imbrication |
| Widget areas | Barres latérales dynamiques et zones de pied de page |
| Site settings | Configuration globale (titre, logo, liens sociaux) |
| Taxonomies | Catégories, étiquettes et taxonomies personnalisées |
| Preview system | URL de prévisualisation signées pour les brouillons |
| Revisions | 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 et résolues à l’exécution.
| Astro Collections | EmDash Collections | |
|---|---|---|
| Storage | Fichiers Markdown/MDX dans src/content/ | Base SQLite/D1 |
| Editing | Éditeur de code | Admin UI |
| Content format | Markdown avec frontmatter | Portable Text (JSON structuré) |
| Updates | Nécessite un rebuild | Instantané (SSR) |
| Schema | Zod dans content.config.ts | Défini dans l’admin, stocké en base |
| Best for | Contenu géré par les développeurs | Contenu géré par les éditeurs |
Utiliser les deux ensemble
Les collections Astro et EmDash peuvent coexister. Utilisez 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";
// Developer-managed docs from files
const docs = await getCollection("docs");
// Editor-managed posts from database
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
limit: 5,
});
---
Configuration
EmDash nécessite deux fichiers de configuration.
Intégration Astro
import { defineConfig } from "astro/config";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";
export default defineConfig({
output: "server", // Required for EmDash
integrations: [
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
}),
],
});
Live Collections Loader
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({
loader: emdashLoader(),
}),
};
Cela enregistre EmDash comme source de contenu live. La collection _emdash achemine en interne vers vos types de contenu (posts, pages, products).
Interroger le contenu
EmDash fournit des fonctions de requête qui suivent le modèle des live content collections d’Astro, en retournant { entries, error } ou { entry, error } :
EmDash
import { getEmDashCollection, getEmDashEntry } from "emdash";
// Get all published posts - returns { entries, error }
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
});
// Get a single post by slug - returns { entry, error, isPreview }
const { entry: post } = await getEmDashEntry("posts", "my-post");
Astro
import { getCollection, getEntry } from "astro:content";
// Get all blog entries
const posts = await getCollection("blog");
// Get a single entry by slug
const post = await getEntry("blog", "my-post"); Options de filtrage
getEmDashCollection prend en charge des filtres absents de getCollection d’Astro :
const { entries: posts } = await getEmDashCollection("posts", {
status: "published", // draft | published | archived
limit: 10, // max results
where: { category: "news" }, // taxonomy filter
});
Rendu du contenu
EmDash stocke le texte enrichi au format Portable Text, un 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 expose des APIs pour des fonctionnalités façon WordPress absentes de la couche contenu d’Astro.
Menus de navigation
---
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
---
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>
)}
Réglages du site
---
import { getSiteSettings, getSiteSetting } from "emdash";
const settings = await getSiteSettings();
// Or fetch individual values:
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 réglages et de l’admin UI :
import emdash from "emdash/astro";
import seoPlugin from "@emdash-cms/plugin-seo";
export default defineConfig({
integrations: [
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: ["read:content"],
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved", { id: event.content.id });
},
},
admin: {
settingsSchema: {
trackingId: { type: "string", label: "Tracking ID" },
},
},
});
Server Rendering
Les sites EmDash tournent en mode SSR. Les changements de contenu apparaissent immédiatement sans rebuilds.
Pour les pages statiques avec getStaticPaths, le contenu est récupéré au 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 charger 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 });
}
---
Étapes suivantes
Getting Started
Créez votre premier site EmDash en moins de 5 minutes.
Querying Content
Maîtrisez l’API de requêtes en détail.
Create a Blog
Construisez un blog complet avec catégories et étiquettes.
Deploy to Cloudflare
Mettez votre site en production sur Workers.