JustFlows

Die Dokumentation ist vorerst nur auf Englisch verfügbar. Der Rest der Website folgt Ihrer Sprache.

Plugins

Write a plugin in plugins/<name>/, then install, activate, and manage .jfpkg packages. Manifest, permissions, and the SDK context.

8 Min. gelesen

Write a plugin

If you are developing against the CE source, create a folder under `plugins/` and start there. One folder per plugin. Copy plugins/hello-world (it listens to content.published). The pnpm workspace includes plugins/*, and the server scans that directory when you run from source. Import types from @justflows/sdk only. Build to dist/index.js — the runtime does not load TypeScript.

  1. 1

    Copy the example

    cp -R plugins/hello-world plugins/acme-seo

  2. 2

    Set the id

    Use a namespaced id in justflows.json and src/index.ts (acme.seo). Declare a GPL-compatible license.

  3. 3

    Write and build

    Edit src/. Then pnpm --filter acme.seo build so dist/index.js exists.

Note

Do not put plugin source under packages/. That tree is platform code. Site owners never copy folders into plugins/ — they install a .jfpkg in Admin.

Install a packaged plugin

Admin → Plugins. Drop a .jfpkg on the page or use justflows plugin install <path>. Then activate. Core is MIT; plugins keep the license in their manifest (example: GPL-2.0-or-later).

Lifecycle APIs

ActionHTTPCLI
ListGET /api/pluginsjustflows plugin list
InstallPOST /api/pluginsjustflows plugin install <path>
ActivatePOST /api/plugins/:id/activatejustflows plugin activate <id>
DeactivatePOST /api/plugins/:id/deactivatejustflows plugin deactivate <id>
DeleteDELETE /api/plugins/:id

Actions fired: plugin.installed, plugin.activated, plugin.deactivated, plugin.uninstalled. Runtime loads active plugins in plugin-runtime before deferred routes register.

A plugin can declare a setupPath and serve a first-run guide from GET/POST /ext/{id}/setup; activating it opens that page. Admin sidebar pages are contributed through the admin.menu filter (needs admin:extend) and re-validated by GET /api/plugins/admin-menu. Plugin settings, secrets, and schema live in plugin_data, not site_settings, and every plugin implements a deleteData hook the host calls on uninstall.

Installed is not active

Uploading a package only installs it. Its module, admin navigation, blocks, and public routes become available after activation and are removed immediately on deactivation. Activation, deactivation, and deletion revalidate cached public pages.

Bundled content extensions

  • Forms — now a fully standalone plugin (removed from core). It ships its own block, /justflows-forms/submit route, framed admin app, and public jf-forms.js enhancement script, and sends submission notifications through the permission-gated ctx.mail.send() API. Keep it active if your site uses forms; the default Contact pattern cannot be imported until its block is active.
  • Gallery — provides Grid, Masonry, Carousel, Slideshow, and List layouts. Carousel uses scroll snap; Slideshow cross-fades; the public output needs no client-side gallery library.
  • Analytics — adds its public behavior only while the extension is active.
  • Cookie Consent — a categorised consent banner and preference center, per-category script and embed gating, and versioned consent records. See Cookie consent.
  • Shop — a product content type, storefront blocks and patterns, and a first-run setup wizard for commerce topology and store identity.

Minimal plugin

ts
import type { PluginModule } from "@justflows/sdk";

const plugin: PluginModule = {
  manifest: {
    id: "acme.welcome",
    name: "Acme Welcome",
    version: "1.0.0",
    license: "GPL-2.0-or-later",
    permissions: [],
    main: "index.js",
  },
  activate(ctx) {
    ctx.hooks.action("content.published", (event) => {
      ctx.logger.info("Published", { contentId: event.contentId });
    });
  },
};

export default plugin;

Ship a front-end asset bundle

For public-site JavaScript — or a standalone stylesheet — declare an assets block ({ dir?, scripts?, styles? }) in justflows.json and drop the files in the package. The host serves <dir>/** at /ext/<pluginId>/** and concatenates every active plugin's scripts and styles into one content-hashed `/jf-plugins.<hash>.js` / `.css` bundle added to every public page. No ctx.http route, no html.head filter. Each script is wrapped in its own IIFE; write them as progressive enhancement (no import/export), .js/.mjs/.css only, at most 20 of each. PLUGIN_ASSETS_BUNDLE=0 emits one tag per file. The static-site exporter downloads the bundle automatically, so a plugin front-end works on a CDN deployment with no extra wiring.

Ship your own admin app

A plugin's admin screens can be its own app rather than React pages compiled into the host bundle. Declare adminApp ({ dir?, routes: [{ path, entry, title }] }) alongside an adminMenu item and ship an HTML build; the host serves it at /ext/<pluginId>/admin/** and mounts each declared route in a same-origin <iframe> inside the admin shell. The two sides talk only over postMessage via the @justflows/admin-bridge package (ready, resize, navigate from the frame; context and route from the host) — no shared React runtime, no core page or route. The frame is same-origin, so it reads the CSRF cookie itself and calls the plugin's own ctx.http routes for data. Rules: dir/entry relative with no .., entry must be .html, each path must be /admin/…, at most 20 routes.

Publish a health check

Declare diagnostics:publish and register a read-only check during activation with ctx.diagnostics.register({ id, label, run() }). It appears in Admin → System → Diagnostics. Check IDs are namespaced by plugin, execution is time-bounded by the host, and returned details are recursively redacted before display or export — checks must not return credentials, personal data, or raw provider responses. Registrations are removed automatically on deactivation.

Ship your own stylesheet

Public components rendered by a plugin should ship their CSS with that plugin. Keep source CSS under src/styles/, minify or copy it to dist/styles/ during pnpm build, read the built file once, and append it with the async theme.css filter during activation. Do not put plugin-specific rules in the active theme and do not inject a second stylesheet link through html.head.

ts
import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";

const marker = "/* acme.catalog */";
const css = (await readFile(
  fileURLToPath(new URL("./styles/catalog.css", import.meta.url)),
  "utf8",
)).trim();

