Questa guida costruisce un plugin sandboxed minimale da zero. Il plugin registra ogni salvataggio di contenuto ed espone una singola route API. Viene eseguito in un runtime isolato fornito dal sandbox runner configurato. Lo stesso codice viene eseguito anche in-process quando un operatore del sito lo sposta da sandboxed: [] a plugins: [], ad esempio su una piattaforma senza sandbox runner.
Se non hai ancora deciso tra sandboxed e native, leggi prima Scegliere un formato di plugin.
Due pezzi
Un plugin sandboxed è:
emdash-plugin.jsonc— un manifest modificato manualmente: identità, il contratto di fiducia (capabilities, hosts, storage), e campi profilo. Nessun codice.src/plugin.ts— il runtime: hooks e routes. Import solo di tipi daemdash/plugin; nessun import runtime diemdash.
emdash-plugin build legge entrambi ed emette gli artefatti dist/ che un sito consuma.
L’esempio seguente mostra il layout dei file di un plugin completo:
my-plugin/
├── emdash-plugin.jsonc # Identità + contratto di fiducia + profilo
├── src/
│ └── plugin.ts # Hooks, routes — viene eseguito nel runtime sandbox
├── package.json
└── tsconfig.json
Configurare il package
-
Crea la directory e un
package.json. Il build èemdash-plugin build; non c’è invocazionetsdownda scrivere.{ "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" } }"."è il descrittore generato che un sito importa;"./sandbox"è il file runtime costruito.emdash-plugin buildgenera entrambi. -
Aggiungi un
tsconfig.json:{ "compilerOptions": { "target": "ES2022", "module": "preserve", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "verbatimModuleSyntax": true, "skipLibCheck": true, "types": [] }, "include": ["src/**/*"], "exclude": ["node_modules"] }
Scrivere il manifest
emdash-plugin.jsonc porta l’identità del plugin (slug), il suo contratto di fiducia (capabilities, allowedHosts, storage), campi profilo, e il publisher pin.
L’esempio seguente mostra un manifest completo per il 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"] } }
}
Note su questo manifest:
slugè un id sicuro per URL, non il nome del package npm./^[a-z][a-z0-9_-]*$/, max 64 caratteri. È un singolo segmento di percorso nelle URL delle route del plugin (/_emdash/api/plugins/<slug>/...) e parte degli identificatori SQL generati per gli indici di storage, quindi@,/, cifre iniziali, e maiuscole falliscono. Abbina unoslugsenza scope (plugin-hello) con un nome di package npm con scope.storagedichiara le collezioni in anticipo.ctx.storage.eventsfunziona al runtime solo perchéeventsè dichiarato qui. Accedere a una collezione non dichiarata genera un errore.versionè omessa. Il build la legge dapackage.jsoncosì c’è un’unica fonte di verità. Vedi il riferimento del manifest.- Il contratto di fiducia è consenso. Modificare
capabilities,allowedHosts, ostoragesuccessivamente richiede un bump di versione — i siti installati hanno acconsentito al vecchio contratto.
Scrivere il runtime
src/plugin.ts esporta per default un oggetto semplice annotato con satisfies SandboxedPlugin. emdash/plugin fornisce solo tipi, quindi un plugin sandboxed non ha dipendenza runtime su emdash.
L’esempio seguente registra ogni salvataggio di contenuto nello storage del plugin ed espone una route recent che restituisce gli ultimi dieci salvataggi:
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;
Note sul file runtime:
satisfies SandboxedPlugintipizza tutto. Inferisceeventdal nome dell’hook (con il tipo di evento canonico completo) ectxcomePluginContext, quindi gli handler non necessitano annotazioni di parametri. Una chiave di hook errata come"content:afterSav"è un errore di compilazione.- Gli handler di hooks prendono
(event, ctx). La forma dell’evento dipende dal nome dell’hook; vedi la guida agli Hooks. - Gli handler di routes prendono
(routeCtx, ctx)— due argomenti.routeCtxè{ input, request, requestMeta? };ctxè lo stessoPluginContext. Le routes sono raggiungibili a/_emdash/api/plugins/<slug>/<route-name>. ctx.storage.eventsfunziona perchéeventsè dichiarato nel manifest.ctx.kvè sempre disponibile — un key-value store per plugin conget,set,delete,list(prefix).
Registrare il plugin
Nel astro.config.mjs del sito, importa l’export di default del plugin e passalo. I plugin sandboxed vanno in sandboxed: []; i plugin in-process vanno in plugins: []. Un plugin sandboxed funziona in entrambi. L’esempio seguente 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 è il pezzo intercambiabile. L’esempio usa sandbox() da @emdash-cms/cloudflare, il runner che la maggior parte dei siti usa oggi. Se nessun runner è configurato (o il runner configurato non è disponibile sulla piattaforma corrente), i plugin sandboxed: [] vengono saltati all’avvio — sposta il plugin in plugins: [] per eseguirlo in-process.
Costruire ed eseguire
Dalla directory del plugin:
emdash-plugin validate # controlla lo schema del manifest prima
emdash-plugin build # emetti dist/
Per un ciclo di modifica, esegui emdash-plugin dev (ricostruisce al salvataggio, mantiene l’ultimo dist/ buono in caso di build fallito). Nel sito, installa o collega il plugin (pnpm add file:../plugin-hello o un link workspace) e avvia il dev server. Salva un contenuto nell’admin e dovresti vedere Content saved … nei log; GET /_emdash/api/plugins/plugin-hello/recent restituisce gli ultimi dieci eventi di salvataggio.
Cosa leggere successivamente
- Il manifest — ogni campo, il contratto di fiducia, publisher pinning
- La CLI
emdash-plugin—build,dev,bundle - Hooks — l’insieme completo di eventi
- Route API — validazione input, route pubbliche, errori
- Storage e KV — opzioni di query, indici, operazioni batch
- Capabilities e sicurezza — accesso ai contenuti, rete, allowlist di host
- Bundling e publishing — spedizione al marketplace