WordPress-Plugins portieren

Auf dieser Seite

Portieren Sie ein WordPress-Plugin, indem Sie Inhaltsverhalten, gespeicherte Daten, HTTP-Routen und Benutzeroberfläche trennen. Wählen Sie anschließend das EmDash-Plugin-Format, das diese Anforderungen unterstützt.

Prüfen, ob das Plugin zu EmDash passt

Gute Kandidaten implementieren Verhalten, das unabhängig vom WordPress-Core ist, etwa Inhaltsvalidierung, externe API-Aufrufe, Hintergrundverarbeitung, benutzerdefinierte gespeicherte Datensätze, Einstellungen oder ein Admin-Tool.

Portieren Sie kein Plugin, dessen einziger Zweck darin besteht, eine WordPress-Funktion abzubilden, die Astro oder EmDash bereits ersetzt. Beispiele sind PHP-Seiten-Caching, WordPress-Rewrite-Regeln, Theme-Vorlagenauswahl oder Änderungen an WordPress-Core-Globals.

Definiert ein Plugin nur einen benutzerdefinierten Beitragstyp oder Felder, hat aber wenig Laufzeitverhalten, legen Sie stattdessen eine EmDash-Collection und Seed-Datei an.

Sandboxed oder Native wählen

Beginnen Sie mit Ein Plugin-Format wählen. Beide Formate teilen Hook-Namen und die PluginContext-APIs, aber ihre Quellpakete unterscheiden sich.

AnforderungSandboxedNative
Registry-InstallationJaNein
Isolierte LaufzeitJa, mit konfiguriertem RunnerNein
Hooks, Routen, KV, strukturierter SpeicherJaJa
Block-Kit-AdminseitenJaJa
Benutzerdefinierte React-Admin-KomponentenNeinJa
Astro-Komponenten für öffentliches RenderingNeinJa
Rohe SeitenfragmenteNeinJa

Wählen Sie Native nur, wenn der Port eine ausschließlich native Build-Time- oder Benutzeroberfläche benötigt.

Sandboxed-Paketformat

emdash-plugin init erzeugt das aktuelle Sandboxed-Format:

my-plugin/
├── emdash-plugin.jsonc
├── src/
│   └── plugin.ts
├── tests/
│   └── plugin.test.ts
├── package.json
└── tsconfig.json

Das Manifest enthält Identität, Publisher, Capabilities, erlaubte Hosts und Speicherdeklarationen. Die Version kommt normalerweise aus package.json.

Das folgende Manifest deklariert eine indizierte Speicher-Collection und die für content:afterSave erforderliche Capability:

{
  "$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 exportiert standardmäßig ein plain object mit dem Typ SandboxedPlugin. Sandboxed-Hook-Handler verwenden { handler }; Sandboxed-Routen-Handler erhalten (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;

Die Route ist unter /_emdash/api/plugins/read-time/recent erreichbar. Ein Speicherfeld muss als Index deklariert sein, bevor eine Abfrage danach filtern oder sortieren kann.

Erstellen Sie das Paket mit emdash-plugin build; fügen Sie diesem Format keinen handgeschriebenen src/index.ts-Deskriptor hinzu. Lesen Sie Ihr erstes Sandboxed-Plugin für das generierte package.json, Build-Ausgabe und Site-Registrierung.

Native-Paketformat

Ein Native-Paket exportiert sowohl eine Deskriptor-Factory für astro.config.mjs als auch eine Laufzeit-Factory mit definePlugin(). Optionale Admin- und Astro-Einstiegspunkte sind separate Paketexporte.

my-native-plugin/
├── src/
│   ├── index.ts
│   ├── admin.tsx
│   └── astro/
│       └── index.ts
├── package.json
└── tsconfig.json

Der folgende reduzierte Native-Einstiegspunkt zeigt die beiden erforderlichen Teile:

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;

Native-Hook-Handler können direkt Funktionen sein. Native-Routen-Handler erhalten ein kombiniertes Kontextargument. Halten Sie die Deskriptor- und Laufzeitkopien von id, version, Capabilities und Entrypoints synchron.

Lesen Sie Ihr erstes Native-Plugin, bevor Sie React-Adminseiten, Portable-Text-Renderer oder Seitenfragmente hinzufügen.

WordPress-Verhalten abbilden

Hooks

Ordnen Sie die Absicht einer WordPress-Action oder eines Filters zu, nicht nur den Namen:

WordPressEmDash
register_activation_hook()plugin:install bei Erstinstallation oder plugin:activate bei Aktivierung
register_uninstall_hook()plugin:uninstall
wp_insert_post_datacontent:beforeSave
save_postcontent:afterSave
before_delete_postcontent:beforeDelete
deleted_postcontent:afterDelete
wp_handle_upload_prefiltermedia:beforeUpload
add_attachmentmedia:afterUpload

Hook-Events haben eigene typisierte Formen. Prüfen Sie die Hook-Referenz, bevor Sie WordPress-Callback-Argumente übersetzen.

Content-Hooks, die Eintragsdaten erhalten, erfordern content:read. Fügen Sie Capabilities entsprechend der APIs hinzu, die der Port aufruft:

CapabilityVerfüg gemachte API
content:readInhalte lesen und Content-Hooks registrieren, die Eintragsdaten bereitstellen
content:writeInhalte erstellen, aktualisieren, veröffentlichen oder löschen; impliziert auch Lesezugriff
media:readMediadatensätze lesen
media:writeMedien erstellen oder aktualisieren; impliziert auch Lesezugriff
network:requestctx.http für in allowedHosts gelistete Hosts verwenden

Optionen und benutzerdefinierte Tabellen

Verwenden Sie ctx.settings für Benutzerkonfiguration und ctx.kv für kleine interne Werte. Beide Stores sind pro Plugin isoliert. Deklarieren Sie Zugangsdaten als secret-Felder in admin.settingsSchema, damit EmDash sie verschlüsselt.

Verwenden Sie deklarierte ctx.storage.<collection>-Collections für abfragbare Plugin-Datensätze. Die Speicherdeklaration gehört in emdash-plugin.jsonc für Sandboxed und in definePlugin() für Native. Öffnen Sie nicht die EmDash-Datenbank und interpolieren Sie kein SQL aus Plugin-Code.

Der folgende Vergleich portiert einen Optionswert, ohne WordPress-Globals dem neuen Plugin preiszugeben:

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") ?? "";
}

