EmDash para desarrolladores de Astro

En esta página

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ísticaDescripción
Interfaz de administraciónInterfaz de edición WYSIWYG completa en /_emdash/admin
Almacenamiento en base de datosContenido almacenado en SQLite, libSQL, Cloudflare D1 o PostgreSQL
Biblioteca de mediosCargar, organizar y servir imágenes y archivos
Menús de navegaciónGestión de menús con arrastrar y soltar con anidamiento
Áreas de widgetsBarras laterales dinámicas y regiones de pie de página
Configuración del sitioConfiguración global (título, logo, enlaces sociales)
TaxonomíasCategorías, etiquetas y taxonomías personalizadas
Sistema de vista previaURLs de vista previa firmadas para contenido en borrador
RevisionesHistorial 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 AstroColecciones de EmDash
AlmacenamientoArchivos Markdown/MDX en src/content/Base de datos SQL (SQLite, libSQL, D1 o Postgres)
EdiciónEditor de códigoInterfaz de administración
Formato de contenidoMarkdown con frontmatterPortable Text (JSON estructurado)
ActualizacionesRequiere reconstrucciónInstantáneo (SSR)
EsquemaZod en content.config.tsDefinido en administración, almacenado en base de datos
Mejor paraContenido gestionado por desarrolladoresContenido 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