Tutorial de EmDash CMS: Crea tu Primer Sitio Paso a Paso
Tutorial de EmDash CMS paso a paso: instala, genera el proyecto y configura tu primer sitio con el panel de administración, passkeys y tu primera query getEmDashCollection.
39 min de lectura

EmDash CMS
Part 2 of 6
Este tutorial de EmDash CMS te lleva de un terminal en blanco a un post publicado: instala los requisitos, genera un proyecto con npm create emdash, recorre el asistente de configuración y escribe tu primera query getEmDashCollection. Empezar lleva de 15 a 30 minutos y no cuesta nada. Todo corre en tu máquina.
EmDash es un CMS full-stack en TypeScript construido sobre Astro, publicado por Cloudflare bajo MIT. [email protected] es la versión con la que se escribió esta guía (finales de septiembre de 2026; ejecuta npm view emdash version antes de empezar, el proyecto avanza rápido). Un solo despliegue te da el sitio, el panel de administración en /_emdash/admin, una REST API, auth con passkeys y una biblioteca de medios. Si aún estás decidiendo si dejar WordPress, lee primero los costes reales y los compromisos operativos de Astro vs WordPress. Para ver cómo se sitúa EmDash frente a otras opciones nativas de Astro, consulta cómo se compara EmDash con otros headless CMS para Astro.
Sin cuenta de Cloudflare. Sin Docker. Sin servidor de base de datos que vigilar. Solo Node.js y SQLite en tu portátil.
Qué vas a construir en este tutorial de EmDash CMS
Estado final: un sitio Astro local con el panel de administración de EmDash integrado, contenido de ejemplo editado y publicado, y una query getEmDashCollection("posts") funcionando en la página de inicio. Entenderás qué hace cada archivo, para que nada del proyecto generado te parezca magia.
Esta es la Parte 2 de una serie. Nuestra reseña de EmDash CMS tras seis meses cubre qué cambió en la 1.0 y para quién es realmente el CMS. Este tutorial se queda en la ruta local sin cuenta cloud (Node.js + SQLite + uploads locales). El despliegue viene después: Cloudflare Workers con D1 y R2 en la Parte 3, y un VPS con Docker en la Parte 4.
Coste de este tutorial: 0 $. Sin cuenta cloud, sin tier de pago, sin periodo de prueba. El estado vive en tres rutas locales: data.db, uploads/ y .env. Borra la carpeta y vuelves a cero.
Prerequisitos: qué necesitas para instalar EmDash CMS
Antes de instalar las herramientas de EmDash CMS, marca esta lista. Los requisitos son pocos, pero uno de ellos es un suelo innegociable. (La documentación oficial de primeros pasos lista el mismo suelo; esta guía añade los prompts, el tour del admin y los modos de fallo.)
- Node.js v22.16.0 o superior (requisito obligatorio)
- npm 10+ (o Bun 1.x; ambas rutas se muestran abajo)
- Un navegador compatible con passkeys: Chrome, Edge, Safari o Firefox
- ~15-30 minutos y ~500 MB de disco libre para
node_modules - Sin cuenta cloud (SQLite más archivos locales son suficientes)
El paquete emdash ocupa unos 23 MB descomprimido en unos 2.500 archivos, así que la primera instalación de dependencias es más lenta que en un proyecto Astro típico. Es lo esperado. No necesitas cuenta de Cloudflare, daemon de Docker, Postgres ni Redis. El puerto 4321 debería estar libre (Astro elige otro puerto si no lo está; más sobre esto en el Paso 2).
Si ya usas pnpm o yarn, el scaffolder también los soporta (pnpm create emdash@latest, yarn create emdash). El resto de la guía usa npm, con el equivalente en bun al lado.
Comprueba tu versión de Node.js (22.16+)
node --versionDebe imprimir v22.16.0 o posterior. En una distro con un Node por defecto más antiguo, no pelees con el gestor de paquetes; usa un gestor de versiones:
nvm install 22 && nvm use 22 # or: fnm install 22Verifica: ejecuta node --version de nuevo. Si ves v22.15.0 o cualquier cosa por debajo de v22.16.0, tanto el scaffolder como el servidor de desarrollo se quejarán después. Arréglalo ahora.
Paso 1: genera un proyecto nuevo con npm create emdash
El scaffolder es create-emdash, y cualquier gestor de paquetes puede ejecutarlo. Yo usé npm para esta pasada, así que esa es la ruta por defecto aquí.
npm
npm create emdash@latestBun
bun create emdash@latestEjecución del scaffolder (destino de despliegue Node.js, plantilla Blog, npm):
— E M D A S H —
┌ Create a new EmDash project
│
◇ Project name? (use "." for current directory)
│ my-emdash-site
│
◇ Where will you deploy?
│ Node.js
│
◇ Which template?
│ Blog
│
◇ Which package manager?
│ npm
│
◇ Install dependencies?
│ Yes
│
◇ Project created!
│
● Wrote EMDASH_ENCRYPTION_KEY to .env.
│
● Installing dependencies with npm...
added 750 packages, and audited 751 packages in 38s
234 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
│
◆ Dependencies installed!
│
◇ Next steps ──────╮
│ │
│ cd my-emdash-site │
│ npm run dev │
│ │
├───────────────────╯
│pnpm create emdash@latest y yarn create emdash funcionan igual y hacen las mismas preguntas. Responde a los prompts de la siguiente manera.
Cada prompt del scaffolder, explicado
Cinco prompts, y solo dos importan para este tutorial.
- Project name.
my-emdash-siteestá bien. Se convierte en el nombre de la carpeta y del paquete npm. - Where will you deploy? Elige Node.js. Esta es la decisión que da forma al
astro.config.mjsgenerado: el adaptador de Node en modostandalonemás un archivo de base de datos SQLite. La opción Cloudflare conecta bindings de D1 y R2 en la config en su lugar; esa ruta es la Parte 3 de esta serie.
Node.js (este tutorial)
SQLite data.db, adaptador de Node, directorio local uploads/. Corre en un portátil hoy y en cualquier VPS después. Sin cuenta cloud en ningún momento.
Cloudflare
La config generada apunta a bindings de D1 (base de datos) y R2 (medios), desplegados con Wrangler. Necesitas una cuenta de Cloudflare, y los plugins en sandbox te empujan a Workers Paid (unos 5 $/mes a septiembre de 2026, verifica el precio al desplegar). La guía completa es la Parte 3.
- Which template? Elige Blog.
create-emdashincluye cinco plantillas: blog, marketing, portfolio, starter y blank. Blog siembra Posts, Pages, comentarios y posts de ejemplo. Es el punto de partida más rico para aprender el modelo de contenido, y es la plantilla detrás de cada captura de abajo. Starter es la variante recortada para proyectos posteriores. (El README de GitHub aún dice “three starter templates”; confía en la carpetatemplates/del repo.) - Which package manager? Mantén el que el scaffolder haya detectado del comando que ejecutaste.
- Install dependencies? Yes.
Si falla la instalación de dependencias
Los archivos del proyecto ya están escritos en ese punto, así que no se pierde nada. El scaffolder imprime un comando de reintento; ejecútalo dentro de la carpeta del proyecto y continúa. Borrar la carpeta y regenerar también funciona, solo cuesta otra descarga.
Qué generó el scaffolder (árbol del proyecto)
Las partes interesantes de my-emdash-site/:
astro.config.mjs: output server, adaptador de Node,react()y la integraciónemdash()con una base de datos SQLite y almacenamiento de medios local. Diseccionado en el Paso 6.src/live.config.ts: conecta el contenido de EmDash con las Live Content Collections de Astro. Seis líneas. También Paso 6.src/pages/index.astro: la página de inicio generada. Ya llama agetEmDashCollection("posts"), que es el tema del Paso 7.seed/seed.json: el modelo de contenido, es decir, colecciones, campos y el contenido de ejemplo que aplicará el asistente de configuración..env: generado y excluido por gitignore, contieneEMDASH_ENCRYPTION_KEY. No la escribiste tú; la escribió el scaffolder. No la commitees.emdash-env.d.ts: tipos TypeScript generados. El servidor de desarrollo lo refresca. Nunca lo edites a mano.
Verifica: la carpeta existe, .env está presente y listado en .gitignore, y node_modules está poblado. Si usas git, commitea el código y seed/seed.json ahora y confirma que .env no aparece en git status.
¿Ya tienes un proyecto Astro? No generes uno nuevo. La guía oficial para añadir EmDash a un proyecto Astro existente cubre los cinco paquetes que instalas a mano (emdash @astrojs/node @astrojs/react react react-dom) más el mismo cableado de config que recorre este tutorial.
Paso 2: arranca el servidor de desarrollo
cd my-emdash-site
npm run dev # or: bun devEl arranque tendrá este aspecto:
➤ npm run dev
> [email protected] dev
> astro dev
13:12:05 [@astrojs/node] Enabling sessions with filesystem storage
— E M D A S H — v1.0.1
13:12:05 [vite] connected.
13:12:05 [types] Generated 1ms
13:12:05 [vite] connected.
› Admin UI http://127.0.0.1:4321/_emdash/admin
› MCP server http://127.0.0.1:4321/_emdash/api/mcp
› Dev bypass http://127.0.0.1:4321/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin
Skips passkey setup/auth and signs you in as a dev admin
astro v7.3.5 ready in 2752 ms
┃ Local http://localhost:4321/
┃ Network use --host to expose
13:12:07 watching for file changes...
13:12:08 [vite] [optimizer] bundling dependencies...
(node:44901) ExperimentalWarning: SQLite is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
[datetime migration] 0 noncanonical values (0 naive) using UTC
Auto-seeded default collections
13:12:10 [200] POST /_emdash/api/typegen 2207ms
13:12:10 [200] POST /_emdash/api/typegen 2msObtienes el banner del servidor de desarrollo de Astro apuntando a http://localhost:4321/. Mantén este terminal abierto; el servidor de desarrollo es tu rebuild, tu generador de tipos y tu runtime a la vez.
Abre http://localhost:4321/ y deberías ver la página de inicio de la plantilla Blog. Abre http://localhost:4321/_emdash/admin/ y un sitio recién creado te redirige al asistente de configuración, que es el Paso 3.
El primer request puede parecer lento. Astro está transpilando el bundle del admin y EmDash está aplicando sus migraciones de base de datos en el primer request (el modo de migración por defecto es runtime: "auto"). Las cargas siguientes son rápidas.
Verifica:
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:4321/
# expect: 200Modo de fallo: si el puerto 4321 está ocupado, Astro elige el siguiente puerto libre y lo imprime. Usa la URL que imprime, no la de este artículo. La URL del asistente de configuración y cualquier passkey que registres después están ligadas al origen que hayas abierto.
Paso 3: primeros pasos con el asistente de configuración de EmDash
El asistente en http://localhost:4321/_emdash/admin/ corre una sola vez, sobre una base de datos vacía. Tres pantallas.
Pantalla 1: información del sitio. Introduce un Site Title (y un tagline opcional). Deja Include sample content seleccionado. De ese checkbox salen el post Welcome y la página About; sin él el admin se ve más vacío que en las capturas de abajo.

