Esta página es para personas que trabajan en EmDash, no para quienes construyen un sitio con él. Explica el diseño de la base de datos, la integración con Astro, las rutas de solicitud, la aplicación de administración, el flujo de medios y el sistema de importación. Si estás construyendo un sitio, lee Arquitectura y el Modelo de contenido en su lugar.
La integración de Astro
EmDash se ejecuta como una integración de Astro desde el paquete emdash. En tiempo de compilación:
-
Inyecta la aplicación de administración y las rutas de API REST con la API
injectRoutede Astro. Nada se copia en el proyecto del usuario. Las principales familias de rutas son:Patrón de ruta Propósito /_emdash/admin/[...path]SPA del panel de administración /_emdash/api/manifestManifiesto del admin (colecciones, plugins) /_emdash/api/content/[collection]/...Operaciones de entradas de contenido /_emdash/api/media/...Operaciones de la biblioteca de medios /_emdash/api/schema/...Gestión de esquemas /_emdash/api/settings/...Configuración del sitio /_emdash/api/menus/...Menús de navegación /_emdash/api/taxonomies/...Categorías, etiquetas, taxonomías personalizadas /_emdash/api/plugins/[pluginId]/[...path]Rutas de API definidas por plugins El inyector de rutas es el inventario completo, incluyendo autenticación, comentarios, búsqueda, importaciones, widgets y otras familias de rutas.
-
Genera módulos virtuales para que el bundler pueda resolver código de configuración y extensión:
Módulo Propósito virtual:emdash/configConfiguración de base de datos, almacenamiento y sitio virtual:emdash/dialectFactory del dialecto de base de datos virtual:emdash/admin-registryImportaciones estáticas para interfaces de admin de plugins virtual:emdash/pluginsImplementaciones de plugins configurados virtual:emdash/media-providersProveedores de medios externos configurados virtual-modules.tsdefine los helpers de runtime restantes y los contenidos de módulos generados. -
Proporciona el loader de Live Content Collections y registra el middleware de runtime. En tiempo de solicitud, el middleware abre las conexiones de base de datos y almacenamiento configuradas y aplica cualquier migración pendiente antes de que las rutas las utilicen.
Esquema database-first
Las definiciones de esquema residen en la base de datos, no en un archivo de configuración estático. _emdash_collections almacena una fila por colección. Sus columnas principales describen la colección y las características que el runtime y el admin exponen:
| Columnas | Propósito |
|---|---|
id, slug | Identidad estable de la colección |
label, label_singular, description, icon | Nombres y orientación mostrados a los editores |
supports, has_seo, comments_enabled, edit_locking | Capacidades opcionales de la colección |
title_field, date_field, admin_config, hidden, sort_order | Lista del admin y comportamiento de navegación |
url_pattern, routable | URL pública y comportamiento del slug |
source | Cómo fue creada la colección |
El valor source registra la procedencia como manual, seed, template:<name>, import:<name> o discovered. Las configuraciones adicionales provienen de migraciones registradas, por lo que database/types.ts y las migraciones son el inventario actual de columnas.
_emdash_fields almacena los campos vinculados a cada colección:
| Columnas | Propósito |
|---|---|
id, collection_id, slug | Identidad del campo y colección propietaria |
label, type, column_type | Etiqueta del editor, tipo de campo EmDash y tipo de almacenamiento SQL |
required, unique, default_value, validation | Restricciones de contenido y valores predeterminados |
widget, options, sort_order | Control del editor y orden de visualización |
searchable, indexed, translatable | Comportamiento de búsqueda, consulta y localización |
collection_id hace referencia a _emdash_collections.id, y cada slug de campo es único dentro de su colección.
Tablas de contenido por colección
Cada colección obtiene su propia tabla, con prefijo ec_. Una colección products con campos title y price produce una tabla con esta forma:
CREATE TABLE ec_products (
-- Columnas del sistema, presentes en cada tabla de contenido
id TEXT PRIMARY KEY,
slug TEXT,
status TEXT DEFAULT 'draft',
author_id TEXT,
primary_byline_id TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
published_at TEXT,
scheduled_at TEXT,
deleted_at TEXT,
version INTEGER DEFAULT 1,
live_revision_id TEXT,
draft_revision_id TEXT,
locale TEXT NOT NULL DEFAULT 'en',
translation_group TEXT,
-- Columnas de contenido, creadas a partir de definiciones de campos
title TEXT NOT NULL,
price REAL,
UNIQUE (slug, locale)
);
Las columnas reales dan a cada campo un tipo de base de datos, permiten índices y claves foráneas, y dejan que las herramientas de base de datos inspeccionen el esquema sin decodificar un blob JSON de contenido. La restricción unique permite que las traducciones compartan un slug mientras mantienen cada slug único dentro de un idioma. Todas las variantes de idioma de la misma entrada comparten un valor translation_group, que permite a EmDash encontrar las filas que son traducciones entre sí.
Los principales aspectos de datos permanecen separados:
| Aspecto | Ubicación | Tablas |
|---|---|---|
| Esquema | Tablas del sistema | _emdash_collections, _emdash_fields |
| Contenido | Tablas por colección | ec_posts, ec_products, … |
| Medios | Tabla separada + almacenamiento | Tabla media + almacenamiento configurado |
| Configuración | Tabla de opciones | options con prefijo site: |
Cambios de esquema en runtime
Agregar un campo a través de la UI de administración ejecuta estos pasos:
- Insertar la definición del campo en
_emdash_fields. - Agregar la columna correspondiente a la tabla
ec_*de la colección y crear un índice cuando el campo está configurado como indexado. - Actualizar los tipos de desarrollo generados para que el nuevo campo aparezca en las herramientas del editor.
La validación de contenido lee las definiciones de campos actuales y construye un esquema Zod cuando se crea o actualiza contenido. Cambiar el tipo SQL subyacente, la restricción required o unique, o el comportamiento de localización de un campo puede requerir una migración de contenido manual; SchemaRegistry rechaza cambios in situ no soportados en lugar de reconstruir la tabla implícitamente.
Validación en runtime
EmDash deriva un esquema Zod de los campos actuales de la colección. El generador delega los detalles de tipo y restricción a generateFieldSchema():
export function generateZodSchema(
collection: CollectionWithFields,
): z.ZodObject<Record<string, ZodType>> {
const shape: Record<string, ZodType> = {};
for (const field of collection.fields) {
shape[field.slug] = generateFieldSchema(field);
}
return z.object(shape);
}
El manejador de contenido también rechaza campos desconocidos, verifica valores de cadena requeridos y verifica referencias a otras colecciones.
Capa de datos
EmDash usa Kysely para SQL tipado a través de SQLite, libSQL, Cloudflare D1 y PostgreSQL. La configuración del sitio selecciona el adaptador de base de datos; la integración expone su factory de dialecto a través de virtual:emdash/dialect.
Loader de Live Content Collections
El contenido se sirve en runtime a través de Live Content Collections de Astro. emdashLoader() implementa la interfaz LiveLoader de Astro y se registra como una única colección _emdash:
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({ loader: emdashLoader() }),
};
La colección única _emdash envuelve cada colección de EmDash. getEmDashCollection("posts") proporciona el filtro de tipo posts, y el loader lo mapea a la tabla ec_posts.
Rutas de solicitud
Una solicitud de contenido desde una página Astro sigue esta ruta:
- La página llama a
getEmDashCollection()ogetEmDashEntry(). - El wrapper de consulta llama a
getLiveCollection()ogetLiveEntry()de Astro con la colección interna_emdashy el tipo de colección EmDash solicitado. emdashLoader()consulta la tablaec_*relevante a través de Kysely, aplicando reglas de publicación, idioma, filtro, ordenación y paginación.- El wrapper de consulta mapea las filas a entradas de Astro y carga sus firmas y términos de taxonomía.
- El componente de Astro renderiza las entradas devueltas.
El estado de vista previa y modo de edición viaja a través del contexto de solicitud, por lo que las mismas funciones de consulta pueden devolver contenido de borrador después de que el middleware verifica la solicitud.
Una solicitud de API de administración sigue una ruta separada:
- El middleware autentica la solicitud y almacena el usuario resuelto en
Astro.locals. - La ruta de API analiza la solicitud y verifica el permiso necesario para esa operación.
- La ruta delega la lógica de negocio a un manejador o repositorio.
- El manejador ejecuta hooks del ciclo de vida de plugins alrededor de la operación de base de datos cuando esa operación expone hooks.
- La ruta devuelve una respuesta JSON estándar de éxito o error a la aplicación de administración.
Internos del panel de administración
El admin es una aplicación de página única React. Astro sirve su shell y el middleware de autenticación protege las rutas de administración. Dentro de la aplicación, TanStack Router maneja la navegación, TanStack Query carga el estado del servidor, TanStack Table renderiza grillas de datos, React Hook Form y Zod gestionan formularios, TipTap edita Portable Text y Kumo proporciona el sistema de diseño.
Para la autenticación de sesión, el middleware redirige una solicitud de navegador no autenticada a la página de inicio de sesión y devuelve un error JSON para una solicitud de API no autenticada. Después de cargar un usuario activo, lo coloca en Astro.locals para la ruta:
const sessionUser = await resolveSessionUser(session);
if (!sessionUser?.id) {
if (isApiRoute) {
return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
}
const loginUrl = new URL("/_emdash/admin/login", getPublicOrigin(url, emdash?.config));
loginUrl.searchParams.set("redirect", url.pathname);
return context.redirect(loginUrl.toString());
}
Después de esta bifurcación, el middleware carga el usuario, rechaza cuentas faltantes o deshabilitadas, coloca el usuario activo en Astro.locals y continúa hacia la ruta.
UI dirigida por manifiesto
El admin no codifica esquemas de colección ni contribuciones de plugins. Obtiene GET /_emdash/api/manifest, que describe las colecciones actuales, campos, plugins, taxonomías, modo de autenticación y otras capacidades configuradas. Un manifiesto abreviado se ve así:
{
"collections": {
"posts": {
"label": "Blog Posts",
"labelSingular": "Post",
"supports": ["drafts", "revisions", "preview"],
"fields": {
"title": { "kind": "string", "label": "Title", "required": true }
}
}
},
"plugins": {
"audit-log": { "version": "0.2.1", "enabled": true }
},
"taxonomies": [
{ "name": "category", "label": "Categories", "hierarchical": true }
],
"version": "0.37.0"
}
El admin usa el manifiesto para construir la navegación de colecciones y los editores de campos. Como el endpoint lee el esquema en vivo, los cambios de colecciones y campos aparecen sin reconstruir la aplicación de administración.
UIs de admin de plugins
Los puntos de entrada de admin de plugins configurados se recopilan en virtual:emdash/admin-registry. El módulo generado usa importaciones estáticas para que el bundler pueda incluir los componentes React:
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
export const pluginAdmins = { seo: pluginAdmin0 };
Conversión de texto rico
Los campos de Portable Text usan TipTap, que está basado en ProseMirror. EmDash convierte Portable Text a ProseMirror cuando el editor carga y lo convierte de vuelta a Portable Text cuando la entrada se guarda. Los bloques desconocidos de plugins o importaciones se preservan como marcadores de solo lectura en lugar de descartarse.
Subidas firmadas
Las subidas de medios usan URLs firmadas directas al almacenamiento cuando el adaptador de almacenamiento las soporta y un endpoint de streaming del mismo origen en caso contrario:
- El cliente solicita un destino de subida desde
POST /_emdash/api/media/upload-url. EmDash crea un elemento de medios pendiente. - El cliente sube al destino devuelto. Los adaptadores compatibles con S3 pueden devolver una URL firmada que evita los límites de tamaño del cuerpo de la aplicación; los bindings nativos de R2 y el almacenamiento local devuelven un endpoint de streaming de EmDash.
- El cliente confirma la subida con
POST /_emdash/api/media/:id/confirm. - EmDash valida el archivo almacenado y marca el elemento de medios como listo.
Extensión del importador de contenido
El importador de WordPress usa una interfaz ImportSource extensible. Una fuente puede sondear una URL, analizar el contenido disponible contra el esquema actual y transmitir elementos de contenido normalizados:
interface ImportSource {
id: string;
name: string;
description: string;
icon: "upload" | "globe" | "wordpress" | "plug";
requiresFile?: boolean;
canProbe?: boolean;
probe?(url: string): Promise<SourceProbeResult | null>;
analyze(input: SourceInput, context: ImportContext): Promise<ImportAnalysis>;
fetchContent(input: SourceInput, options: FetchOptions): AsyncGenerator<NormalizedItem>;
fetchMedia?(url: string, input: SourceInput): Promise<Blob>;
}
La fuente WXR importa archivos de exportación de WordPress. La fuente de conector importa directamente desde sitios con el plugin EmDash para WordPress. Una fuente REST separada detecta sitios WordPress públicos, pero dirige al usuario a una exportación WXR porque la importación directa por REST no está implementada. Registra otra fuente cuando un importador pueda producir las mismas formas de análisis normalizado y elementos de contenido.