Primeros pasos
Crea tu primer sitio con EmDash en menos de 5 minutos.
EmDash es un CMS creado específicamente para Astro, no un headless CMS genérico con un adaptador añadido después. Amplía tu sitio con contenido respaldado por base de datos, una interfaz de administración pulida y funciones al estilo de WordPress, sin romper el flujo de trabajo de Astro.
Todo lo que sabes sobre Astro sigue aplicándose. EmDash mejora tu sitio; no reemplaza tu flujo de trabajo.
EmDash proporciona las características de gestión de contenido que carecen los sitios de 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 o Cloudflare D1 |
| 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 y anidación |
| Áreas de widgets | Barras laterales y regiones de pie de página dinámicas |
| Configuración del sitio | Configuración global (título, logotipo, 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 |
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 SQLite/D1 |
| 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áneas (SSR) |
| Esquema | Zod en content.config.ts | Definido en la administración, almacenado en la base de datos |
| Ideal para | Contenido gestionado por desarrolladores | Contenido gestionado por editores |
Las colecciones de Astro y EmDash pueden coexistir. Usa las colecciones de Astro para contenido de desarrolladores (documentación, 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 filesconst docs = await getCollection("docs");
// Editor-managed posts from databaseconst { entries: posts } = await getEmDashCollection("posts", { status: "published", limit: 5,});---EmDash requiere dos archivos de configuración.
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", }), }), ],});import { defineLiveCollection } from "astro:content";import { emdashLoader } from "emdash/runtime";
export const collections = { _emdash: defineLiveCollection({ loader: emdashLoader(), }),};Esto registra a EmDash como una fuente de contenido en vivo. La colección _emdash enruta internamente a tus tipos de contenido (publicaciones, páginas, productos).
EmDash proporciona funciones de consulta que siguen el patrón de colecciones de contenido en vivo de Astro, devolviendo { entries, error } o { entry, error }:
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");import { getCollection, getEntry } from "astro:content";
// Get all blog entriesconst posts = await getCollection("blog");
// Get a single entry by slugconst post = await getEntry("blog", "my-post");getEmDashCollection admite filtrado que getCollection de Astro no admite:
const { entries: posts } = await getEmDashCollection("posts", { status: "published", // draft | published | archived limit: 10, // max results where: { category: "news" }, // taxonomy filter});EmDash almacena texto enriquecido como Portable Text, un formato JSON estructurado. Renderízalo con el componente PortableText:
---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>---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>EmDash proporciona APIs para características al estilo de WordPress que no existen en la capa de contenido de 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>)}---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>)}---import { getSiteSettings, getSiteSetting } from "emdash";
const settings = await getSiteSettings();// O recuperar 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>Extiende EmDash con plugins que añaden 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: ["read:content"],
hooks: { "content:afterSave": async (event, ctx) => { ctx.log.info("Contenido guardado", { id: event.content.id }); }, },
admin: { settingsSchema: { trackingId: { type: "string", label: "Tracking ID" }, }, },});Los sitios de EmDash se ejecutan en modo SSR. Los cambios en el contenido aparecen inmediatamente sin reconstrucciones.
Para páginas estáticas con getStaticPaths, el contenido se obtiene en el momento de la construcció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 el 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("Error del servidor", { status: 500 });}
if (!post) { return new Response(null, { status: 404 });}---Primeros pasos
Crea tu primer sitio con 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.