Pantalla 2: tu cuenta. El email es obligatorio; el nombre es opcional. El primer usuario siempre es Admin, el rol con acceso total a ajustes, plugins, esquema y otros usuarios. Los roles por debajo de Admin son Subscriber, Contributor, Author y Editor (en ese orden de capacidad). No los necesitarás hasta que alguien más edite contigo.
Pantalla 3: registra una passkey. Cuando aparezca el prompt de credenciales del navegador, aprueba. Luego haz clic en Open the dashboard.
Verifica: /_emdash/admin carga el dashboard sin formulario de login de por medio. Esa única comprobación prueba que el admin React se montó, la base de datos es alcanzable y tu passkey funciona. Si aparece el dashboard, el resto de este tutorial es papeleo.
Registra tu primera passkey (y la trampa de localhost)
Una passkey es una credencial WebAuthn. El prompt se ve distinto según la plataforma: Touch ID o contraseña de dispositivo en macOS, Windows Hello en Windows, Face ID o huella en móvil, o una llave de seguridad física en cualquier parte. Las passkeys se sincronizan por iCloud Keychain, Google Password Manager o 1Password, así que normalmente obtienes una vía de recuperación gratis.
EmDash permite hasta 10 passkeys por usuario y se niega a eliminar la última. Registra una passkey de respaldo en los ajustes de cuenta antes de tener algo que valga la pena perder.
Las passkeys están ligadas al dominio
Una passkey registrada en localhost:4321 nunca funcionará en example.com, y lo contrario también es cierto. Después de desplegar (Partes 3 y 4), te registras de nuevo en el dominio real. Planifícalo ahora para que el primer bloqueo del día del despliegue no te pille por sorpresa.
Conviene saberlo antes de depender de ello: el fallback de magic link solo funciona si configuras el email, y hay rate limits (las opciones de passkey están limitadas por IP por minuto). Si eres el único admin, pierdes todas las passkeys y no hay email configurado, la recuperación significa resetear la base de datos. Registra una passkey de respaldo.
Paso 4: un recorrido por el panel de administración de EmDash
Un ancla conceptual antes de hacer clic por ahí: EmDash edita datos, los componentes Astro son dueños del HTML. Esto no es un page builder visual. No hay layout de arrastrar y soltar. Los editores cambian contenido, menús y widgets; tus plantillas deciden qué se renderiza y dónde.

