Esta guía construye un plugin sandboxed mínimo desde cero. El plugin registra cada guardado de contenido y expone una única ruta API. Se ejecuta en un runtime aislado proporcionado por el sandbox runner configurado. El mismo código también se ejecuta en proceso cuando un operador del sitio lo mueve de sandboxed: [] a plugins: [], por ejemplo en una plataforma sin sandbox runner.
Si aún no has decidido entre sandboxed y native, lee primero Elegir un formato de plugin.
Dos piezas
Un plugin sandboxed es:
emdash-plugin.jsonc— un manifest editado manualmente: identidad, el contrato de confianza (capabilities, hosts, storage), y campos de perfil. Sin código.src/plugin.ts— el runtime: hooks y routes. Imports solo de tipos desdeemdash/plugin; sin import runtime deemdash.
emdash-plugin build lee ambos y emite los artefactos dist/ que un sitio consume.
El siguiente ejemplo muestra el diseño de archivos de un plugin completo:
my-plugin/
├── emdash-plugin.jsonc # Identidad + contrato de confianza + perfil
├── src/
│ └── plugin.ts # Hooks, routes — se ejecuta en el runtime sandbox
├── package.json
└── tsconfig.json
Configurar el paquete
-
Crea el directorio y un
package.json. El build esemdash-plugin build; no hay invocación detsdownque escribir.{ "name": "@my-org/plugin-hello", "version": "0.1.0", "type": "module", "main": "dist/index.mjs", "exports": { ".": { "import": "./dist/index.mjs", "types": "./dist/index.d.mts" }, "./sandbox": "./dist/plugin.mjs" }, "files": ["dist", "emdash-plugin.jsonc"], "scripts": { "build": "emdash-plugin build", "dev": "emdash-plugin dev" }, "peerDependencies": { "emdash": ">=0.13.0" }, "devDependencies": { "@emdash-cms/plugin-cli": "0.2.0", "emdash": ">=0.13.0", "typescript": "^5.9.0" } }"."es el descriptor generado que un sitio importa;"./sandbox"es el archivo runtime construido.emdash-plugin buildgenera ambos. -
Añade un
tsconfig.json:{ "compilerOptions": { "target": "ES2022", "module": "preserve", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "verbatimModuleSyntax": true, "skipLibCheck": true, "types": [] }, "include": ["src/**/*"], "exclude": ["node_modules"] }
Escribir el manifest
emdash-plugin.jsonc lleva la identidad del plugin (slug), su contrato de confianza (capabilities, allowedHosts, storage), campos de perfil, y el publisher pin.
El siguiente ejemplo muestra un manifest completo para el plugin hello:
{
"$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",
"slug": "plugin-hello",
"publisher": "did:plc:abc123def456", // your Atmosphere account DID
"license": "MIT",
"author": { "name": "Jane Doe", "url": "https://example.com" },
"security": { "email": "security@example.com" },
"capabilities": [],
"allowedHosts": [],
"storage": { "events": { "indexes": ["timestamp"] } }
}
Notas sobre este manifest:
sluges un id seguro para URL, no el nombre del paquete npm./^[a-z][a-z0-9_-]*$/, máx 64 caracteres. Es un único segmento de ruta en las URLs de rutas del plugin (/_emdash/api/plugins/<slug>/...) y parte de identificadores SQL generados para índices de storage, así que@,/, dígitos iniciales, y mayúsculas fallan. Combina unslugsin scope (plugin-hello) con un nombre de paquete npm con scope.storagedeclara colecciones por adelantado.ctx.storage.eventsfunciona en runtime solo porqueeventsestá declarado aquí. Acceder a una colección no declarada lanza un error.versionse omite. El build lo lee depackage.jsonpara que haya una única fuente de verdad. Ver la referencia del manifest.- El contrato de confianza es consentimiento. Cambiar
capabilities,allowedHosts, ostoragemás tarde requiere un incremento de versión — los sitios instalados consintieron al contrato antiguo.
Escribir el runtime
src/plugin.ts exporta por defecto un objeto simple anotado con satisfies SandboxedPlugin. emdash/plugin proporciona solo tipos, así que un plugin sandboxed no tiene dependencia runtime en emdash.
El siguiente ejemplo registra cada guardado de contenido en el storage del plugin y expone una ruta recent que devuelve los últimos diez guardados:
import type { SandboxedPlugin } from "emdash/plugin";
export default {
hooks: {
"content:afterSave": {
handler: async (event, ctx) => {
ctx.log.info("Content saved", {
collection: event.collection,
id: event.content.id,
});
await ctx.storage.events.put(`save-${Date.now()}`, {
timestamp: new Date().toISOString(),
collection: event.collection,
contentId: event.content.id,
});
},
},
},
routes: {
recent: {
handler: async (_routeCtx, ctx) => {
const result = await ctx.storage.events.query({ limit: 10 });
return { events: result.items };
},
},
},
} satisfies SandboxedPlugin;
Notas sobre el archivo runtime:
satisfies SandboxedPlugintipea todo. Infiereeventdesde el nombre del hook (con el tipo de evento canónico completo) yctxcomoPluginContext, así que los handlers no necesitan anotaciones de parámetros. Una clave de hook mal escrita como"content:afterSav"es un error de compilación.- Los handlers de hooks toman
(event, ctx). La forma del evento depende del nombre del hook; ver la guía de Hooks. - Los handlers de routes toman
(routeCtx, ctx)— dos argumentos.routeCtxes{ input, request, requestMeta? };ctxes el mismoPluginContext. Las routes son accesibles en/_emdash/api/plugins/<slug>/<route-name>. ctx.storage.eventsfunciona porqueeventsestá declarado en el manifest.ctx.kvsiempre está disponible — un key-value store por plugin conget,set,delete,list(prefix).
Registrar el plugin
En el astro.config.mjs del sitio, importa la exportación por defecto del plugin y pásala. Los plugins sandboxed van en sandboxed: []; los plugins en proceso van en plugins: []. Un plugin sandboxed funciona en ambos. El ejemplo siguiente usa sandboxed::
import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { sandbox } from "@emdash-cms/cloudflare";
import hello from "@my-org/plugin-hello";
export default defineConfig({
integrations: [
emdash({
sandboxed: [hello],
sandboxRunner: sandbox(),
}),
],
});
sandboxRunner es la pieza intercambiable. El ejemplo usa sandbox() de @emdash-cms/cloudflare, el runner que la mayoría de sitios usan hoy. Si no hay runner configurado (o el runner configurado no está disponible en la plataforma actual), los plugins sandboxed: [] se omiten al inicio — mueve el plugin a plugins: [] para ejecutarlo en proceso.
Construir y ejecutar
Desde el directorio del plugin:
emdash-plugin validate # verifica el schema del manifest primero
emdash-plugin build # emite dist/
Para un bucle de edición, ejecuta emdash-plugin dev (reconstruye al guardar, mantiene el último dist/ bueno en un build fallido). En el sitio, instala o enlaza el plugin (pnpm add file:../plugin-hello o un enlace de workspace) e inicia el servidor dev. Guarda un contenido en el admin y deberías ver Content saved … en los logs; GET /_emdash/api/plugins/plugin-hello/recent devuelve los últimos diez eventos de guardado.
Qué leer a continuación
- El manifest — cada campo, el contrato de confianza, publisher pinning
- La CLI
emdash-plugin—build,dev,bundle - Hooks — el conjunto completo de eventos
- Rutas API — validación de input, rutas públicas, errores
- Storage y KV — opciones de query, índices, operaciones batch
- Capabilities y seguridad — acceso a contenido, red, allowlists de hosts
- Bundling y publishing — envío al marketplace