Identifizieren Sie bei einer WordPress-Custom-Tabelle die Felder für Filter und Sortierung, bevor Sie Speicher deklarieren. Das folgende Sandboxed-Manifest-Fragment indiziert beide von der Abfrage verwendeten Felder:

"storage": {
  "jobs": { "indexes": ["status", "createdAt"] }
}

Die Laufzeit kann dann Job-Datensätze speichern und abfragen:

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,
});

Deklarieren Sie sowohl status als auch createdAt als Indizes im Manifest oder in der Native-Speicherdefinition, bevor Sie diese Abfrage ausführen.

REST-Endpunkte

Ordnen Sie eine WordPress-REST-Route einer Plugin-Route zu. EmDash mountet sie unter /_emdash/api/plugins/<plugin-id>/<route-name>. Definieren Sie ein inputSchema, wenn die Route Eingaben akzeptiert, und geben Sie JSON-serialisierbare Daten zurück.

Einstellungen und Adminseiten

Sandboxed-Plugins beschreiben Adminseiten mit Block Kit und lesen oder schreiben Werte über Routen und KV. Sie liefern kein React in die Admin-Anwendung.

Native-Plugins können admin.settingsSchema für ein generiertes Formular verwenden. Nutzen Sie einen adminEntry-Paketexport für benutzerdefinierte React-Seiten, Widgets, Feld-Widgets oder Listenspalten.

Dateien und Medien

Verwenden Sie die Medien-APIs für hochgeladene oder generierte Dateien. Sandboxed-Plugins haben keinen Dateisystemzugriff. Native-Plugins teilen den Host-Prozess, aber das Schreiben deployment-lokaler Dateien ist keine portable Speicherstrategie.

Plugin portieren

  1. Erfassen Sie WordPress-Hooks, Optionen, Custom-Tabellen, Cron-Jobs, REST-Routen, Adminseiten, Blöcke, Shortcodes und externe Hosts.

  2. Entfernen Sie Verhalten, das zu Astro-Routing, dem EmDash-Inhaltsmodell oder der Deployment-Plattform gehört.

  3. Wählen Sie das Sandboxed- oder Native-Paketformat. Notieren Sie jede Capability und jeden erlaubten Host, den das verbleibende Verhalten benötigt.

  4. Definieren Sie KV-Schlüssel und strukturierte Speicher-Collections. Fügen Sie Indizes für jedes Feld hinzu, das in where oder orderBy verwendet wird.

  5. Portieren Sie ein beobachtbares Verhalten nach dem anderen. Testen Sie Hook oder Route mit repräsentativen Inhalten und Fehlerfällen.

  6. Fügen Sie Block Kit oder Native-Admin-UI erst hinzu, wenn die zugrunde liegenden Routen und Speicher funktionieren.

  7. Testen Sie Installation, Upgrade, Aktivierung, Deaktivierung, Deinstallation mit und ohne Datenlöschung sowie Capability-Änderungen.

Nächste Schritte