El sidebar se agrupa en tres secciones. Content contiene Pages, Posts, Media y cualquier tipo de contenido que crees después. Manage cubre Comments, Menus, Redirects, Widgets, Sections, Categories, Tags y Bylines. Admin es el extremo del operador: Content Types, Byline Schema, Users, Plugins, Import y Settings. Dedica tu tiempo a las áreas de abajo; el resto son obvias cuando las necesites.
Posts y páginas: borradores vs. publicados
Posts y Pages son las dos colecciones que siembra la plantilla Blog. La vista de lista muestra el estado (draft, published, scheduled, trash), y el editor es TipTap sobre Portable Text (JSON estructurado, no HTML crudo en la base de datos).
Abrir una entrada toma un bloqueo de edición. Si alguien más (u otra pestaña) la tiene abierta, tu guardado obsoleto es rechazado en vez de ganar en silencio. Ese comportamiento importa más de lo que parece cuando un agente también está editando a través del servidor MCP después.
Save vs Publish
Save escribe un borrador privado. Publish changes lo hace público. Nada aparece en el sitio hasta que publicas. El control de programación registra una hora de publicación para un borrador, así que “publicar más tarde” es una acción de primera clase, no un hack con cron.
Biblioteca de medios: subidas, recorte y límites
Subidas de arrastrar y soltar con estados de cola por archivo. Formatos aceptados: PNG, JPEG, GIF, WebP, AVIF, MP4, WebM, MOV, MP3, WAV, Ogg y PDF. Las imágenes tienen presets de recorte (Original, Freeform, Square, 4:3, 3:2, 16:9) más un punto focal. Carpetas, filtros por tipo y búsqueda por nombre de archivo la mantienen usable más allá de unas docenas de archivos.

