EmDash para desarrolladores de Astro

En esta página

EmDash es un CMS creado específicamente para Astro: no un CMS headless genérico con un adaptador de Astro. Extiende tu sitio Astro con contenido respaldado por base de datos, una admin UI pulida y funciones al estilo WordPress (menús, widgets, taxonomías), conservando la experiencia de desarrollo que esperas.

Todo lo que sabes sobre Astro sigue aplicándose. EmDash mejora tu sitio; no sustituye tu flujo de trabajo.

Qué aporta EmDash

EmDash ofrece las funciones de gestión de contenido que les faltan a los sitios Astro basados solo en archivos:

FunciónDescripción
Admin UIInterfaz de edición WYSIWYG completa en /_emdash/admin
Database storageContenido en SQLite, libSQL o Cloudflare D1
Media librarySubir, organizar y servir imágenes y archivos
Navigation menusGestión de menús con arrastrar y soltar y anidación
Widget areasBarras laterales dinámicas y regiones de pie de página
Site settingsConfiguración global (título, logo, enlaces sociales)
TaxonomiesCategorías, etiquetas y taxonomías personalizadas
Preview systemURL de vista previa firmadas para borradores
RevisionsHistorial de versiones del contenido

Astro Collections vs EmDash

Las colecciones astro:content de Astro son basadas en archivos y se resuelven en tiempo de build. Las colecciones EmDash están respaldadas por base de datos y se resuelven en tiempo de ejecución.

Astro CollectionsEmDash Collections
StorageArchivos Markdown/MDX en src/content/Base de datos SQLite/D1
EditingEditor de códigoAdmin UI
Content formatMarkdown con frontmatterPortable Text (JSON estructurado)
UpdatesRequiere rebuildInstantáneo (SSR)
SchemaZod en content.config.tsDefinido en admin, guardado en base de datos
Best forContenido gestionado por desarrolladoresContenido gestionado por editores

Usar ambos a la vez

Las colecciones Astro y EmDash pueden convivir. Usa colecciones Astro para contenido de desarrolladores (docs, changelogs) y EmDash para contenido editorial (entradas de blog, páginas):

---
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,
});
---

Configuración

EmDash requiere dos archivos de configuración.

Integración 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(),
	}),
};

Esto registra EmDash como fuente de contenido en vivo. La colección _emdash enruta internamente a tus tipos de contenido (posts, pages, products).

Consultar contenido

EmDash ofrece funciones de consulta que siguen el patrón de live content collections de Astro, devolviendo { entries, error } o { 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");

Opciones de filtrado

getEmDashCollection admite filtros que getCollection de Astro no tiene:

const { entries: posts } = await getEmDashCollection("posts", {
	status: "published", // draft | published | archived
	limit: 10, // max results
	where: { category: "news" }, // taxonomy filter
});

Renderizar contenido

EmDash guarda texto enriquecido como Portable Text, un formato JSON estructurado. Renderízalo con el componente 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>

Funciones dinámicas

EmDash proporciona APIs para funciones al estilo WordPress que no existen en la capa de contenido de Astro.

Menús de navegación

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

Áreas 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>
)}

Ajustes del sitio

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

Extiende EmDash con plugins que añaden hooks, almacenamiento, ajustes y admin UI:

import emdash from "emdash/astro";
import seoPlugin from "@emdash-cms/plugin-seo";

export default defineConfig({
	integrations: [
		emdash({
			// ...
			plugins: [seoPlugin({ generateSitemap: true })],
		}),
	],
});

Crea plugins personalizados con 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

Los sitios EmDash usan modo SSR. Los cambios de contenido aparecen al instante sin rebuilds.

Para páginas estáticas con getStaticPaths, el contenido se obtiene en tiempo de 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);
---

Para páginas dinámicas, define prerender = false para cargar contenido en cada petición:

---
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 });
}
---

Próximos pasos