EmDash pour les développeurs Astro

Sur cette page

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 adminInterface d’édition WYSIWYG complète sur /_emdash/admin
Stockage en BDContenu stocké dans SQLite, libSQL, Cloudflare D1 ou PostgreSQL
MédiathèqueTélécharger, organiser et servir des images et fichiers
Menus de navigationGestion de menus par glisser-déposer avec imbrication
Zones de widgetsBarres latérales dynamiques et zones de pied de page
Paramètres du siteConfiguration globale (titre, logo, liens sociaux)
TaxonomiesCatégories, tags et taxonomies personnalisées
Système d’aperçuURLs d’aperçu signées pour le contenu en brouillon
RévisionsHistorique 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 AstroCollections EmDash
StockageFichiers Markdown/MDX dans src/content/Base de données SQL (SQLite, libSQL, D1 ou Postgres)
ÉditionÉditeur de codeInterface admin
Format de contenuMarkdown avec frontmatterPortable Text (JSON structuré)
Mises à jourNécessite une reconstructionInstantané (SSR)
SchémaZod dans content.config.tsDéfini dans l’admin, stocké en BD
Idéal pourContenu géré par les développeursContenu 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.

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