Dos comportamientos que conviene conocer desde el principio:
- Replace image mantiene el ID del medio, así que cada referencia a esa imagen se actualiza a la vez. Úsalo para “mismo hueco, nueva imagen” en vez de borrar y volver a subir.
- Used in (seguimiento de uso) está apagado hasta que lo activas una vez en Settings. Es de un solo sentido: una vez activado, no se puede desactivar.
Las subidas de SVG están bloqueadas por defecto
Los SVG están excluidos porque pueden llevar contenido activo. Tu logo en formato SVG fallará al subirlo; exporta una copia PNG o WebP. El maxUploadSize por defecto es 50 MB. Ambos límites son configurables, pero déjalos quietos hasta tener una razón para no hacerlo.
Menús y áreas de widgets (por qué el sitio no cambia todavía)
Esta es la sección que hace tropezar a todos. Crea un menú en Menus (nómbralo primary, etiquétalo “Primary nav”) y añade un enlace personalizado. Luego abre Widget Areas (/_emdash/admin/widgets), añade un área sidebar y suelta dentro un widget de contenido y core:recent-posts.
Ahora recarga el sitio público. Nada cambió.
Ese es el comportamiento correcto. EmDash guardó tu menú y tus widgets; ninguna plantilla Astro los está renderizando todavía. Las plantillas llaman a getMenu("primary", { locale }) (devuelve null cuando el menú no existe) y getWidgetArea("sidebar") con un pequeño renderizador de widgets. Hasta que escribas ese código, los cambios del lado del admin permanecen invisibles. Los tipos de widget cubren contenido (Portable Text), menús y componentes como core:recent-posts, core:categories, core:tags, core:search y core:archives.

Nada en las páginas Astro generadas renderiza áreas de widgets todavía, así que crear una ahora es la forma honesta de ver los datos guardarse. El lado del renderizado pertenece a tus plantillas, no a este tutorial.
Content Types y Settings: el esquema y los controles
Dos pantallas del grupo Admin terminan el recorrido. Ojéalas ahora, cámbialas después.
Content Types es el editor de esquema: el mismo modelo que seed/seed.json escribió durante la configuración, editable en la UI. Crear un tipo pide etiquetas en singular y plural más un slug. El slug es lo que tu código pasa a getEmDashCollection("testimonials") y lo que usan los endpoints de la API. Los toggles merecen una lectura: Routable exige un slug antes de que una entrada pueda publicarse, Edit locking es el comportamiento de retener la entrada de la sección de Posts, y URL pattern acepta los tokens {slug}, {id} y de fecha, así que un permalink estilo WordPress /{year}/{month}/{day}/{slug} es una línea de config. Un grupo de navegación pliega tipos relacionados en una carpeta colapsable del sidebar, y el campo de icono acepta un nombre de icono Phosphor. Los tipos nuevos aparecen bajo Content en cuanto guardas.

Dos trampas de esquema para anotar: añadir un campo a un tipo existente no rellena valores en entradas viejas (solo obtienen el campo nuevo cuando las editas y guardas), y cada cambio de modelo regenera emdash-env.d.ts en el siguiente tick del servidor de desarrollo, así que tu código de queries se mantiene tipado.
Settings reúne los controles operativos en una pantalla: Site (identidad, logo, enlaces sociales, campos SEO), Media (el toggle unidireccional de seguimiento de uso de la sección de medios), Security (passkeys y dominios de auto-registro), API Tokens (personal access tokens para la REST API y el servidor MCP), Email (estado del proveedor y correo de prueba, la misma dependencia de magic link del Paso 3), Backups (descarga manual más backups automáticos programados), Transfer (exportar este sitio a otra instalación de EmDash) y Language para la UI del admin.

Los dos que conviene recordar el primer día: API Tokens es de donde sale un token con alcance de contributor cuando conectes un agente más adelante, y Backups es el botón que convierte el trípode data.db + uploads/ + clave de cifrado del Paso 6 en un archivo descargable.
Paso 5: publica tu primera edición (una mini demo de EmDash CMS)
Este es el momento demo: una edición en el admin apareciendo en el sitio público sin rebuild.
- Ve a Posts y abre el post de ejemplo Welcome.
- Cambia el título a
Hello from EmDash. - Haz clic en Save. La entrada ahora es un borrador con tu cambio.
- Haz clic en Publish changes.
- Recarga
http://localhost:4321/.