ctx.hooks.filter("theme.css", (current) =>
  current.includes(marker) ? current : `${current}\n${marker}\n${css}\n`,
);
  • Package the ready-to-serve file below dist/; installed plugins do not run TypeScript or a CSS toolchain.
  • Use plugin-namespaced classes and theme tokens such as --color-*, --space-*, and --radius-*.
  • Plugin CSS is added after theme styles and Customizer tokens, but before the site owner's Additional CSS.
  • Deactivation removes the filter and cache revalidation rebuilds /theme.css without the plugin CSS.
  • The combined plugin contribution is limited to 512 KiB. It is trusted extension output, so never concatenate request or site data into CSS.

Note

The theme.css filter runs once per cached stylesheet build, not per page request, and may be async. Cache file contents in the plugin process. See plugins/hello-world for a minimal example and plugins/shop for a production implementation.

Manifest rules

  • id — dot-separated namespace, e.g. acme.my-plugin.
  • version — semver.
  • license — required; the plugin's own license. Must be a GPL-compatible SPDX identifier (GPL, MIT, BSD, ISC, …). Not inherited from MIT core.
  • permissions — declared up front; sensitive ones are network:outbound, users:manage, settings:manage, auth:hook.
  • main — entry file, default index.js.
  • Optional minJustflowsVersion / maxJustflowsVersion, description, author, homepage.
  • Plugins, themes, and CSS providers share an engines.justflows range the installer enforces before a package leaves staging; the public SDK export surface is snapshotted in CI so an export cannot vanish without a deprecation cycle. The legacy top-level justflows range is still accepted as a deprecated alias.
  • Optional hostCooperative: true — the runtime normally leaves the first-party ids it renders itself (justflows.seo, …) inactive; the flag means the installed module only augments (extra routes, autodiscovery) and is safe to activate.

Plugin permissions

content:read|create|update|delete|publish, media:read|upload|delete, users:read|manage, settings:read|manage, network:outbound, admin:extend, jobs:register, auth:hook, mail:transport|templates|hook|send, diagnostics:publish.

Context (`activate(ctx)`)

  • hooks — typed action / gate / filter registration (see Hooks).
  • cache — namespaced jf-cache (plugin:{id}:…). remember, get, set, delete, invalidate.
  • settings — plugin settings store.
  • logger — attributed to your plugin id.
  • contentensureType, ensurePage, deleteType for plugins that own CMS types; listPublished(query?) reads published entries by type / locale / author / date (needs content:read).
  • databases / secrets — prefixed plugin tables (ensureSchema, upsert, find, delete) and encrypted secret storage.
  • cookiesdeclare() a non-essential cookie and list() the resolved site registry.
  • capabilitiesregister() a runtime capability so it appears in the role editor and access policy.
  • patternsregister() a validated, plugin-scoped block pattern for the editor library.
  • diagnosticsregister() a read-only health check for Admin → System → Diagnostics (needs diagnostics:publish).
  • mailsend() through the host transport without seeing credentials (needs mail:send); registerTemplate() ships versioned system-email templates.
  • i18ndefaultLocale() and locales() return the site's configured locales, read-only.
  • runtime — the running Justflows, SDK package, and SDK API versions.
  • pluginId, version, permissions set.

Good to know

You do not unregister hooks on deactivate — the loader disposes registrations when the plugin stops. The synchronous html.head filter context includes locale (the page's content locale) for locale-aware tags. Import types from @justflows/sdk only. Author guides in the CE repo: docs/PLUGINS.md, docs/MANIFEST.md, docs/PERMISSIONS.md, docs/PACKAGING.md, docs/THEMES.md, docs/BLOCKS.md, docs/TESTING-EXTENSIONS.md.