Zum Inhalt springen
Dokumentation

Einrichtung deiner Entwicklungsumgebung

Diese Anleitung erklärt, wie du deine Entwicklungsumgebung für die Plugin-Entwicklung einrichtest.

Ein Plugin in Neleto ist einfach ein normaler Webserver. Das bedeutet, dass du Plugins in jeder Programmiersprache schreiben kannst.

Die einzigen 2 Anforderungen, damit dein Webserver als Plugin funktioniert, sind:

  1. Die Manifest-Datei plugin.json. Diese Datei definiert den Namen, die Version, den Startbefehl (bin) und Metadaten deines Plugins. Hier ist ein minimales Beispiel:
{
    "name": "example-plugin",
    "version": "0.1.0",
    "bin": "bun run main.js"
}
  1. Das gepackte Plugin muss alles enthalten, was zum Ausführen unter einem x86 Ubuntu 24.04 Docker-Container benötigt wird. Dies umfasst Laufzeiten für Programmiersprachen wie JavaScript, was bedeutet, dass du sie mit deinem Plugin bündeln musst. Im Fall von Bun kannst du --compile beim Bauen deines Plugins verwenden, um die Bun-Laufzeit einzubinden.

Laufzeitvertrag

Wenn Neleto dein Plugin startet, setzt es einige Umgebungsvariablen. Dein Server muss sie auslesen, anstatt Werte fest zu verdrahten:

VariableImmer gesetztBedeutung
PORTjaDer Port, auf dem dein Webserver lauschen muss. Binde 0.0.0.0:$PORT.
DATABASE_URLjaEin PostgreSQL-Connection-String für die eigene Datenbank deines Plugins.

Lausche auf PORT und binde alle Interfaces:

const port = Number(process.env.PORT) || 3000;
Bun.serve({ port, fetch: handler });
let port = std::env::var("PORT").unwrap_or_else(|_| "3000".into());
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}")).await?;

Die Datenbank deines Plugins

Jedes Plugin erhält eine eigene PostgreSQL-Datenbank, getrennt vom CMS und von anderen Plugins. Der Connection-String wird als DATABASE_URL injiziert, du musst nichts bereitstellen. Lege deine Tabellen an, indem du deine Migrationen beim Start ausführst:

let db = sqlx::PgPool::connect(&std::env::var("DATABASE_URL")?).await?;
sqlx::migrate!("./migrations").run(&db).await?; // Migrationen im Paket mitliefern

Nimm dein Migrations-Verzeichnis in include_files auf, damit es mit dem Paket ausgeliefert wird.

CMS-Inhalte lesen

Um die Seiten, Beiträge, Dateien usw. der Website zu lesen oder zu schreiben, rufe das CMS aus deinem Plugin über seine REST-API auf (oder lass einen Agenten die MCP-Tools nutzen). Das ist die unterstützte, stabile Schnittstelle für Inhalte, greife nicht direkt auf die Datenbank des CMS zu.

Logging

Schreibe nach stdout / stderr. Neleto erfasst beides und zeigt es im Tab Logs des Plugins im Admin-Bereich an (siehe Plugins verwalten), die erste Anlaufstelle, wenn ein Plugin sich nicht wie erwartet verhält.

Die Manifest-Datei plugin.json

Um dein Plugin besser in Neleto zu integrieren, kannst du weitere Werte in der plugin.json angeben. Die Manifest-Datei plugin.json muss diesem TypeScript-Typ entsprechen:

export type ApiEndpoint = {
    /**
     * Wenn nicht angegeben, können alle eingeloggten Benutzer auf diese Route zugreifen. Jeder
     * Eintrag ist eine Berechtigungs-ID wie `"page.update"`; der Benutzer benötigt EINE davon (ODER-Logik).
     */
    allowed_permissions?: Array<string>;
    /**
     * Dies ist die Route, in einem Format wie `/foo/:bar/:somepath*`
     */
    route: string;
};

export type PluginHelper = {
    /**
     * Der Name des Helpers
     */
    name: string;
    /**
     * Die Route, unter der sich der Helper befindet. Diese Route erhält die Argumente als Eingabe und sollte den Wert zurückgeben
     */
    route: string;
} & (
    | {
          type: "inline";
          /**
           * Die Argumenttypen, die dieser Helper erwartet (Länge = Anzahl der Argumente).
           * Akzeptiert entweder `{ name, type }`-Objekte oder eine einfache Liste von Typen,
           * z. B. `["String", "Int"]`.
           */
          arguments: Array<FunctionArgument | VariableType>;
          /**
           * Der Typ des Rückgabewerts dieses Helpers
           */
          return_value: VariableType;
      }
    | {
          /**
           * Ein Block-Helper, wie der `if`-Helper: `{{#if <arguments>}}<body>{{/if}}`
           */
          type: "block";
          arguments: Array<VariableType>;
      }
);

