Ce guide construit un plugin sandboxed minimal à partir de zéro. Le plugin enregistre chaque sauvegarde de contenu et expose une seule route API. Il s’exécute dans un runtime isolé fourni par le sandbox runner configuré. Le même code s’exécute également en processus lorsqu’un opérateur de site le déplace de sandboxed: [] vers plugins: [], par exemple sur une plateforme sans sandbox runner.
Si vous n’avez pas encore décidé entre sandboxed et native, lisez d’abord Choisir un format de plugin.
Deux pièces
Un plugin sandboxed est :
emdash-plugin.jsonc— un manifest édité manuellement : identité, le contrat de confiance (capabilities, hosts, storage), et champs de profil. Pas de code.src/plugin.ts— le runtime : hooks et routes. Imports de types uniquement depuisemdash/plugin; pas d’import runtime deemdash.
emdash-plugin build lit les deux et émet les artefacts dist/ qu’un site consomme.
L’exemple suivant montre la disposition des fichiers d’un plugin complet :
my-plugin/
├── emdash-plugin.jsonc # Identité + contrat de confiance + profil
├── src/
│ └── plugin.ts # Hooks, routes — s'exécute dans le runtime sandbox
├── package.json
└── tsconfig.json
Configurer le package
-
Créez le répertoire et un
package.json. Le build estemdash-plugin build; il n’y a pas d’invocationtsdownà écrire.{ "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" } }"."est le descripteur généré qu’un site importe ;"./sandbox"est le fichier runtime construit.emdash-plugin buildgénère les deux. -
Ajoutez un
tsconfig.json:{ "compilerOptions": { "target": "ES2022", "module": "preserve", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "verbatimModuleSyntax": true, "skipLibCheck": true, "types": [] }, "include": ["src/**/*"], "exclude": ["node_modules"] }
Écrire le manifest
emdash-plugin.jsonc porte l’identité du plugin (slug), son contrat de confiance (capabilities, allowedHosts, storage), les champs de profil, et le publisher pin.
L’exemple suivant montre un manifest complet pour le 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"] } }
}
Notes sur ce manifest :
slugest un identifiant sécurisé pour URL, pas le nom du package npm./^[a-z][a-z0-9_-]*$/, max 64 caractères. C’est un segment de chemin unique dans les URLs de routes du plugin (/_emdash/api/plugins/<slug>/...) et fait partie des identifiants SQL générés pour les index de storage, donc@,/, les chiffres initiaux, et les majuscules échouent. Associez unslugsans scope (plugin-hello) avec un nom de package npm scopé.storagedéclare les collections à l’avance.ctx.storage.eventsfonctionne au runtime uniquement parce queeventsest déclaré ici. Accéder à une collection non déclarée lève une erreur.versionest omise. Le build la lit depuispackage.jsonpour qu’il y ait une seule source de vérité. Voir la référence du manifest.- Le contrat de confiance est un consentement. Modifier
capabilities,allowedHosts, oustorageultérieurement nécessite une mise à jour de version — les sites installés ont consenti à l’ancien contrat.
Écrire le runtime
src/plugin.ts exporte par défaut un objet simple annoté avec satisfies SandboxedPlugin. emdash/plugin fournit uniquement des types, donc un plugin sandboxed n’a pas de dépendance runtime sur emdash.
L’exemple suivant enregistre chaque sauvegarde de contenu dans le storage du plugin et expose une route recent qui renvoie les dix dernières sauvegardes :
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;
Notes sur le fichier runtime :
satisfies SandboxedPlugintype tout. Il infèreeventdepuis le nom du hook (avec le type d’événement canonique complet) etctxcommePluginContext, donc les handlers n’ont pas besoin d’annotations de paramètres. Une clé de hook mal tapée comme"content:afterSav"est une erreur de compilation.- Les handlers de hooks prennent
(event, ctx). La forme de l’événement dépend du nom du hook ; voir le guide des Hooks. - Les handlers de routes prennent
(routeCtx, ctx)— deux arguments.routeCtxest{ input, request, requestMeta? };ctxest le mêmePluginContext. Les routes sont accessibles à/_emdash/api/plugins/<slug>/<route-name>. ctx.storage.eventsfonctionne parce queeventsest déclaré dans le manifest.ctx.kvest toujours disponible — un key-value store par plugin avecget,set,delete,list(prefix).
Enregistrer le plugin
Dans le astro.config.mjs du site, importez l’export par défaut du plugin et passez-le. Les plugins sandboxed vont dans sandboxed: [] ; les plugins en processus vont dans plugins: []. Un plugin sandboxed fonctionne dans les deux. L’exemple ci-dessous utilise 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 est la pièce interchangeable. L’exemple utilise sandbox() de @emdash-cms/cloudflare, le runner que la plupart des sites utilisent aujourd’hui. Si aucun runner n’est configuré (ou si le runner configuré n’est pas disponible sur la plateforme actuelle), les plugins sandboxed: [] sont ignorés au démarrage — déplacez le plugin dans plugins: [] pour l’exécuter en processus.
Construire et exécuter
Depuis le répertoire du plugin :
emdash-plugin validate # vérifier le schéma du manifest d'abord
emdash-plugin build # émettre dist/
Pour une boucle d’édition, exécutez emdash-plugin dev (reconstruit à la sauvegarde, garde le dernier bon dist/ en cas d’échec du build). Dans le site, installez ou liez le plugin (pnpm add file:../plugin-hello ou un lien workspace) et démarrez le serveur dev. Sauvegardez un contenu dans l’admin et vous devriez voir Content saved … dans les logs ; GET /_emdash/api/plugins/plugin-hello/recent renvoie les dix derniers événements de sauvegarde.
Que lire ensuite
- Le manifest — chaque champ, le contrat de confiance, publisher pinning
- La CLI
emdash-plugin—build,dev,bundle - Hooks — l’ensemble complet d’événements
- Routes API — validation d’input, routes publiques, erreurs
- Storage et KV — options de requête, index, opérations batch
- Capabilities et sécurité — accès au contenu, réseau, allowlists d’hôtes
- Bundling et publishing — envoi vers le marketplace