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:
- 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"
}
- 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
--compilebeim 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:
| Variable | Immer gesetzt | Bedeutung |
|---|---|---|
PORT | ja | Der Port, auf dem dein Webserver lauschen muss. Binde 0.0.0.0:$PORT. |
DATABASE_URL | ja | Ein 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:
| Schema | URL |
|---|---|
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-Variablentypen | https://neleto.io/schema/plugin/v1/variable-type.json |
| Plugin-Store-Index | https://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:
| Muster | Art | Beschreibung |
|---|---|---|
:name | Normal | Entspricht einem Pfadstück, schließt / aus |
:name? | Optional | Entspricht einem optionalen Pfadstück, schließt / aus |
/:name?/ /:name? | OptionalSegment | Entspricht einem optionalen Pfadsegment, schließt / aus, Präfix oder Suffix sollte / sein |
+ :name+ | OneOrMore | Entspricht einem Pfadstück, inkludiert / |
* :name* | ZeroOrMore | Entspricht einem optionalen Pfadstück, inkludiert / |
/*/ /* /:name*/ /:name* | ZeroOrMoreSegment | Entspricht null oder mehr Pfadsegmenten, Präfix oder Suffix sollte / sein |
| Fall | Parameter |
|---|---|
:a:b | a b |
:a:b? | a b |
:a-:b :a.:b :a~:b | a b |
:a_a-:b_b | a_a b_b |
:a\\: :a\\_ | a |
:a\\::b :a\\_:b | a b |
:a* | a |
* | *1 |
*.* | *1 *2 |
:a+ | a |
+ | +1 |
+.+ | +1 +2 |
/*/abc/+/def/g | *1 +2 |