export type FunctionArgument = { name: string; type: VariableType };

export type PluginManifest = {
    /**
     * Der Name des Plugins
     */
    name: PluginName;
    /**
     * Die Version des Plugins (semver)
     */
    version: string;
    /**
     * Alles, was zum Ausführen deines Plugins nötig ist — ausführbare Dateien, dynamisch geladene
     * Bibliotheken, Assets, gebündelte Laufzeiten — und in das gebaute Paket aufgenommen werden
     * muss. Glob-Muster sind erlaubt. Erforderlich.
     */
    include_files: Array<string>;
    /**
     * Der Befehl zum Starten des Plugins
     */
    bin?: string | null;
    /**
     * Rewriter ist eine Route, die dein Plugin behandelt. Diese Route wird per POST aufgerufen, wann immer
     * eine Seite gerendert wird, mit dem gerenderten HTML im Request-Body. Die Antwort dieser
     * Route wird entweder an das nächste Plugin oder an die Render-Ausgabe weitergeleitet.
     */
    rewriter?: string;
    /**
     * Hier kannst du Routen definieren, die als Handlebars-Helper beim Rendern fungieren
     */
    helpers?: Array<PluginHelper>;
    /**
     * Hier kannst du Routen definieren, die als Seiten verwendet werden, wenn ein Benutzer die Website besucht. Diese
     * Routen haben das Präfix nicht vor ihrem Pfad, wenn sie aufgerufen werden.
     */
    pages?: Array<PluginPage>;
    /**
     * Hier kannst du Routen definieren, die als authentifizierte API-Endpunkte verwendet werden. Der Schlüssel ist die
     * Route, in einem Format wie `/foo/:bar/:somepath*`. Diese Routen haben das Präfix
     * `/api/rest/plugins/<dein-plugin-name>/api`, wenn sie vom Client aufgerufen werden, da sie
     * durch das CMS-Backend weitergeleitet werden.
     */
    api?: Array<ApiEndpoint>;
    /**
     * Dies ist der Pfad, in dem dein Plugin Voreinstellungen für Komponenten definiert. Dieser Pfad muss
     * in deinem gebauten Plugin-Paket enthalten sein. Komponenten müssen in Ordnern mit diesen
     * Dateien bereitgestellt werden: `template.hbs`, `script.js`, `style.css`, `.meta.json` (erforderlich),
     * `form.json` (erforderlich)
     */
    components?: string | null;
    /**
     * Dies ist der Pfad, in dem dein Plugin Voreinstellungen für Seiten-Layouts definiert. Dieser Pfad muss
     * in deinem gebauten Plugin-Paket enthalten sein. Layouts müssen in Ordnern mit diesen
     * Dateien bereitgestellt werden: `template.hbs`, `script.js`, `style.css`, `.meta.json` (erforderlich)
     */
    layouts?: string | null;
    /**
     * Routen, die Dashboard-Karten im CMS-Dashboard rendern.
     */
    dashboard_cards?: Array<PluginDashboardCard>;
    /**
     * Diese Route wird als Einstiegspunkt für die Benutzeroberfläche deines Plugins verwendet. Diese Routen haben das
     * Präfix `/api/rest/plugins/<dein-plugin-name>/ui`. Der Zugriff auf diese UI-Routen ist nur
     * auf Benutzer beschränkt, die auch Plugins verwalten können.
     */
    ui?: string | null;
    /**
     * Pfad (relativ zum Plugin-Stammverzeichnis) zu einem im Paket gebündelten Hero-Bild, das als
     * breites Banner auf der Karte und Detailseite des Plugins im Plugin-Store angezeigt wird.
     * Empfohlen: ein ~3:1-Banner, z. B. 1600×540, PNG/JPG/WebP/SVG, ≤ 500 KB.
     */
    hero_image?: string | null;
};

export type PluginName = string;