La página de inicio ahora lista “Hello from EmDash”. Nada se rebuildeó, nada se redesplegó: el servidor de desarrollo nunca se reinició y astro build nunca corrió.
Por qué funcionó en dos segundos: la página de inicio de la plantilla Blog llama a getEmDashCollection("posts") en tiempo de render. Con las Live Content Collections, la query lee el contenido publicado de la base de datos en cada request, así que la siguiente carga de página ve la siguiente versión.
Verifica: renombra el post otra vez, guarda, publica, recarga. La ida y vuelta funciona. Ahora despublícalo y recarga; el post desaparece de la lista. Si alguna dirección falla, la página probablemente está prerenderizada. Esa es la causa número uno de “mi edición no apareció”, y el Paso 7 explica el arreglo.
Paso 6: cómo encajan los archivos de configuración
Cuatro archivos explican el proyecto entero. El modelo mental primero: el esquema vive en la base de datos, los tipos se generan a partir de él, el contenido vive en la base de datos, y el HTML pertenece a Astro. Tu repo guarda código y el modelo inicial, no el contenido.
astro.config.mjs: output server, adaptador de Node y react()
Esta es la forma que genera la ruta Node/SQLite (también la forma que escribirías a mano en un proyecto existente):
import node from "@astrojs/node";
import react from "@astrojs/react";
import { defineConfig } from "astro/config";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";
export default defineConfig({
output: "server",
adapter: node({ mode: "standalone" }),
integrations: [
react(),
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
}),
],
});Línea a línea: output: "server" hace que las páginas se rendericen en tiempo de request, que es lo que te da publicación instantánea. node({ mode: "standalone" }) produce un servidor Node autocontenido en dist/server/entry.mjs. react() registra React porque el panel de administración es una app React. sqlite({ url: "file:./data.db" }) es tu base de datos entera, un archivo. local({ directory: "./uploads", ... }) guarda los medios en disco y los sirve por la ruta de medios de EmDash; si omites storage, EmDash usa por defecto ./.emdash/uploads con el mismo baseUrl.
react() es obligatorio, incluso en un sitio Astro puro
Olvidar react() en integrations es la trampa clásica de EmDash. Instalar @astrojs/react, react y react-dom no basta; la integración debe estar registrada. Síntoma: /_emdash/admin se queda colgado en “Loading EmDash…”. Arregla la config y reinicia el servidor de desarrollo.
Para lo que el framework subyacente está haciendo últimamente con el output server y los builds, mira las novedades en rendimiento de build de Astro.
src/live.config.ts: el puente entre la base de datos y Astro
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({ loader: emdashLoader() }),
};Este es el cable entre el contenido respaldado por base de datos y la capa de contenido de Astro. emdashLoader() lee el modelo que vive en la base de datos y lo expone como una live collection, así que getEmDashCollection("posts") queda tipada y consultable. Si ya existe un src/live.config.ts en tu proyecto, añade la entrada _emdash a su objeto collections en vez de sobrescribir el archivo. Un src/content.config.ts basado en archivos sigue funcionando al lado.
El servidor de desarrollo también refresca emdash-env.d.ts cuando cambia el modelo, así que tu editor recoge los campos nuevos sin paso manual. No edites ese archivo; regenera, nunca parchees. (Para esquemas remotos hay una ruta aparte: npx emdash types escribe .emdash/types.ts y .emdash/schema.json.)
seed/seed.json, .env y EMDASH_ENCRYPTION_KEY
seed/seed.json declara tu modelo inicial: colecciones, sus campos y contenido de ejemplo. El asistente de configuración lo aplica sobre una base de datos vacía. Nunca sobrescribe datos existentes en ejecuciones posteriores. Este archivo es lo que commiteas a git y versionas a medida que tu esquema evoluciona; el contenido en sí no está en el repo.
.env guarda EMDASH_ENCRYPTION_KEY, generada por el scaffolder. Puedes generar una tú mismo:
npx emdash secrets generate --write .env--write se niega a sobrescribir una clave existente sin --force, lo cual es un buen valor por defecto. Lo que hace la clave: cifra los secrets de plugins en reposo. La clave en sí nunca se almacena en la base de datos.
Haz backup de la clave de cifrado por separado
Restaura la base de datos sin EMDASH_ENCRYPTION_KEY y cada ajuste de plugin cifrado queda ilegible. Guarda una copia de recuperación fuera de tus backups normales de base de datos, el mismo sitio donde guardarías una exportación de tu gestor de contraseñas. Esta es la tercera pata del trípode de backup: data.db + uploads/ + la clave.
Variables de entorno que conviene conocer (lista completa en la referencia de configuración):
| Variable | Qué hace |
|---|---|
EMDASH_SITE_URL |
URL pública del sitio. Obligatoria en cualquier host que no sea loopback; la configuración falla con SITE_URL_REQUIRED en caso contrario. Hace fallback a SITE_URL. |
EMDASH_DATABASE_URL |
Sobrescribe la ubicación de la base de datos fuera de astro.config.mjs. |
EMDASH_ALLOWED_ORIGINS |
Orígenes de navegador extra aceptados en la verificación de passkey (despliegues multi-hostname). |
EMDASH_PREVIEW_SECRET |
Secret para las URLs de preview de borradores. |
EMDASH_IP_SALT |
Salt para el hash de IP de comentaristas. |
EMDASH_MIGRATIONS_MODE |
auto (por defecto: aplica en el primer request), check (503 mientras haya pendientes), manual. |
Plugins de EmDash: cómo funcionan y cómo añadir uno
Sí, los plugins se instalan con npm, pero a qué paquete npm recurres depende de cuál de los dos formatos de plugin de EmDash quieras decir. La distinción es todo el diseño.
Los plugins nativos son paquetes npm normales que corren en-proceso con acceso completo a tu sitio. Son para código en el que confías: paquetes first-party como forms, SEO, audit log e integraciones de email. Instala y registra uno en astro.config.mjs:
npm install @emdash-cms/plugin-forms// astro.config.mjs
import forms from "@emdash-cms/plugin-forms";
export default defineConfig({
output: "server",
adapter: node({ mode: "standalone" }),
integrations: [
react(),
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
plugins: [forms()],
}),
],
});npm install, importa, añade al array plugins: [], reinicia el servidor de desarrollo. Las pantallas y hooks del plugin se unen al admin en el siguiente request, y sus entradas aparecen bajo Admin > Plugins. Actualizar es npm update como cualquier otra dependencia, que es la idea: el código confiable se comporta como código confiable.
Los plugins en sandbox son las instalaciones del registro. Cada uno trae un manifiesto de capacidades (content:read, email:send, hostnames declarados para llamadas salientes) que el admin te hace aprobar al instalar, y cada hook y ruta corre en un proceso workerd aislado con límites fijos: 128 MB de memoria, 30 segundos de tiempo de pared, sin acceso de red a hosts no declarados. Los instalas desde la pantalla Plugins dentro de /_emdash/admin, no editando package.json.
En esta ruta Node.js, los plugins en sandbox necesitan un paquete extra, porque la capa de aislamiento es el runtime workerd de Cloudflare corriendo como proceso hijo en tu propia máquina:
npm install @emdash-cms/sandbox-workerd workerdsandboxRunner: "@emdash-cms/sandbox-workerd/sandbox",Sin él el sitio corre bien pero cada instalación del registro falla con SANDBOX_NOT_AVAILABLE, lo que confunde a la gente porque nada parece roto hasta que lo intentas. (En Cloudflare el equivalente es el binding Worker Loader, que necesita Workers Paid; la Parte 3 lo cubre. sandbox: false corre el código en sandbox en-proceso, pero eso es un interruptor de depuración, no una postura.)
¿Necesitas plugins hoy?
No. Este tutorial funciona de principio a fin sin ninguno, y la plantilla Blog ya cubre posts, páginas, medios, menús y widgets. Vuelve a esta sección cuando quieras un plugin de formularios, una instalación del registro o campos SEO automatizados. Es un añadido de dos minutos, no un paso de configuración.
Paso 7: consulta tus primeros posts con getEmDashCollection
El src/pages/index.astro de la plantilla Blog ya hace la versión básica. Esta es la forma que vale la pena aprender, con manejo de errores y el cache hint:
---
import { getEmDashCollection } from "emdash";
const { entries: posts, error, cacheHint } = await getEmDashCollection("posts", {
orderBy: { published_at: "desc" },
limit: 7,
});
if (error) {
console.error("Failed to load posts:", error);
return new Response("Unable to load posts", { status: 500 });
}
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---
<ul>
{posts.map((post) => <li>{post.data.title}</li>)}
</ul>Cinco cosas que enseña:
- Los errores vuelven como datos, no excepciones. El objeto resultado es
{ entries, error, cacheHint, nextCursor, hasMore }. Un arrayentriesvacío significa tanto “sin coincidencias” como “la query falló”, así que compruebaerrorantes de confiar en una lista vacía. - Las queries públicas por defecto usan
status: "published". Los borradores son invisibles paragetEmDashCollectionhasta que publicas (o le pasas a alguien una URL de preview). orderByusa nombres de columna de la base de datos:published_at,created_at,updated_at. Los nombres de campo camelCase que ves enentry.data(comoentry.data.publishedAt) no son válidos enorderBy. Usa la forma snake_case en todas partes; un par de ejemplos viejos en la documentación usan camelCase ahí, y la guía de queries es la que vale.entry.ides el slug que se expone a la URL;entry.data.ides el ID de contenido estable, que sobrevive a un cambio de slug. Enlaza al primero, guarda referencias al segundo.- Los filtros
wherese combinan con AND (objeto de pares clave/valor) con valores array que significan OR, y claves de taxonomía que matchean slugs de términos. Los cursores de paginación vuelven comonextCursormás un flaghasMore.
Para una página de post individual, cambia a getEmDashEntry:
---
import { getEmDashEntry } from "emdash";
import { PortableText, Image } from "emdash/ui";
const { entry: post, error, isPreview } = await getEmDashEntry("posts", Astro.params.slug!);
if (error) return new Response("Unable to load post", { status: 500 });
if (!post) return new Response(null, { status: 404 });
---
<h1>{post.data.title}</h1>
<PortableText value={post.data.content} />
<Image image={post.data.featured_image} priority />isPreview es true en requests con token de preview, así que puedes etiquetar vistas de borradores. PortableText e Image vienen de emdash/ui; getSeoMeta(post, { siteTitle, siteUrl, path }) rellena tus meta tags. Cuando la caché de Astro está activada, Astro.cache.set(cacheHint) etiqueta la página y los eventos de publicación la invalidan.
Verifica: edita el título del post una vez más, guarda, publica, recarga. El cambio aparece en el siguiente request porque la página se renderiza en servidor (output: "server" del Paso 6). Las páginas prerenderizadas son lo contrario: congeladas en build time hasta que rebuildeas. Si tus ediciones nunca aparecen, comprueba de cuál de las dos es tu página.
Solución de errores comunes al configurar EmDash CMS
Síntoma, causa, arreglo. Estos son los fallos con los que la gente choca en la primera hora.
| Síntoma | Causa | Arreglo |
|---|---|---|
| Admin colgado en “Loading EmDash…” | react() falta en integrations |
Registra react() en astro.config.mjs; instalar los paquetes solo no basta. Reinicia el servidor de desarrollo. |
Errores de getEmDashCollection() sobre una live collection |
src/live.config.ts no tiene la entrada _emdash |
Añade defineLiveCollection({ loader: emdashLoader() }) al objeto collections. |
| Las ediciones no aparecen en la página | La página está prerenderizada, o el sitio no es output: "server" |
Renderiza en servidor para ver las publicaciones en el siguiente request; las páginas prerenderizadas necesitan un rebuild. |
| Passkey rechazada tras el despliegue | Las passkeys están ligadas al dominio | Registra una passkey nueva en el dominio nuevo. La de localhost se queda en localhost. |
orderBy rechaza campos camelCase |
orderBy toma nombres de columna de la base de datos |
Usa published_at, created_at, updated_at. |
| La instalación de dependencias murió a mitad del scaffold | Tropiezo de red o del registro | Los archivos del proyecto permanecen. Ejecuta el comando de reintento que imprimió el scaffolder. |
| Servidor de desarrollo en un puerto inesperado | 4321 ya en uso | Usa la URL que imprime Astro. Las passkeys y la URL del asistente van con ese origen. |
La configuración falla con SITE_URL_REQUIRED |
Configuración en un host no-loopback sin EMDASH_SITE_URL |
Define EMDASH_SITE_URL (o siteUrl en la config) antes de correr la configuración fuera de localhost. |
Rollback y borrón y cuenta nueva. Ctrl+C al servidor de desarrollo, luego borra la carpeta del proyecto; eso es un deshacer completo, ya que data.db, uploads/ y .env viven dentro. Dos advertencias para versiones posteriores de este proyecto: npx emdash migrate no tiene down ni --dry-run (comprueba el trabajo pendiente con npx emdash migrate --check), así que “revertir” un cambio de esquema significa restaurar archivos, no correr una migración de bajada. Para el proceso antes de cambiar por un data.db de backup; un servidor vivo sujetando el file handle viejo te corromperá la tarde.
Dos chequeos rápidos de salud cuando algo parezca raro:
npx emdash doctor # local DB: connection, migrations, collections, users
npx emdash whoami # dev-bypass auth on localhost shows your admin userVerificación: tu primer sitio EmDash funciona
Recorre esta lista antes de pasar al despliegue. Cada ítem corresponde a algo que construyó este tutorial.
http://localhost:4321/renderiza y lista el post renombrado/_emdash/admincarga el dashboard (pruebareact()+ base de datos + auth)- La ida y vuelta de borradores funciona: editar → Save → Publish changes → visible; Unpublish → desaparece
- Una subida de medio aparece dentro de una página Portable Text, y borrar funciona
npx emdash doctorpasa contra la base de datos local- Opcional:
npx emdash whoamimuestra tu usuario admin
FAQ: preguntas comunes sobre cómo empezar con EmDash
¿Necesito una cuenta de Cloudflare para seguir este tutorial?
No. Todo este tutorial corre en Node.js con SQLite (data.db) y un directorio local uploads/. Cloudflare entra solo cuando eliges el destino de despliegue Cloudflare o despliegas a Workers, que es la Parte 3 de la serie. Nada aquí requiere tarjeta, prueba ni cuenta.
¿Dónde vive mi contenido? ¿Puedo seguir usando git?
El contenido vive en la base de datos SQLite (data.db), no en tu repo. Es deliberado: el JSON de Portable Text en una base de datos es lo que permite a los editores trabajar sin tocar git. Lo que sí commiteas es código más seed/seed.json, el modelo inicial versionado. Haz backup de tres cosas juntas: data.db, uploads/ y EMDASH_ENCRYPTION_KEY de .env.
¿Qué es una passkey y qué pasa si mi navegador no la soporta?
Una passkey es una credencial WebAuthn, normalmente respaldada por Touch ID, Windows Hello, Face ID o una llave hardware, y sincronizada por iCloud Keychain, Google Password Manager o 1Password. Todo navegador de escritorio actual las maneja en localhost. El fallback de magic link existe pero necesita email configurado. Consejo práctico: registra una passkey de respaldo en los ajustes de cuenta el día que crees tu admin.
¿Por qué necesito React en un sitio que no es React?
El panel de administración en /_emdash/admin es una aplicación React. La integración react() de Astro es lo que la monta. Olvídala y el admin se cuelga en “Loading EmDash…”. Tus plantillas de sitio pueden seguir siendo Astro puro; React se incluye para la UI de edición.
Save vs Publish: ¿cuál hace público el contenido?
Publish changes. Save escribe un borrador privado que solo los editores (y las URLs de preview) pueden ver. Como las páginas se renderizan en servidor y están respaldadas por Live Content Collections, una edición publicada aparece en el siguiente request sin rebuild ni reinicio del servidor de desarrollo. Los posts programados registran una hora de publicación y cambian automáticamente.
¿Por qué mi menú o widget no cambió el sitio?
Porque el admin guarda datos y tus plantillas Astro los renderizan. Un menú llamado primary solo aparece cuando una plantilla llama a getMenu("primary", { locale }); los widgets necesitan getWidgetArea("sidebar") más un renderizador. EmDash no es un page builder visual, así que los datos de layout del lado del admin permanecen invisibles hasta que tu código los pida.
¿Qué cifra EMDASH_ENCRYPTION_KEY y qué pasa si la pierdo?
Cifra los secrets de plugins en reposo. La clave la provee el operador y nunca se almacena en la base de datos, así que restaurar data.db sin la clave deja los ajustes de plugins ilegibles. Guarda una copia de recuperación fuera de tus backups de base de datos. Y recuerda: está en gitignore a propósito. Commitearla anula todo el sentido de cifrar secrets.
Siguientes pasos: despliega tu sitio EmDash
Ya tienes un sitio EmDash local corriendo y un modelo mental funcional de sus partes. Local está bien para construir. Todavía no es un sitio web.
Tres rutas hacia adelante, en el orden en que las cubre esta serie:
- Parte 3: Cloudflare Workers + D1 + R2. La ruta todo-Cloudflare, la misma plataforma sobre la que corre el Cloudflare Blog. El tier gratis cubre un sitio básico (las plantillas se entregan sin el binding Worker Loader); los plugins en sandbox necesitan Workers Paid, unos 5 $/mes a septiembre de 2026. Verifica el precio antes de comprometerte.
- Parte 4: tu propio VPS con Node.js y Docker. La documentación oficial de despliegue en Node.js te da un Dockerfile multi-stage
node:22-alpiney un archivo compose con un volumen nombrado para/app/data(ese volumen guarda tu base de datos y tus uploads en un solo sitio). Un VPS de 1-2 GB como un Hetzner Cloud VPS corre a escala de blog cómodamente por unos 4-5 €/mes. Si quieres el stack alrededor mientras tanto, desplegar un sitio Astro en un VPS con CloudPanel o alojar apps Node.js en un VPS con CloudPanel y PM2 cubren las dos mitades de ese montaje. - Alternativa zero-ops. Desplegar apps en tu propio servidor con Coolify te da despliegues con git push y un proxy HTTPS sin escribir units de systemd a mano.
Endurecimiento de producción, adelantado ahora y cubierto en serio en las Partes 3 y 4: define EMDASH_SITE_URL en cualquier host que no sea localhost, corre el backup de tres patas (data.db + uploads/ + la clave de cifrado), mantén el límite de subida de 50 MB y la prohibición de SVG hasta tener una razón para cambiarlos, y define trustedProxyHeaders detrás de un reverse proxy para que los rate limits de auth por IP se comporten bien.
Comandos CLI que vale la pena tener abiertos en un segundo terminal:
npx emdash doctor # local database health check
npx emdash whoami # who the dev bypass says you are
npx emdash content list posts # list content from the terminal
npx emdash seed --validate # CI-safe seed check
npx emdash secrets generate --write .env
npm run build
node --env-file=.env ./dist/server/entry.mjs # standalone does NOT auto-load .envEsa última línea es el cliffhanger de la Parte 4: el servidor standalone espera que le pases --env-file. Olvídalo y la app arranca sin EMDASH_ENCRYPTION_KEY.


