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.

HookPayloadThe 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.publishedthe pagenothing 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.changedevery setting's valueafter someone saves the module's settings form
entry.created, entry.updated, entry.published, entry.unpublished, entry.deletedthe entry as ctx.content.entry gives itfrom 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 sentsee 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.