export type PluginPage = {
    /**
     * Der Name der Seite
     */
    name: string;
    /**
     * Dies ist die Route, in einem Format wie `/foo/:bar/:somepath*`
     */
    route: string;
    /**
     * Erlaubt die Auswahl eines Layouts für diese Seite im CMS. Wenn ein Layout ausgewählt ist,
     * wird die Seite darin gerendert (siehe First Steps -> Seiten-Endpunkt). Standard: false.
     */
    allow_select_layout?: boolean;
    /**
     * Erzeugt Tailwind-Klassen für das ausgewählte Layout. Ohne Wirkung, wenn
     * allow_select_layout nicht aktiviert ist.
     */
    enable_tailwind?: boolean;
};

export type PluginDashboardCard = {
    /**
     * Eindeutige (innerhalb dieses Plugins) ID der Karte. Nicht für den Benutzer sichtbar; wird
     * verwendet, um zu speichern, welche Karten ein Benutzer anzeigen möchte — darf sich daher nie ändern.
     */
    name: string;
    /**
     * Beschriftung, die angezeigt wird, wenn der Benutzer auswählt, welche Dashboard-Karten angezeigt werden.
     */
    label: Translated;
    /**
     * Optionale längere Beschreibung, die dem Benutzer angezeigt wird.
     */
    description?: Translated | null;
    /**
     * Die Route, die dein Plugin bereitstellt, um die gesamte Dashboard-Karte zu rendern.
     */
    route: string;
};

/** Ein übersetzter String, nach Sprachcode gekeyed (`en` und `de` erforderlich). */
export type Translated = { en: string; de: string } & Record<string, string>;

/** Das Typsystem, das von Helper-Argumenten und Rückgabewerten verwendet wird. */
export type VariableType =
    | "String"
    | "Number"
    | "Int"
    | "Float"
    | "Boolean"
    | "Any"
    | "Never"
    | "Nullish"
    | { Object: Record<string, VariableType> }
    | { Array: VariableType }
    | { Literal: string }
    | { IntLiteral: number }
    | { BooleanLiteral: boolean }
    | { Option: VariableType }
    | { Union: Array<VariableType> }
    | { Record: { key: VariableType; values: VariableType } }
    | { Named: string }
    | { Function: { name: string; args: Array<FunctionArgument | VariableType>; return_type: VariableType } };

Schema & Editor-Validierung

Der obige TypeScript-Typ dient zum Nachlesen. Das maßgebliche, maschinenlesbare Schema wird als JSON Schema (Draft 2020-12) veröffentlicht und immer aus dem Backend generiert, sodass es nie veraltet:

SchemaURL
Plugin-Manifest (plugin.json)https://neleto.io/schema/plugin/v1/manifest.json
Komponenten-Formular (form.json)https://neleto.io/schema/plugin/v1/form.json
Helper-Variablentypenhttps://neleto.io/schema/plugin/v1/variable-type.json
Plugin-Store-Indexhttps://neleto.io/schema/plugin/v1/repository.json

Füge deiner plugin.json einen $schema-Schlüssel hinzu, um in Editoren wie VS Code Autovervollständigung und Inline-Validierung zu erhalten:

{
    "$schema": "https://neleto.io/schema/plugin/v1/manifest.json",
    "name": "example-plugin",
    "version": "0.1.0",
    "include_files": ["main"]
}
name, version und include_files sind erforderlich. include_files listet alles auf (ausführbare Dateien, Assets, Laufzeiten), was in das gepackte Plugin gebündelt werden muss — ein Manifest ohne dieses Feld ist ungültig.

Routen mit diesem Format /foo/:bar/:somepath* folgen diesen Regeln:

MusterArtBeschreibung
:nameNormalEntspricht einem Pfadstück, schließt / aus
:name?OptionalEntspricht einem optionalen Pfadstück, schließt / aus
/:name?/ /:name?OptionalSegmentEntspricht einem optionalen Pfadsegment, schließt / aus, Präfix oder Suffix sollte / sein
+ :name+OneOrMoreEntspricht einem Pfadstück, inkludiert /
* :name*ZeroOrMoreEntspricht einem optionalen Pfadstück, inkludiert /
/*/ /* /:name*/ /:name*ZeroOrMoreSegmentEntspricht null oder mehr Pfadsegmenten, Präfix oder Suffix sollte / sein
FallParameter
:a:ba b
:a:b?a b
:a-:b :a.:b :a~:ba b
:a_a-:b_ba_a b_b
:a\\: :a\\_a
:a\\::b :a\\_:ba b
:a*a
**1
*.**1 *2
:a+a
++1
+.++1 +2
/*/abc/+/def/g*1 +2