EmDash es un CMS construido específicamente para Astro. Extiende tu sitio Astro con contenido respaldado por base de datos, una interfaz de administración pulida y funciones al estilo WordPress (menús, widgets, taxonomías) mientras preserva la experiencia de desarrollo que esperas.
Todo lo que sabes sobre Astro sigue siendo válido. EmDash añade gestión de contenido sobre tu flujo de trabajo existente con Astro.
Qué añade EmDash
EmDash proporciona las funciones de gestión de contenido que les faltan a los sitios Astro basados en archivos:
| Característica | Descripción |
|---|---|
| Interfaz de admin | Interfaz de edición WYSIWYG completa en /_emdash/admin |
| Almacenamiento en BD | Contenido almacenado en SQLite, libSQL, Cloudflare D1 o PostgreSQL |
| Biblioteca de medios | Subir, organizar y servir imágenes y archivos |
| Menús de navegación | Gestión de menús con arrastrar y soltar con anidamiento |
| Áreas de widgets | Barras laterales dinámicas y regiones de pie de página |
| Configuración del sitio | Configuración global (título, logo, enlaces sociales) |
| Taxonomías | Categorías, etiquetas y taxonomías personalizadas |
| Sistema de vista previa | URLs de vista previa firmadas para contenido en borrador |
| Revisiones | Historial de versiones del contenido |
Astro Collections vs EmDash
Las colecciones astro:content de Astro están basadas en archivos y se resuelven en tiempo de compilación. Las colecciones de EmDash están respaldadas por base de datos y se resuelven en tiempo de ejecución.
| Colecciones de Astro | Colecciones de EmDash | |
|---|---|---|
| Almacenamiento | Archivos Markdown/MDX en src/content/ | Base de datos SQL (SQLite, libSQL, D1 o Postgres) |
| Edición | Editor de código | Interfaz de admin |
| Formato de contenido | Markdown con frontmatter | Portable Text (JSON estructurado) |
| Actualizaciones | Requiere reconstrucción | Instantáneo (SSR) |
| Esquema | Zod en content.config.ts | Definido en admin, almacenado en BD |
| Ideal para | Contenido gestionado por desarrolladores | Contenido gestionado por editores |
Usar ambos juntos
Las colecciones de Astro y EmDash pueden coexistir. Usa las colecciones de Astro para contenido de desarrolladores (docs, changelogs) y EmDash para contenido editorial (publicaciones de blog, páginas):
---
import { getCollection } from "astro:content";
import { getEmDashCollection } from "emdash";
// Docs gestionados por desarrolladores desde archivos
const docs = await getCollection("docs");
// Publicaciones gestionadas por editores desde la base de datos
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
limit: 5,
});
---
Configuración
EmDash requiere dos archivos de configuración.
Integración con Astro
La siguiente configuración registra EmDash como una integración de Astro en modo de salida de servidor:
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", // Requerido para EmDash
integrations: [
react(), // Requerido — la interfaz de admin es una app React
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
}),
],
});
Live Collections Loader
El siguiente archivo registra EmDash como fuente de contenido en vivo:
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({
loader: emdashLoader(),
}),
};
La colección _emdash redirige internamente a tus tipos de contenido (publicaciones, páginas, productos).
Consultar contenido
EmDash proporciona 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";
// Obtener todas las publicaciones publicadas - devuelve { entries, error }
const { entries: posts } = await getEmDashCollection("posts", {
status: "published",
});
// Obtener una publicación por slug - devuelve { entry, error, isPreview }
const { entry: post } = await getEmDashEntry("posts", "my-post");
Astro
import { getCollection, getEntry } from "astro:content";
// Obtener todas las entradas del blog
const posts = await getCollection("blog");
// Obtener una entrada por slug
const post = await getEntry("blog", "my-post"); Opciones de filtrado
getEmDashCollection soporta filtrado que getCollection de Astro no ofrece:
const { entries: posts } = await getEmDashCollection("posts", {
status: "published", // draft | published | archived
limit: 10, // resultados máximos
where: { category: "news" }, // filtro de taxonomía
});
Renderizar contenido
EmDash almacena 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
El siguiente layout obtiene un menú por ubicación y lo renderiza con elementos anidados:
---
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
El siguiente layout obtiene un área de widgets y renderiza cada 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>
)}
Configuración del sitio
El siguiente componente lee la configuración global del sitio y renderiza un logo o título:
---
import { getSiteSettings, getSiteSetting } from "emdash";
const settings = await getSiteSettings();
// O consultar valores individuales:
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, configuración e interfaz de 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 })],
}),
],
});
Crea plugins personalizados con 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 seguimiento" },
},
},
});
Renderizado en servidor
Los sitios EmDash se ejecutan en modo SSR, por lo que el contenido se sirve en tiempo de ejecución y los cambios aparecen inmediatamente.
Para páginas estáticas con getStaticPaths, el contenido se obtiene en tiempo de compilación:
---
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, establece prerender = false para obtener contenido en cada solicitud:
---
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
Primeros pasos
Crea tu primer sitio EmDash en menos de 5 minutos.
Consultar contenido
Aprende la API de consultas en detalle.
Crear un blog
Construye un blog completo con categorías y etiquetas.
Desplegar en Cloudflare
Lleva tu sitio a producción en Workers.