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 de 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 agrega gestión de contenido sobre tu flujo de trabajo existente de Astro.
Qué agrega EmDash
EmDash proporciona las funciones de gestión de contenido que carecen los sitios Astro basados en archivos:
| Característica | Descripción |
|---|---|
| Interfaz de administración | Interfaz de edición WYSIWYG completa en /_emdash/admin |
| Almacenamiento en base de datos | Contenido almacenado en SQLite, libSQL, Cloudflare D1 o PostgreSQL |
| Biblioteca de medios | Cargar, 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 |
Colecciones de Astro 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 administración |
| 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 administración, almacenado en base de datos |
| Mejor para | Contenido gestionado por desarrolladores | Contenido gestionado por editores |
Usar ambos juntos
Las colecciones de Astro y EmDash pueden coexistir. Usa colecciones de Astro para contenido de desarrolladores (docs, registros de cambios) y EmDash para contenido de editores (publicaciones 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 de 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 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",
}),
}),
],
});
Cargador de colecciones en vivo
El siguiente archivo registra EmDash como una 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 enruta internamente a tus tipos de contenido (posts, páginas, productos).
Consultar contenido
EmDash proporciona funciones de consulta que siguen el patrón de colecciones de contenido en vivo 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 filtrado que getCollection de Astro no ofrece:
const { entries: posts } = await getEmDashCollection("posts", {
status: "published", // draft | published | archived
limit: 10, // max results
where: { category: "news" }, // taxonomy filter
});
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> Características dinámicas
EmDash proporciona APIs para características al estilo de WordPress que no existen en la capa de contenido de Astro.
Menús de navegación
El siguiente diseño 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 diseño 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();
// 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 agregan hooks, almacenamiento, configuraciones e interfaz de administración:
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: ["content:read"],
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved", { id: event.content.id });
},
},
admin: {
settingsSchema: {
trackingId: { type: "string", label: "Tracking ID" },
},
},
});
Renderizado de 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
Getting Started
Crea tu primer sitio EmDash en menos de 5 minutos.
Querying Content
Aprende la API de consulta en detalle.
Create a Blog
Construye un blog completo con categorías y etiquetas.
Deploy to Cloudflare
Lleva tu sitio a producción en Workers.