Skip to content
Documentation

First Steps

Get started with your plugin

Build a webserver

Every plugin in Neleto is just a normal Webserver with some JSON REST endpoints. These endpoints can serve a variety of purposes. They can be used to manipulate rendered html before being sent to the client or cached, or they can define functions or helpers for the renderer itself. Here are the types (we're using typescript as an example, but you can build your plugin in any language):

Rewriter-endpoint

This endpoint can be specified under rewriter in your plugin.json.

// The body of the request this endpoint receives looks like this:
type Body = string;
// And the response needs to look like this:
type Response = string;

Helper-endpoint

This endpoint can be used in the array under helpers in your plugin.json.

// The body of the request this endpoint receives looks like this:
type Body = {
    // the name of the helper called
    name: string;
    args: JsonValue[]; // any value that is valid json
};
// And the response needs to look like this:
type Response = {
    // any value that is valid json, this needs to match the VariableType in the
    // `type` section of this helper-endpoint in your `plugin.json`
    data: JsonValue;
};

Page-endpoint

This endpoint can be used in the array under pages in your plugin.json.

Page-endpoints are just normal endpoints which get the request directly forwarded from the client. They can also return anything, as they can also function as an api endpoint.

Admins and Developers can select a layout in the cms for every page-endpoint defined by plugins. When a page-endpoint has a layout selected, the request to that endpoint will be transformed into a POST request and the rendered layout will be included in the body of the request, the response will not be changed.

The rendered layout contains a single <slot></slot> element which marks where the page content belongs. Replace it with your content and return the complete HTML document:

const layout = await request.text();
const page = layout.replace(/<slot[^>]*><\/slot>/g, yourContent);
return new Response(page, { headers: { "Content-Type": "text/html" } });

The layout renders the current page language as <html lang="…"> — read it to localise your content — and ships an empty <title></title>. Set the title and any meta / Open Graph tags yourself by manipulating the layout before returning it.

A layout is only applied once someone selects one for the page in the cms. Until then (or if none is selected) the endpoint receives a normal GET request and should render a standalone response.

Multiple languages

Neleto serves non-default languages under a path prefix (for example /en/your-page). To keep the layout's language switch working, declare both the plain and the language-prefixed route so both reach your plugin, and read the active language from the layout's <html lang>:

"pages": [
    { "route": "/your-page/:slug", "allow_select_layout": true },
    { "route": "/:lang/your-page/:slug", "allow_select_layout": true }
]
Page endpoints are always public though, if you need to make an authenticated endpoint, use an Api-endpoint

Api-endpoint

This endpoint can be used in the array under api in your plugin.json.

Api endpoints are similar to page-endpoints, except their access can be controlled to only be accessible by logged in users holding a specific permission (see access control below). Api-endpoints are proxied through the cms backend with the route prefix /api/rest/plugins/<plugin-name>/api. This means a route which your plugin defines as /foo will be available under /api/rest/plugins/<your-plugin-name>/api/foo. The query, body and request method will not be modified when proxying the request, it will only check for valid authentication and the required permission.

Access control

Restrict an api route with allowed_permissions in plugin.json — an array of CMS permission ids (e.g. page.update) with OR semantics (the user needs any one of them):

"api": [
    { "route": "/settings", "allowed_permissions": ["page.update"] }
]

If allowed_permissions is omitted or empty, the route is accessible to every logged-in user (only authentication is checked).

allowed_roles is deprecated and ignored — it is not part of the manifest schema and the backend never reads it. A route that declares only allowed_roles has no authorization check and is open to every logged-in user, regardless of the roles listed. Use allowed_permissions instead.
The /api/rest/plugins/<your-plugin-name>/api/ part is a fixed gateway prefix. Whatever you append is matched verbatim against the routes you declared under api in plugin.json. This means /api can appear twice in the client URL: a plugin route of /settings is reached at /api/rest/plugins/<name>/api/settings, but a plugin route of /api/settings is reached at /api/rest/plugins/<name>/api/api/settings. Keep the manifest route, your server's route, and the client fetch path in sync.A 404 on a plugin API call is always a path mismatch — never the HTTP method. Every method and body is forwarded unchanged; a method your server doesn't handle returns 405, not 404. If you get a 404, count the /api segments first.

The admin UI usually derives the prefix from its own location so the paths stay correct. Note how the plugin's own route (including its /api namespace, if it has one) is passed to the helper:

// base = this plugin's mount point, e.g. /api/rest/plugins/<name>
const base = location.pathname.replace(/\/ui\/?(index\.html)?$/, "");
const api = (p) => base + "/api" + p; // adds the gateway's fixed /api prefix

fetch(api("/settings")); // plugin route "/settings"      -> .../api/settings
fetch(api("/api/settings")); // plugin route "/api/settings" -> .../api/api/settings

Ui-endpoint

The ui-endpoint is similar to api-endpoints in that it is only accessible to logged in users. The only difference is that the user role will not be checked for this endpoint, and that it will be used to serve a custom admin ui for your plugin. You can only define one ui-endpoint in your plugin.json, because this one endpoint will be used as the entrypoint to the ui of your plugin inside an iframe.

Your plugin server can listen on more than one endpoint and you can use links to navigate between them, but these links will need to have the prefix /api/rest/plugins/<your-plugin-name>/ui.