Este tutorial crea un plugin sandboxed que registra eventos de guardado de contenido y expone una ruta de salud pequeña. Scaffoldearás el paquete con la CLI del plugin, añadirás un hook y una ruta, registrarás el plugin en un sitio EmDash y confirmarás que ambos handlers se ejecutan.
Si aún no has elegido un formato de plugin, lee primero Elegir un formato de plugin.
Requisitos previos
Necesitas:
- Node.js y pnpm;
- un sitio EmDash con un runner de sandbox configurado; y
- un handle o DID de cuenta Atmosphere para el campo publisher del manifiesto.
Scaffoldear el plugin
-
Ejecuta el scaffolder del plugin desde el directorio que contendrá el nuevo proyecto.
pnpm dlx @emdash-cms/plugin-cli init save-logEl comando pide el publisher, autor, contacto de seguridad y repositorio fuente, y luego muestra un resumen del proyecto antes de crear esta estructura:
save-log/ ├── .agents/ │ └── skills -> ../skills ├── .claude/ │ ├── CLAUDE.md -> ../AGENTS.md │ └── skills -> ../skills ├── AGENTS.md ├── emdash-plugin.jsonc ├── package.json ├── pnpm-workspace.yaml ├── README.md ├── skills/ │ └── creating-plugins/SKILL.md ├── src/ │ └── plugin.ts ├── tests/ │ └── plugin.test.ts ├── tsconfig.json ├── vitest.config.ts └── .gitignore -
Instala las dependencias del paquete generado.
cd save-log pnpm install
Definir el acceso y el storage del plugin
emdash-plugin.jsonc contiene la identidad del plugin, la información del registro y el contrato de confianza. Añade la capability content:read porque content:afterSave expone el contenido guardado al plugin. Declara una colección de storage events para que el hook pueda mantener un registro consultable de cada guardado.
El siguiente manifiesto contiene los campos usados en este tutorial. Conserva los valores de publisher, autor y seguridad producidos por el scaffolder.
{
"$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",
"slug": "save-log",
"publisher": "did:plc:abc123def456",
"license": "MIT",
"author": { "name": "Jane Doe", "url": "https://example.com" },
"security": { "email": "security@example.com" },
"description": "Records content-save events.",
"capabilities": ["content:read"],
"allowedHosts": [],
"storage": {
"events": { "indexes": ["savedAt"] },
},
}
La declaración content:read indica al operador del sitio que el hook recibe contenido guardado y es obligatoria cuando este formato de plugin se ejecuta in-process. Acceder a ctx.storage.events lanzaría si la colección estuviera ausente. La referencia del manifiesto explica el resto de campos y las reglas de validación.
Añadir el hook y la ruta
Sustituye el src/plugin.ts generado por la siguiente definición de runtime:
import type { SandboxedPlugin } from "emdash/plugin";
const plugin: SandboxedPlugin = {
hooks: {
"content:afterSave": {
handler: async (event, ctx) => {
const savedAt = new Date().toISOString();
const contentId = String(event.content.id);
await ctx.storage.events.put(`${savedAt}:${contentId}`, {
savedAt,
collection: event.collection,
contentId,
});
ctx.log.info("Content save recorded", {
collection: event.collection,
contentId,
});
},
},
},
routes: {
health: {
public: true,
handler: async (_routeCtx, ctx) => {
return { ok: true, plugin: ctx.plugin.id };
},
},
},
};
export default plugin;
src/plugin.ts asigna la definición a una constante tipada como SandboxedPlugin y la exporta como default. La anotación da al hook y a la ruta sus tipos de parámetros sin añadir el runtime de EmDash al bundle ni producir rutas de declaración específicas del gestor de paquetes.
Los handlers de hooks reciben (event, ctx). Los handlers de rutas reciben (routeCtx, ctx). La ruta health es pública y de solo lectura, así que puede comprobarse sin una sesión de administración. Las rutas públicas están expuestas a Internet; Rutas API explica la autenticación y las reglas de origen del navegador antes de exponer datos reales o mutaciones.
Actualizar el test generado
El test scaffolded invoca la ruta original hello a través del host de transporte Worker Loader. Sustitúyelo por un test de la ruta health:
import { afterEach, describe, expect, it } from "vitest";
import { createPluginTestHost, type PluginTestHost } from "@emdash-cms/plugin-test";
let host: PluginTestHost | undefined;
afterEach(async () => {
await host?.dispose();
host = undefined;
});
describe("health route", () => {
it("identifies the running plugin", async () => {
host = await createPluginTestHost();
const result = await host.invokeRoute("health");
expect(result).toEqual({ ok: true, plugin: "save-log" });
});
});
El test construye el plugin e invoca la ruta a través de Worker Loader y PluginBridge. La guía de testing de plugins sandboxed cubre hooks, fixtures de contenido, aserciones de storage y los límites de las pruebas locales con workerd.
Validar y construir
Ejecuta el test generado, valida el manifiesto y construye los artefactos npm.
pnpm run validate
pnpm run typecheck
pnpm run test
pnpm run build
La build crea:
dist/plugin.mjs, con el código del hook y de la ruta;dist/manifest.json, con el manifiesto de runtime y los nombres de hooks y rutas descubiertos; ydist/index.mjs, el descriptor exportado por defecto que importa un sitio.
dist/ es salida generada. El scaffold lo excluye de Git porque la build del plugin lo recrea.
Registrar el plugin
Instala el paquete local en tu sitio EmDash. Ejecuta este comando desde el directorio del sitio y ajusta la ruta relativa si los proyectos no son hermanos.
pnpm add file:../save-log
Importa la exportación por defecto generada en astro.config.mjs y añádela a sandboxed:
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import saveLog from "save-log";
export default defineConfig({
integrations: [
emdash({
sandboxed: [saveLog],
sandboxRunner: "@emdash-cms/sandbox-workerd/sandbox",
}),
],
});
Este ejemplo usa el runner workerd de Node.js. Conserva el runner ya configurado por tu sitio si usa Cloudflare Workers u otra configuración admitida.
Ejecutar el plugin
Arranca ambos procesos de desarrollo:
- Ejecuta
pnpm deven el directorio del plugin. La CLI reconstruye el plugin cuando cambia su código fuente o el manifiesto. - Ejecuta el comando de desarrollo del sitio en el directorio del sitio.
Abre la siguiente ruta en el sitio:
http://localhost:4321/_emdash/api/plugins/save-log/health
La respuesta contiene el envelope estándar de la API y el valor devuelto por el plugin:
{
"success": true,
"data": { "ok": true, "plugin": "save-log" },
}
Guarda una entrada en el admin de EmDash. El registro del sitio contiene Content save recorded, y el hook escribe un elemento en la colección events del plugin.
Seguir construyendo
- Hooks explica eventos de hooks, capabilities, orden y errores.
- Rutas API cubre validación, permisos, rutas públicas y exposición MCP.
- Block Kit añade una página de administración sin enviar JavaScript del navegador.
- Ajustes almacena la configuración del plugin específica del sitio.
- Storage cubre consultas indexadas y paginación.
- Testing cubre tests de transporte sandbox directo y tests de acciones del host respaldados por runtime.
- Empaquetar y publicar publica el plugin en el registro.