Portez un plugin WordPress en séparant son comportement de contenu, ses données stockées, ses routes HTTP et son interface utilisateur. Choisissez ensuite le format de plugin EmDash qui prend en charge ces besoins.
Vérifier si le plugin a sa place dans EmDash
Les bons candidats possèdent un comportement indépendant du cœur WordPress, comme la validation de contenu, les appels API externes, le traitement en arrière-plan, des enregistrements personnalisés, des réglages ou un outil d’administration.
Ne portez pas un plugin dont le seul rôle est d’implémenter une fonction WordPress déjà remplacée par Astro ou EmDash. Exemples : cache de pages PHP, règles de réécriture WordPress, sélection de modèles de thème ou modifications des globales du cœur.
Pour un plugin qui ne définit qu’un type de publication ou des champs personnalisés avec peu de comportement à l’exécution, créez plutôt une collection EmDash et un fichier seed.
Choisir sandboxed ou native
Commencez par Choisir un format de plugin. Les deux formats partagent les noms de hooks et les API PluginContext, mais leurs paquets sources diffèrent.
| Exigence | Sandboxed | Native |
|---|---|---|
| Installation via le registry | Oui | Non |
| Runtime isolé | Oui, avec un runner configuré | Non |
| Hooks, routes, KV, stockage structuré | Oui | Oui |
| Pages admin Block Kit | Oui | Oui |
| Composants React admin personnalisés | Non | Oui |
| Composants Astro pour le rendu public | Non | Oui |
| Fragments de page bruts | Non | Oui |
Choisissez native uniquement lorsque le port nécessite une surface build-time ou UI exclusive à native.
Format de paquet sandboxed
emdash-plugin init crée le format sandboxed actuel :
my-plugin/
├── emdash-plugin.jsonc
├── src/
│ └── plugin.ts
├── tests/
│ └── plugin.test.ts
├── package.json
└── tsconfig.json
Le manifeste contient l’identité, l’éditeur, les capabilities, les hôtes autorisés et les déclarations de stockage. La version provient normalement de package.json.
Le manifeste suivant déclare une collection de stockage indexée et la capability requise par content:afterSave :
{
"$schema": "./node_modules/@emdash-cms/plugin-cli/schemas/emdash-plugin.schema.json",
"slug": "read-time",
"publisher": "did:plc:abc123def456",
"license": "MIT",
"author": { "name": "Example Author" },
"security": { "email": "security@example.com" },
"capabilities": ["content:read"],
"allowedHosts": [],
"storage": {
"calculations": { "indexes": ["contentId", "updatedAt"] }
}
}
src/plugin.ts exporte par défaut un objet plain typé avec SandboxedPlugin. Les handlers de hooks sandboxed utilisent { handler } ; les handlers de routes sandboxed reçoivent (routeCtx, ctx) :
import type { SandboxedPlugin } from "emdash/plugin";
export default {
hooks: {
"content:afterSave": {
handler: async (event, ctx) => {
await ctx.storage.calculations.put(event.content.id, {
contentId: event.content.id,
updatedAt: new Date().toISOString(),
});
},
},
},
routes: {
recent: {
handler: async (_routeCtx, ctx) => {
const result = await ctx.storage.calculations.query({
orderBy: { updatedAt: "desc" },
limit: 10,
});
return { items: result.items };
},
},
},
} satisfies SandboxedPlugin;
La route est disponible à /_emdash/api/plugins/read-time/recent. Un champ de stockage doit être déclaré comme index avant qu’une requête puisse filtrer ou trier dessus.
Compilez le paquet avec emdash-plugin build ; n’ajoutez pas de descripteur src/index.ts écrit à la main à ce format. Lisez Votre premier plugin sandboxed pour le package.json généré, la sortie de build et l’enregistrement sur le site.
Format de paquet native
Un paquet native exporte à la fois une factory de descripteur pour astro.config.mjs et une factory runtime construite avec definePlugin(). Les points d’entrée admin et Astro optionnels sont des exports de paquet séparés.
my-native-plugin/
├── src/
│ ├── index.ts
│ ├── admin.tsx
│ └── astro/
│ └── index.ts
├── package.json
└── tsconfig.json
Le point d’entrée native réduit suivant montre les deux pièces requises :
import { definePlugin } from "emdash";
import type { PluginDescriptor } from "emdash";
export interface ReadTimeOptions {
wordsPerMinute?: number;
}
export function readTimePlugin(options: ReadTimeOptions = {}): PluginDescriptor {
return {
id: "read-time",
version: "0.1.0",
format: "native",
entrypoint: "@example/plugin-read-time",
capabilities: ["content:read"],
options,
};
}
export function createPlugin(options: ReadTimeOptions = {}) {
return definePlugin({
id: "read-time",
version: "0.1.0",
capabilities: ["content:read"],
admin: {
settingsSchema: {
wordsPerMinute: {
type: "number",
label: "Words per minute",
default: options.wordsPerMinute ?? 200,
min: 1,
},
},
},
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved", { id: event.content.id });
},
},
});
}
export default createPlugin;
Les handlers de hooks native peuvent être des fonctions directement. Les handlers de routes native reçoivent un seul argument de contexte combiné. Gardez alignées les copies descripteur et runtime de id, version, capabilities et points d’entrée.
Lisez Votre premier plugin native avant d’ajouter des pages admin React, des renderers Portable Text ou des fragments de page.
Mapper le comportement WordPress
Hooks
Mappez l’intention d’une action ou d’un filtre WordPress, pas seulement le nom :
| WordPress | EmDash |
|---|---|
register_activation_hook() | plugin:install à la première installation, ou plugin:activate à l’activation |
register_uninstall_hook() | plugin:uninstall |
wp_insert_post_data | content:beforeSave |
save_post | content:afterSave |
before_delete_post | content:beforeDelete |
deleted_post | content:afterDelete |
wp_handle_upload_prefilter | media:beforeUpload |
add_attachment | media:afterUpload |
Les événements de hooks ont leurs propres formes typées. Consultez la référence des hooks avant de traduire les arguments de callback WordPress.
Les hooks de contenu qui reçoivent des données d’entrée exigent content:read. Ajoutez des capabilities selon les API appelées par le port :
| Capability | API rendue disponible |
|---|---|
content:read | Lire le contenu et enregistrer des hooks de contenu exposant des données d’entrée |
content:write | Créer, mettre à jour, publier ou supprimer du contenu ; implique aussi la lecture |
media:read | Lire les enregistrements média |
media:write | Créer ou mettre à jour des médias ; implique aussi la lecture |
network:request | Utiliser ctx.http pour les hôtes listés dans allowedHosts |
Options et tables personnalisées
Utilisez ctx.settings pour la configuration utilisateur et ctx.kv pour de petites valeurs internes. Les deux stores sont isolés par plugin. Déclarez les identifiants comme champs secret dans admin.settingsSchema pour qu’EmDash les chiffre.
Utilisez des collections ctx.storage.<collection> déclarées pour des enregistrements de plugin interrogeables. La déclaration de stockage appartient à emdash-plugin.jsonc pour sandboxed et à definePlugin() pour native. N’ouvrez pas la base EmDash et n’interpolez pas de SQL depuis le code du plugin.
La comparaison suivante porte une valeur d’option sans exposer les globales WordPress au nouveau plugin :
WordPress
$api_key = get_option('read_time_api_key', '');
update_option('read_time_api_key', $new_api_key); EmDash
import type { PluginContext } from "emdash/plugin";
export async function saveApiKey(ctx: PluginContext, newApiKey: string) {
await ctx.settings.set("apiKey", newApiKey);
}
export async function readApiKey(ctx: PluginContext) {
return await ctx.settings.get<string>("apiKey") ?? "";
} Pour une table personnalisée WordPress, identifiez les champs utilisés pour filtrer et trier avant de déclarer le stockage. Le fragment de manifeste sandboxed suivant indexe les deux champs utilisés par la requête :
"storage": {
"jobs": { "indexes": ["status", "createdAt"] }
}
Le runtime peut alors stocker et interroger des enregistrements de jobs :
await ctx.storage.jobs.put("job-123", {
status: "pending",
createdAt: new Date().toISOString(),
});
const pending = await ctx.storage.jobs.query({
where: { status: "pending" },
orderBy: { createdAt: "asc" },
limit: 50,
});
Déclarez status et createdAt comme index dans le manifeste ou la définition de stockage native avant d’exécuter cette requête.
Points de terminaison REST
Mappez une route REST WordPress à une route de plugin. EmDash la monte à /_emdash/api/plugins/<plugin-id>/<route-name>. Définissez un inputSchema lorsque la route accepte une entrée et renvoyez des données sérialisables en JSON.
Réglages et pages admin
Les plugins sandboxed décrivent les pages admin avec Block Kit et lisent ou écrivent des valeurs via routes et KV. Ils n’embarquent pas React dans l’application admin.
Les plugins native peuvent utiliser admin.settingsSchema pour un formulaire généré. Utilisez un export adminEntry du paquet pour des pages React personnalisées, widgets, widgets de champ ou colonnes de liste.
Fichiers et médias
Utilisez les API média pour les fichiers téléversés ou générés. Les plugins sandboxed n’ont pas accès au système de fichiers. Les plugins native partagent le processus hôte, mais écrire des fichiers locaux au déploiement n’est pas une stratégie de stockage portable.
Porter le plugin
-
Inventoriez les hooks, options, tables personnalisées, tâches cron, routes REST, pages admin, blocs, shortcodes et hôtes externes WordPress.
-
Supprimez le comportement qui relève du routage Astro, du modèle de contenu EmDash ou de la plateforme de déploiement.
-
Choisissez le format de paquet sandboxed ou native. Notez chaque capability et hôte autorisé nécessaire au comportement restant.
-
Définissez les clés KV et les collections de stockage structuré. Ajoutez des index pour chaque champ utilisé dans
whereouorderBy. -
Portez un comportement observable à la fois. Testez le hook ou la route avec du contenu représentatif et des cas d’erreur.
-
Ajoutez Block Kit ou l’UI admin native seulement après que les routes et le stockage sous-jacents fonctionnent.
-
Testez l’installation, la mise à niveau, l’activation, la désactivation, la désinstallation avec et sans suppression de données, et les changements de capabilities.
Prochaines étapes
- Manifeste de plugin sandboxed pour le contrat de confiance et les métadonnées du paquet.
- Capabilities pour l’accès au contenu, aux médias, aux hôtes réseau et aux hooks.
- Storage pour KV et collections indexées.
- Pages admin React pour l’UI exclusive native.