Este tutorial cria um plugin sandboxed que registra eventos de salvamento de conteúdo e expõe uma pequena rota de saúde. Você vai gerar o pacote com a CLI do plugin, adicionar um hook e uma rota, registrar o plugin em um site EmDash e confirmar que ambos os handlers são executados.
Se ainda não escolheu um formato de plugin, leia primeiro Escolher um formato de plugin.
Pré-requisitos
Você precisa de:
- Node.js e pnpm;
- um site EmDash com um runner de sandbox configurado; e
- um handle ou DID de conta Atmosphere para o campo publisher do manifesto.
Gerar o plugin
-
Execute o gerador do plugin a partir do diretório que conterá o novo projeto.
pnpm dlx @emdash-cms/plugin-cli init save-logO comando pede o publisher, autor, contato de segurança e repositório fonte, depois mostra um resumo do projeto antes de criar esta estrutura:
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 -
Instale as dependências do pacote gerado.
cd save-log pnpm install
Definir o acesso e o storage do plugin
emdash-plugin.jsonc contém a identidade do plugin, informações do registry e o contrato de confiança. Adicione a capability content:read porque content:afterSave expõe o conteúdo salvo ao plugin. Declare uma collection de storage events para que o hook possa manter um registro consultável de cada salvamento.
O manifesto a seguir contém os campos usados neste tutorial. Mantenha os valores de publisher, author e security produzidos pelo gerador.
{
"$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"] },
},
}
A declaração content:read diz ao operador do site que o hook recebe conteúdo salvo e é necessária quando este formato de plugin é executado in-process. Acessar ctx.storage.events lançaria se a collection estivesse ausente. A referência do manifesto explica os demais campos e as regras de validação.
Adicionar o hook e a rota
Substitua o src/plugin.ts gerado pela seguinte definição 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 atribui a definição a uma constante tipada como SandboxedPlugin e a exporta como default. A anotação dá ao hook e à rota seus tipos de parâmetros sem adicionar o runtime EmDash ao bundle nem produzir caminhos de declaração específicos do gerenciador de pacotes.
Handlers de hooks recebem (event, ctx). Handlers de rotas recebem (routeCtx, ctx). A rota health é pública e somente leitura, então pode ser verificada sem uma sessão de administração. Rotas públicas são voltadas à internet; Rotas de API explica autenticação e regras de origin do navegador antes de expor dados reais ou mutações.
Atualizar o teste gerado
O teste gerado invoca a rota original hello pelo host de transporte Worker Loader. Substitua-o por um teste da rota 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" });
});
});
O teste constrói o plugin e invoca a rota pelo Worker Loader e PluginBridge. O guia de teste de plugins sandboxed cobre hooks, fixtures de conteúdo, asserções de storage e os limites de testes workerd locais.
Validar e compilar
Execute o teste gerado, valide o manifesto e construa os artefatos npm.
pnpm run validate
pnpm run typecheck
pnpm run test
pnpm run build
A build cria:
dist/plugin.mjs, com o código do hook e da rota;dist/manifest.json, com o manifesto de runtime e os nomes de hooks e rotas descobertos; edist/index.mjs, o descriptor exportado por padrão que um site importa.
dist/ é saída gerada. O gerador a exclui do Git porque a build do plugin a recria.
Registrar o plugin
Instale o pacote local no seu site EmDash. Execute este comando a partir do diretório do site e ajuste o caminho relativo se os projetos não forem irmãos.
pnpm add file:../save-log
Importe a exportação padrão gerada em astro.config.mjs e adicione-a 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 exemplo usa o runner workerd do Node.js. Mantenha o runner já configurado pelo seu site se ele usar Cloudflare Workers ou outra configuração suportada.
Executar o plugin
Inicie ambos os processos de desenvolvimento:
- Execute
pnpm devno diretório do plugin. A CLI reconstrói o plugin quando a fonte ou o manifesto muda. - Execute o comando de desenvolvimento do site no diretório do site.
Abra a seguinte rota no site:
http://localhost:4321/_emdash/api/plugins/save-log/health
A resposta contém o envelope padrão da API e o valor retornado pelo plugin:
{
"success": true,
"data": { "ok": true, "plugin": "save-log" },
}
Salve uma entrada no admin EmDash. O log do site contém Content save recorded, e o hook escreve um item na collection events do plugin.
Continuar construindo
- Hooks explica eventos de hooks, capabilities, ordem e erros.
- Rotas de API cobre validação, permissões, rotas públicas e exposição MCP.
- Block Kit adiciona uma página de administração sem enviar JavaScript do navegador.
- Configurações armazena configuração do plugin específica do site.
- Storage cobre consultas indexadas e paginação.
- Testing cobre testes de transporte sandbox diretos e testes de ações do host respaldados por runtime.
- Empacotar e publicar publica o plugin no registry.