Modules
Jobs, hooks and events
Jobs and the queue
ctx.jobs.every, ctx.jobs.cron (minute hour day month weekday, with *, lists, ranges, steps, mon–sun, jan–dec, @daily and the like) and ctx.jobs.daily("03:30") repeat work while the module runs; times are UTC. Jobs are checked every 15 seconds.
The queue keeps work in the database, so it survives restarts and is tried again when it fails:
ctx.queue.handle("send-receipt", |payload, job| { // job: { id, name, attempt, max_attempts, created_at }
ctx.email.send(payload.to, "Your receipt", payload.body)?; // Err (or a failure) tries again later
Ok(())
})?;
ctx.queue.push("send-receipt", { to, body }, { delay: "5m", attempts: 5 })?;
A pushed job starts straight away (or after delay). A failed attempt waits 30 seconds, then a minute, two, and so on up to an hour; after attempts (3 by default, at most 20) it stays as failed. Modules → a module → Queue lists waiting and failed jobs, with Retry and Discard. A job left running when the site stopped goes back in the queue at start-up. Both need jobs = true.
Hooks and events
Listen with ctx.hooks.on(name, fn); every name below must be listed in the manifest's hooks, except settings.changed.
| Hook | Payload | The listener |
|---|---|---|
head | { path, locale } | returns extra <head> HTML for every page |
page.html | { path, locale, html } | returns the page's new HTML, or null to leave it. Runs on every page core renders (cached pages once, when built), one module after another by id, with 10 s each |
response.headers | { path, status, locale } | returns a map of headers to add to the page. Accepted: cache-control, link, vary, the security policies (content-security-policy, permissions-policy, referrer-policy, strict-transport-security, cross-origin-*), timing-allow-origin and anything starting x- |
page.published | the page | nothing to return |
media.uploaded | { id, filename, mime_type, byte_size, width, height, url, optimisable } | sent once the item's sizes exist |
media.optimised | { id, filename, settings, bytes: { original, next_gen: { webp, avif } } } | after new next-generation copies are written |
media.deleted | { id, filename } | |
cache.cleared | { reason, pages, path } | when core drops the page cache (content, media.optimised, …). Clearing through ctx.cache.clear does not send it |
core.ready | {} | once at start-up, when ctx.media, ctx.cache and ctx.pages can be used. Modules load before that, so apply anything that needs core here as well as in init |
settings.changed | every setting's value | after someone saves the module's settings form |
entry.created, entry.updated, entry.published, entry.unpublished, entry.deleted | the entry as ctx.content.entry gives it | from the admin or a module; entry.deleted before the row goes |
activity | { kind, subject_id, summary, user_id, at } | everything the activity log records (page.published, user.signed_in, user.invited, module.installed, …) |
sitemap.urls | {} | returns a list of paths, or { loc, lastmod, changefreq, priority }, for /sitemap.xml. Core lists published pages, content listings and entries itself |
robots.txt | { indexable } | returns lines to add to /robots.txt |
search.query | { q, locale } | returns a list of { title, url, description } to add to /search results after core's own |
<module>.<event> | what the module sent | see Events between modules |
Events between modules
ctx.events.emit("forms.submitted", payload) tells every running module listening on that hook (hooks = ["forms.submitted"] in theirs), without waiting. A module's events are named after it: <its id>.<anything>. ctx.hooks.call is the other way round: one module asks one other and waits for the answer.
Modules that need modules
[requires]
forms = ">=1.0"
A module loads only while each module it requires is installed, switched on, running, and at a version in range; otherwise it shows why ("needs module forms >=1.0, but it is switched off"). Modules load in an order that puts required ones first, a required module cannot be switched off while one that needs it runs, and switching one back on loads the modules that were waiting for it. ctx.modules.has("forms", ">=1.2") checks for an optional one at run time.