EmDash pour les développeurs Astro

Sur cette page

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 UIInterface d’édition WYSIWYG complète sur /_emdash/admin
Database storageContenu dans SQLite, libSQL ou Cloudflare D1
Media libraryTéléverser, organiser et servir images et fichiers
Navigation menusGestion des menus par glisser-déposer avec imbrication
Widget areasBarres latérales dynamiques et zones de pied de page
Site settingsConfiguration globale (titre, logo, liens sociaux)
TaxonomiesCatégories, étiquettes et taxonomies personnalisées
Preview systemURL de prévisualisation signées pour les brouillons
RevisionsHistorique 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 CollectionsEmDash Collections
StorageFichiers Markdown/MDX dans src/content/Base SQLite/D1
EditingÉditeur de codeAdmin UI
Content formatMarkdown avec frontmatterPortable Text (JSON structuré)
UpdatesNécessite un rebuildInstantané (SSR)
SchemaZod dans content.config.tsDéfini dans l’admin, stocké en base
Best forContenu géré par les développeursContenu 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.

---
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