EmDash se configura a través de dos archivos: astro.config.mjs para la integración y src/live.config.ts para las colecciones de contenido.
Integración de Astro
Configure EmDash como una integración de Astro en astro.config.mjs:
import { defineConfig } from "astro/config";
import emdash, { local, s3 } from "emdash/astro";
import { sqlite, libsql } from "emdash/db";
export default defineConfig({
integrations: [
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
plugins: [],
}),
],
});
Opciones de integración
database
Requerido. Configuración del adaptador de base de datos. Elija un adaptador:
// SQLite (Node.js)
database: sqlite({ url: "file:./data.db" });
// PostgreSQL
database: postgres({ connectionString: process.env.DATABASE_URL });
// libSQL
database: libsql({
url: process.env.LIBSQL_DATABASE_URL,
authToken: process.env.LIBSQL_AUTH_TOKEN,
});
// Cloudflare D1 (importar desde @emdash-cms/cloudflare)
database: d1({ binding: "DB" });
Ver Opciones de base de datos para más detalles.
storage
Requerido. Configuración del adaptador de almacenamiento de medios. Elija un adaptador:
// Sistema de archivos local (desarrollo)
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
});
// Enlace R2 (Cloudflare Workers)
storage: r2({
binding: "MEDIA",
publicUrl: "https://pub-xxxx.r2.dev", // opcional
});
// Compatible con S3 (cualquier plataforma) — todos los campos de variables de entorno S3_*
storage: s3()
// O con valores explícitos
storage: s3({
endpoint: "https://s3.amazonaws.com",
bucket: "my-bucket",
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
region: "us-east-1", // opcional, predeterminado: "auto"
publicUrl: "https://cdn.example.com", // opcional
});
Ver Opciones de almacenamiento para más detalles.
plugins
Opcional. Array de plugins de EmDash. El siguiente ejemplo registra un plugin:
import seoPlugin from "@emdash-cms/plugin-seo";
plugins: [seoPlugin()];
fonts
Opcional. Configuración de fuentes de la interfaz de administración.
Por defecto, EmDash carga Noto Sans a través de la API de fuentes de Astro. Las fuentes se descargan de Google en tiempo de compilación y se alojan localmente, por lo que no hay solicitudes CDN en tiempo de ejecución. La fuente base cubre escrituras latina, cirílica, griega, devanagari y vietnamita.
Para agregar soporte para sistemas de escritura adicionales, pase nombres de escritura. El siguiente ejemplo agrega árabe y japonés:
emdash({
fonts: {
scripts: ["arabic", "japanese"],
},
})
Las escrituras disponibles son arabic, armenian, bengali, chinese-simplified, chinese-traditional, chinese-hongkong, devanagari, ethiopic, farsi, georgian, gujarati, gurmukhi, hebrew, japanese, kannada, khmer, korean, lao, malayalam, myanmar, oriya, sinhala, tamil, telugu, thai y tibetan.
Cada escritura se asigna a la variante correspondiente de Noto Sans en Google Fonts (por ejemplo, "arabic" carga Noto Sans Arabic). Todas las fuentes comparten un único nombre de font-family y usan unicode-range para que el navegador solo descargue los archivos que necesita para los caracteres en la página.
Establezca en false para deshabilitar completamente la inyección de fuentes y usar fuentes del sistema:
emdash({
fonts: false,
})
El CSS de administración usa la variable CSS --font-emdash. Esto se establece automáticamente mediante la configuración de fuentes anterior.
auth
Opcional. Un adaptador de autenticación. El inicio de sesión integrado de EmDash utiliza passkeys; configurar auth los reemplaza con un proveedor externo. El adaptador de Cloudflare Access, access(), es proporcionado por @emdash-cms/cloudflare:
import { access } from "@emdash-cms/cloudflare";
emdash({
auth: access({
teamDomain: "myteam.cloudflareaccess.com",
audience: "your-app-audience-tag",
roleMapping: {
Admins: 50,
Editors: 40,
},
}),
});
Opciones para access():
| Opción | Tipo | Predeterminado | Descripción |
|---|---|---|---|
teamDomain | string | requerido | Su dominio de equipo de Cloudflare Access |
audience | string | — | Etiqueta de Audience (AUD) de la aplicación. En Workers, prefiera audienceEnvVar. |
audienceEnvVar | string | "CF_ACCESS_AUDIENCE" | Variable de entorno desde la cual leer la etiqueta de audiencia en tiempo de ejecución |
autoProvision | boolean | true | Crear un usuario de EmDash en el primer inicio de sesión |
defaultRole | number | 30 | Nivel de rol para usuarios no coincidentes con roleMapping (ver Roles de usuario) |
syncRoles | boolean | false | Volver a aplicar roleMapping en cada inicio de sesión en lugar de solo en el aprovisionamiento |
roleMapping | object | — | Mapear nombres de grupos IdP a niveles de rol de EmDash; la primera coincidencia gana |
authProviders
Opcional. Un array de proveedores de inicio de sesión conectables (nivel superior, junto a auth). Cada entrada es el resultado de llamar a una fábrica de proveedores, como se muestra a continuación:
import { github } from "emdash/auth/providers/github";
import { google } from "emdash/auth/providers/google";
import { atproto } from "@emdash-cms/auth-atproto";
emdash({
authProviders: [github(), google(), atproto()],
});
Proveedores integrados:
github()— leeEMDASH_OAUTH_GITHUB_CLIENT_ID/EMDASH_OAUTH_GITHUB_CLIENT_SECRET(o respaldos sin prefijo).google()— leeEMDASH_OAUTH_GOOGLE_CLIENT_ID/EMDASH_OAUTH_GOOGLE_CLIENT_SECRET.atproto()— Inicio de sesión de cuenta Atmosphere (Bluesky y la red AT Protocol más amplia). No se necesitan variables de entorno. Acepta{ allowedDIDs, allowedHandles, defaultRole }. Ver la guía de inicio de sesión de Atmosphere.
Los paquetes de terceros pueden registrar sus propios proveedores usando la misma forma AuthProviderDescriptor — ver Proveedores de inicio de sesión.
siteUrl
Opcional. El origen público orientado al navegador para el sitio (esquema + host + puerto opcional, sin ruta).
Detrás de un proxy inverso de terminación TLS, Astro.url devuelve la dirección interna (http://localhost:4321) en lugar de la pública (https://cms.example.com). Esto rompe passkeys, coincidencia de origen CSRF, redirecciones OAuth, redirecciones de inicio de sesión, descubrimiento MCP, exportaciones de instantáneas, sitemap, robots.txt y datos estructurados JSON-LD. Establezca siteUrl para arreglar todo esto a la vez.
La integración valida este valor en tiempo de carga: debe ser una URL válida con protocolo http: o https: y se normaliza a origen (se elimina la ruta).
El siguiente ejemplo establece el origen público:
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
siteUrl: "https://cms.example.com",
});
Cuando siteUrl no está configurado en config, EmDash verifica las variables de entorno en orden: EMDASH_SITE_URL, luego SITE_URL. Esto es útil para implementaciones de contenedores donde la URL pública se establece en tiempo de ejecución.
Verificación de passkey de múltiples orígenes
siteUrl define un único origen canónico. Cuando la misma implementación de EmDash es accesible bajo varios nombres de host que comparten un dominio padre registrable (por ejemplo, https://example.com y https://preview.example.com), la verificación de passkey rechaza aserciones cuyo origen no coincide exactamente con siteUrl — aunque WebAuthn permite que los passkeys sean válidos en subdominios bajo el mismo rpId.
Declare orígenes aceptados adicionales a través de allowedOrigins en astro.config.mjs o la variable de entorno EMDASH_ALLOWED_ORIGINS. El siteUrl canónico permanece como la fuente de rpId; las entradas listadas aquí se aceptan en tiempo de verificación. Las dos fuentes se fusionan en tiempo de ejecución, por lo que la configuración puede declarar los orígenes estables (versionados, revisados por código) mientras que el entorno agrega extras específicos del entorno (por ejemplo, vistas previas de PR efímeras).
El siguiente ejemplo declara un origen adicional en la configuración:
emdash({
siteUrl: "https://example.com",
allowedOrigins: ["https://preview.example.com"],
})
Los valores equivalentes también pueden provenir de variables de entorno:
EMDASH_SITE_URL=https://example.com
EMDASH_ALLOWED_ORIGINS=https://preview.example.com,https://staging.example.com
Validación
EmDash valida estos para prevenir configuración muerta que el navegador nunca honraría:
- Cada entrada debe ser una URL analizable
http:ohttps:sin punto final y sin etiquetas vacías en el nombre de host. - Cuando
allowedOriginsno está vacío,siteUrldebe estar configurado (cualquier fuente) y no debe ser un literal de IP ni tener un nombre de host con punto final. - Cada origen debe ser el mismo nombre de host que
siteUrlo un subdominio de él. (WebAuthn requiere querpIdsea un sufijo registrable de cada origen.)
Cuando la validación falla, verá un error atribuido a la fuente como EmDash config error in EMDASH_ALLOWED_ORIGINS: "https://other-site.com" is not a subdomain of siteUrl "https://example.com". Allowed origins must be the same hostname as siteUrl or a subdomain of it.
Dónde aparece el error depende de dónde se declaran los valores:
- En el inicio de Astro, cuando tanto
config.allowedOriginscomoconfig.siteUrlprovienen deastro.config.mjs— los errores tipográficos en el código fallan la compilación. - En la primera verificación de passkey, cuando cualquier valor proviene de
EMDASH_ALLOWED_ORIGINSoEMDASH_SITE_URL— los desajustes de entorno aparecen como 500 en el primer intento de verificación.
Configuración de proxy inverso
Astro solo refleja X-Forwarded-* cuando el host público está permitido. Configure security.allowedDomains para el nombre de host (y esquemas) que alcanzan sus usuarios. En astro dev, agregue vite.server.allowedHosts coincidentes para que Vite acepte el encabezado Host del proxy.
Prefiera arreglar allowedDomains (y encabezados reenviados) primero; use siteUrl cuando la URL reconstruida todavía diverge del origen del navegador (típico cuando TLS se termina al frente y la solicitud upstream permanece http://).
Con TLS al frente, vincular el servidor de desarrollo a loopback (astro dev --host 127.0.0.1) suele ser suficiente: el proxy se conecta localmente mientras siteUrl coincide con el origen HTTPS público.
Si su proxy escribe un encabezado de IP de cliente, establezca trustedProxyHeaders para que los límites de tasa de EmDash puedan usar la IP real del cliente en lugar de agrupar cada solicitud bajo una clave “unknown” compartida.
La siguiente configuración establece allowedDomains, vite.server.allowedHosts y siteUrl juntos para una implementación de proxy inverso:
import { defineConfig } from "astro/config";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";
export default defineConfig({
security: {
allowedDomains: [
{ hostname: "cms.example.com", protocol: "https" },
{ hostname: "cms.example.com", protocol: "http" },
],
},
vite: {
server: {
allowedHosts: ["cms.example.com"],
},
},
integrations: [
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
siteUrl: "https://cms.example.com",
}),
],
});
trustedProxyHeaders
Opcional. Encabezados en los que confiar para la resolución de IP del cliente cuando se ejecuta detrás de un proxy inverso que usted controla. Utilizado por límites de tasa de autenticación (magic-link, registro, passkey, flujo de dispositivo OAuth) y el endpoint de comentarios públicos.
En Cloudflare, el objeto cf adjunto a la solicitud se usa automáticamente — normalmente no necesita configurar esto. En implementaciones autoalojadas detrás de nginx, Caddy, Traefik, Fly, Railway o similares, establezca esto en el encabezado que su proxy escribe para que los límites de tasa puedan agrupar por IP de cliente real en lugar de tratar cada solicitud como “unknown”.
El siguiente ejemplo confía en el encabezado x-real-ip establecido por nginx, Caddy o Traefik:
emdash({
database: sqlite({ url: "file:./data.db" }),
trustedProxyHeaders: ["x-real-ip"],
});
Los encabezados se prueban en orden. Los valores que coinciden con *-forwarded-for se analizan como listas separadas por comas y se usa la primera entrada. El siguiente ejemplo prefiere el encabezado de Fly.io y recurre a x-forwarded-for:
emdash({
trustedProxyHeaders: ["fly-client-ip", "x-forwarded-for"],
});
Cuando no está configurado en config, EmDash lee la variable de entorno EMDASH_TRUSTED_PROXY_HEADERS (separada por comas). Un array vacío explícito en config anula la variable de entorno.
maxUploadSize
Opcional. Tamaño máximo permitido de carga de archivo multimedia en bytes. Se aplica tanto a cargas multiparte directas como a cargas de URL firmadas. El valor predeterminado es 52_428_800 (50 MB). El siguiente ejemplo aumenta el límite a 100 MB:
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
maxUploadSize: 100 * 1024 * 1024, // 100 MB
});
| Valor | Descripción |
|---|---|
number (bytes) | Debe ser un entero finito positivo |
| omitido | Predeterminado es 50 MB |
Las cargas que exceden el límite configurado se rechazan con una respuesta 413 Payload Too Large en la ruta de carga directa, o un 400 Validation Error en la ruta de URL firmada.
Adaptadores de base de datos
Importe los adaptadores desde emdash/db:
import { sqlite, libsql, postgres } from "emdash/db";
sqlite(config)
Base de datos SQLite usando better-sqlite3. El siguiente ejemplo se conecta a un archivo local:
| Opción | Tipo | Descripción |
|---|---|---|
url | string | Ruta de archivo con prefijo file: |
sqlite({ url: "file:./data.db" });
libsql(config)
Base de datos libSQL. El siguiente ejemplo se conecta a una base de datos libSQL remota:
| Opción | Tipo | Descripción |
|---|---|---|
url | string | URL de la base de datos |
authToken | string | Token de autenticación (opcional para archivos locales) |
libsql({
url: process.env.LIBSQL_DATABASE_URL,
authToken: process.env.LIBSQL_AUTH_TOKEN,
});
postgres(config)
Base de datos PostgreSQL con agrupación de conexiones.
| Opción | Tipo | Descripción |
|---|---|---|
connectionString | string | URL de conexión PostgreSQL |
host | string | Host de la base de datos |
port | number | Puerto de la base de datos |
database | string | Nombre de la base de datos |
user | string | Usuario de la base de datos |
password | string | Contraseña de la base de datos |
ssl | boolean | Habilitar SSL |
pool.min | number | Tamaño mínimo del pool (predeterminado: 0) |
pool.max | number | Tamaño máximo del pool (predeterminado: 10) |
El siguiente ejemplo se conecta con una cadena de conexión:
postgres({ connectionString: process.env.DATABASE_URL });
d1(config)
Base de datos Cloudflare D1. Importar desde @emdash-cms/cloudflare.
| Opción | Tipo | Predeterminado | Descripción |
|---|---|---|---|
binding | string | — | Nombre de enlace D1 de wrangler.jsonc |
session | string | "disabled" | Modo de replicación de lectura: "disabled", "auto" o "primary-first" |
bookmarkCookie | string | "__em_d1_bookmark" | Nombre de cookie para marcadores de sesión |
El siguiente ejemplo muestra un enlace básico y uno con réplicas de lectura habilitadas:
// Básico
d1({ binding: "DB" });
// Con réplicas de lectura
d1({ binding: "DB", session: "auto" });
Cuando session es "auto" o "primary-first", EmDash usa la API de sesiones D1 para enrutar consultas de lectura a réplicas cercanas. Los usuarios autenticados obtienen consistencia de lectura-después-de-escritura basada en marcadores. Ver Opciones de base de datos — Réplicas de lectura para detalles.
Adaptadores de almacenamiento
Importe local y s3 desde emdash/astro. El adaptador r2 se importa desde @emdash-cms/cloudflare:
import emdash, { local, s3 } from "emdash/astro";
import { r2 } from "@emdash-cms/cloudflare";
local(config)
Almacenamiento en sistema de archivos local. El siguiente ejemplo sirve cargas desde un directorio local:
| Opción | Tipo | Descripción |
|---|---|---|
directory | string | Ruta del directorio |
baseUrl | string | URL base para servir archivos |
local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
});
r2(config)
Enlace de Cloudflare R2. El siguiente ejemplo usa un enlace R2 con una URL pública:
| Opción | Tipo | Descripción |
|---|---|---|
binding | string | Nombre de enlace R2 |
publicUrl | string | URL pública opcional |
r2({
binding: "MEDIA",
publicUrl: "https://pub-xxxx.r2.dev",
});
s3(config?)
Almacenamiento compatible con S3. Todos los campos de configuración son opcionales: cualquier campo omitido de
s3({...}) se resuelve desde la variable de entorno S3_* coincidente cuando el
proceso de Node se inicia. Los valores explícitos siempre tienen precedencia.
Requisito previo: instale @aws-sdk/client-s3 y @aws-sdk/s3-request-presigner
en su proyecto. EmDash core no incluye el AWS SDK. Ver
Opciones de almacenamiento: Almacenamiento compatible con S3
para detalles.
| Opción | Tipo | Descripción |
|---|---|---|
endpoint | string | URL del endpoint S3 (S3_ENDPOINT) |
bucket | string | Nombre del bucket (S3_BUCKET) |
accessKeyId | string | Clave de acceso (S3_ACCESS_KEY_ID) |
secretAccessKey | string | Clave secreta (S3_SECRET_ACCESS_KEY) |
region | string | Región, predeterminado "auto" (S3_REGION) |
publicUrl | string | URL CDN opcional (S3_PUBLIC_URL) |
Los siguientes ejemplos resuelven todos los campos desde el entorno, mezclan configuración y entorno, o pasan cada campo explícitamente:
// Todos los campos de variables de entorno S3_* (implementaciones de contenedores Node)
s3()
// Mezcla: CDN de configuración, resto del entorno
s3({ publicUrl: "https://cdn.example.com" })
// Todo explícito
s3({
endpoint: "https://xxx.r2.cloudflarestorage.com",
bucket: "media",
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
publicUrl: "https://cdn.example.com",
})
La resolución de variables de entorno en tiempo de ejecución es una característica solo de Node. En Cloudflare
Workers, los secretos y variables se exponen a través del parámetro env del
manejador fetch, no a través de process.env, por lo que las variables de entorno S3_*
no se capturan. Las implementaciones de Workers deben usar el adaptador r2(config)
o pasar valores explícitos a s3({...}). Ver
Opciones de almacenamiento para detalles.
Colecciones en vivo
Configure el cargador de EmDash en src/live.config.ts:
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({
loader: emdashLoader(),
}),
};
Opciones del cargador
La función emdashLoader() no toma argumentos:
emdashLoader();
Variables de entorno
EmDash respeta estas variables de entorno:
| Variable | Descripción |
|---|---|
EMDASH_SITE_URL | Origen público orientado al navegador (recurre a SITE_URL) |
EMDASH_ALLOWED_ORIGINS | Lista separada por comas de orígenes adicionales aceptados por la verificación de passkey (implementaciones multi-subdominio). |
EMDASH_DATABASE_URL | Anular URL de base de datos |
EMDASH_ENCRYPTION_KEY | Clave para cifrar secretos de plugins en reposo. Proporcionada por el operador — nunca almacenada en la base de datos. |
EMDASH_PREVIEW_SECRET | Anulación opcional para el secreto HMAC de vista previa. Cuando no está configurado, se genera un valor estable por sitio y se almacena en la base de datos. |
EMDASH_IP_SALT | Anulación opcional para el salt de hash de IP del comentarista. Cuando no está configurado, se genera un valor estable por sitio y se almacena en la base de datos. |
EMDASH_AUTH_SECRET | Heredado. Se usa como fuente de salt de IP si está configurado; las instalaciones existentes deben mantener esto para preservar hashes de IP de comentarista estables a través de actualizaciones. |
EMDASH_URL | URL remota de EmDash para sincronización de esquema |
Genere una clave de cifrado con el siguiente comando:
npx emdash secrets generate
Configuración de package.json
Las plantillas y sitios pueden declarar metadatos opcionales bajo una clave emdash en package.json:
{
"emdash": {
"label": "My Blog Template",
"seed": ".emdash/seed.json",
"url": "https://my-site.pages.dev"
}
}
| Opción | Descripción |
|---|---|
label | Nombre de plantilla para mostrar |
seed | Ruta al archivo JSON de semilla |
url | URL remota para sincronización de esquema |
Configuración de TypeScript
EmDash genera tipos en .emdash/types.ts. Agregue un alias de ruta a su tsconfig.json:
{
"compilerOptions": {
"paths": {
"@emdash-cms/types": ["./.emdash/types.ts"]
}
}
}
Genere tipos con el siguiente comando:
npx emdash types