Документация пока доступна только на английском языке. Остальная часть сайта соответствует вашему языку.
Hooks
Typed actions, gates, and filters — the complete core hook list and how to register them.
10 минута чтения
Hooks are how you change what Justflows does without changing Justflows. Write the plugin in plugins/<name>/ (copy plugins/hello-world) and register hooks in activate(). Names autocomplete from @justflows/sdk.
| I want to… | Use a… | Names look like |
|---|---|---|
| React after something happened | action | content.published |
| Stop something before it commits | gate | content.beforeCreate |
| Change a value on the way through | filter | content.render |
Actions
Observe only. Cannot cancel. If your handler throws, Justflows logs it against your plugin and continues — a broken analytics plugin must not block publishing. Treat payloads as read-only. Async handlers are awaited in order.
| Action | When |
|---|---|
app.starting / app.started / app.stopping | Process lifecycle |
content.created / updated / deleted / published / unpublished | Content |
comments.created | A visitor or moderator comment was stored as pending or approved: { siteId, contentId, commentId, parentId, status }. Spam and trash do not fire it, and it carries no email, IP address, or comment text. |
media.uploaded / media.deleted | Media |
user.created / updated / deleted | Users (needs users:read) |
auth.login / logout / loginFailed | Auth (needs auth:hook) |
workspace.created / suspended / reactivated / deleted | Multisite workspace lifecycle. No database password in the payload. |
workspace.purged | A suspended or deleted workspace was removed for good. A notification only. |
site.created | A site was added to a workspace |
plugin.installed / activated / deactivated / uninstalled | Plugins |
theme.installed / theme.activated | Themes |
request.before / request.after | HTTP |
page.viewed | A visitor received a public page: status below 400, not a preview, not the static exporter's crawl. Fired after the response is sent, with { siteId, path, locale?, contentId?, userAgent, referrer?, host } |
site.underConstruction.viewed | Unpublished site hit |
cache.revalidated | After selective cache revalidate |
webhook.delivery | After each outgoing webhook attempt (status, response, error) |
staticExport.completed / staticExport.deploy | A static / edge export run finished; deploy fires right after so a plugin can push the directory to a CDN |
tenancy.workspaceCreated | With workspace.created, plus the first administrator's email. Listening needs platform:tenancy. |
quota.updated | A platform operator or plugin saved limits. Listening needs platform:tenancy. |
domain.added / activated / failed / removed | Custom domain lifecycle |
email.received | An inbound message was accepted for this site (needs mail:hook) |
Gates
Run before commit. Call event.cancel("Human-readable reason") to abort. Fail closed: if your gate throws, the operation is aborted and attributed to your plugin. Priority order; first cancellation wins.
content.beforeCreate/beforeUpdate/beforeDelete/beforePublishmedia.beforeUpload/media.beforeDeleteworkspace.beforeCreate/beforeSuspend/beforeReactivate/beforeDelete/beforePurge,site.beforeCreatedomain.beforeAdd(refuse a custom domain before it is added)
ctx.hooks.gate("media.beforeUpload", (event) => {
if (event.sizeBytes > 10_000_000) {
event.cancel("Files must be under 10 MB.");
}
});Filters
You must return a value. On core hooks TypeScript enforces it. If a custom filter returns undefined, the previous value is kept and a warning is logged. Throws skip your filter and keep the last good value.
| Filter | Transforms |
|---|---|
content.input / content.output | Content records in/out |
content.render | Public HTML string (may be async) |
media.metadata | Metadata object |
navigation.items | Menu tree (runs after the host resolves a menu, visibility rules applied) |
menu.design.presets | One-click menu layout presets a plugin/theme ships (static data; ids "<pluginId>:<slug>") |
menu.visibility.evaluate | Answer an item-level custom visibility condition your plugin owns (seeded false, fail-closed) |
http.responseHeaders | Header map (must be sync) |
html.head | Extra <head> HTML (public pages await it, so a handler may be async). The PublicPageContext carries the page's locale, title, content type and fields, description, canonical URL, share image, origin, and whether it is indexable. |
html.title | The page's <title>, seeded with the page title. Not applied when the page is not indexable. |
html.canonical | The absolute URL in <link rel="canonical">; return "" to drop it. Not applied when the page is not indexable. |
site.identity | { siteTitle, tagline } per locale, seeded from the site's identity settings |
plugin.settings.schema | A plugin's settings form for this site: drop fields or fill a select's options (check pluginId) |
admin.body | HTML added at the end of every administration screen (may be async) |
analytics.head | Tracking markup for a public page's <head>, seeded with "" (must be sync). A tag plugin appends below priority 100; a consent plugin rewrites it at the default priority |
security.cspSources | Extra https:// origins for this response's Content-Security-Policy (script-src, frame-src, connect-src, img-src, …), seeded with {}. 'unsafe-inline', *, paths, and http: are dropped; frame-ancestors cannot be changed |
email.route | Which site files an inbound message, seeded with the mailbox owner's site id; return siteId to claim it for the open site (needs mail:hook) |
operatorKey.ensure | Called with hooks.apply: ask the host to create a missing 32-byte key in the installation .env. The name must belong to the calling plugin; the value is never returned or stored in the database |
theme.css | Plugin CSS appended to cached /theme.css (may be async) |
seo.sitemapPaths | Array of sitemap path strings |
site.underConstruction.render | Under-construction HTML (must be sync) |
comments.render | Rendered public comments block HTML (may be async) |
header.templates / header.resolve / header.config | Contribute named header designs, own a page's header per request, or adjust the resolved HeaderConfig before render |
webhook.eventTypes | Array of deliverable webhook event names |
webhook.payload | Webhook JSON body before it is signed and sent |
staticExport.routes / staticExport.assets | Seed paths crawled by the exporter, and same-origin asset URLs the scanner cannot discover |
staticExport.formAction | Override the <form action> written into exported HTML for a dynamic endpoint |
staticExport.exclude | Paths the static exporter leaves to the live app ({ path, match: "prefix" | "exact" }[]) |
theme.layoutScopes | Customizer layout sections (content and wide width) for a public prefix a plugin owns |
permalinks.typeBases | Default permalink bases for a plugin's own content types; stored values win |
media.placeholder | Replace or clear (null) the placeholder image for a kind (must be sync) |
mcp.tools | No longer listed or run. Offer a plugin route to agents with the mcp route option instead (see Bring your own AI). |
account.sections | Server-rendered sections on the account page |
widgets.areas | Add widget areas with default widgets and content types |
quota.effectiveLimit | Lower a stored limit; it can never raise one (needs platform:tenancy) |
SYNC_FILTERS: http.responseHeaders and site.underConstruction.render — handlers must not be async. html.head, html.title, html.canonical, and site.identity are awaited. content.blocks, content.render, and theme.css may be async.
Options and ownership
priority— lower runs earlier (default 100).once— auto-dispose after first dispatch.id— stable label in diagnostics.network: true— run this handler on every site while the plugin is active on the main site, also on sites that never activated it (needsplatform:network; see Network-wide handlers).- Plugins may emit only hooks under their own manifest id (
justflows.acme-seo.scoreCalculated). - Declaration merging on
ActionEventMap/GateEventMap/FilterValueMaptypes your own names.
Network-wide handlers
On a multisite installation a handler normally runs only on the sites that activated the plugin. Register it with { network: true } to run it on every site while the plugin is active on the main site. That needs the sensitive platform:network permission; without it, registering throws. Only the handlers you mark run network-wide: routes, jobs, and every other handler stay on the sites that activated the plugin. Inside a network handler, ctx.settings and ctx.data still belong to the request's site; read the main site's choices with ctx.network.mainSiteSettings.get(key) and its secrets with ctx.network.mainSiteSecrets, and link to a site with ctx.network.site(siteId?), which returns its public url. Files the handler's markup loads from /ext/<plugin id>/ are served on every site.
ctx.hooks.filter("admin.body", async (html) => {
const tag = await ctx.network?.mainSiteSettings.get<string>("adminTag");
if (!tag || !/^[A-Z0-9-]+$/.test(tag)) return html;
return `${html}<script src="/ext/acme.tags/tag.js" data-tag="${tag}"></script>`;
}, { network: true });Header and comment hooks
header.templates contributes named headers that appear in the per-page picker, build()-rendered at request time. header.resolve lets a plugin own a page's header per request; header.config adjusts the resolved header before render. comments.render restyles or replaces the rendered comments block. New SDK types: HeaderConfig, HeaderTemplate, HeaderBuildContext, HeaderResolveContext, CommentsBlockRenderContext, PublicComment. Outgoing webhooks add two filters — webhook.eventTypes to register event names and webhook.payload to reshape a body — plus the webhook.delivery action after every attempt; see Webhooks. Access-policy changes emit access-change hooks; a plugin registers a runtime capability with ctx.capabilities.register() and declares cookies with ctx.cookies.declare() (reading the resolved site registry with ctx.cookies.list()). The synchronous analytics.head filter lets Cookie consent defer third-party tags until consent is granted. For menus, menu.design.presets contributes one-click layout presets to the designer's design panel and menu.visibility.evaluate resolves a plugin-defined item condition — an unrecognized or deactivated condition id hides the item. See docs/HOOKS.md → Contributing a menu design preset in the Community Edition repository.
Note
Longer narrative and cleanup rules: docs/HOOKS.md in the Community Edition repository.