Modules
Content, widgets and templates
Content
With content = "read" a module reads the site's structured content: ctx.content.types() (each with its fields, taxonomies and path), ctx.content.terms("categories"), and entries. An entry comes as its row (id, title, slug, status, excerpt, fields, featured_media_id, published_at, …) with type (the type's slug), path, url and terms ([{ id, slug, name, taxonomy }]). entries(type, { status }) takes "published" (the default), "draft" or "all"; term a term's slug or id; q text in the title or summary; order published_at, title, created_at or updated_at, with desc.
With content = "write" it creates and changes entries through the same checks as the admin: field values are tidied against the type (unknown fields dropped, rich text cleaned, select values checked), slugs made safe and unique within the type. update merges fields, so one field can change alone. Writes are recorded in the activity log under the module's name and send the entry events below.
ctx.content.ensure_type({ slug, name, singular, description, fields, has_archive, show_in_nav }) makes a content type the module needs when the site has none at that slug, and otherwise adds the fields it lacks. It never removes or changes what is there, so the site's own edits stay.
Widgets
fn(settings, place): place is { path, lang, builder, title }, where builder is true while the page is open in the page builder (show a placeholder there rather than nothing). Pages are cached, so nothing about the visitor is passed; a widget that must differ per visitor should load that part from a module route.
Settings describe the inspector's inputs:
type | Input |
|---|---|
text, textarea, rich, code | text, a text area, the rich text editor, a code area |
int, float, number | a number |
bool | a switch |
select | a list: options as values or [value, label] pairs |
seg | buttons side by side, options likewise |
colour | one of the theme's colours |
lines | a list of strings, one per line |
media | a library item, stored as media_id (one per widget) |
items | a repeating list, stored as items: fields: [["title", "Title"], ["text", "Text", "textarea"], ["media_id", "Image", "media"]] |
content_type, taxonomy, icon, level | one of the site's content types, taxonomies, icons, or a heading level |
Each takes label, default and help. options_from: "forms.options" fills a select's choices when the builder opens: core calls that hook in the module (listed in hooks) and uses the list it answers.
Theme styles
A module whose public markup has its own look (a product card, a booking calendar) can let the site owner restyle it from the theme. Its stylesheet reads custom properties with a fallback, and init declares a style section with a control for each:
// assets/shop.css: .shop-card { border-radius: var(--gsy-shop-card-radius, 8px); background: var(--gsy-shop-card-bg, var(--gsy-color-surface)); }
ctx.theme.style_section({
id: "cards", label: "Product cards", description: "The cards in product lists.",
groups: [{ label: "Card", controls: [
{ token: "shop-card-bg", label: "Background", type: "colour", default: "var(--gsy-color-surface)" },
{ token: "shop-card-radius", label: "Corners", type: "range", min: 0, max: 24, step: 1, unit: "px", default: "8px" },
{ token: "shop-card-shadow", label: "Shadow", type: "choice", options: [["none", "None"], ["0 4px 16px rgb(0 0 0 / .12)", "Soft"]], default: "none" }
] }],
sample: "<link rel=\"stylesheet\" href=\"" + ctx.assets.url("shop.css") + "\"><div class=\"shop-card\">Oak stool <b>£120</b></div>"
})?;
Each control sets one key of the theme's custom map, which every page carries as --gsy-<token> on :root. Controls take token, label, type, and optionally help and default (the CSS value your stylesheet falls back to, shown beside the control; Reset goes back to it):
type | Asks for | Also takes |
|---|---|---|
colour | one of the theme's colours (stored as var(--gsy-color-<name>)) or a colour picked by hand (a hex value) | |
range | a number on a slider, stored with its unit (12px) | min, max (required), step (default 1), unit (px, rem, em, %, or none) |
choice | one of a few values, as buttons or a list | options: [value, label] pairs or plain values |
text | any CSS value, typed |
A section usually styles several elements: give each group an id (lowercase letters, digits and dashes, unique in the section) and the editor shows the groups as a list of elements, one at a time, with the chosen one in the address. Mark the parts of sample each element styles with data-style="<group id>": in the preview they outline on hover, and a click opens that element's controls. The forms module (gansey-module-forms, from 2.0) is the example: one group per part of a form, from text boxes to the Back button.
groups: [
{ id: "card", label: "Card", controls: [ … ] },
{ id: "price", label: "Price tag", selector: ".shop-card .shop-price", controls: [ … ] },
],
sample: "<div class=\"shop-card\" data-style=\"card\">Oak stool <b class=\"shop-price\" data-style=\"price\">£120</b></div>"
Each element has its own Custom CSS box under its controls, and the section ends with one for the whole section, for what the controls do not cover. That CSS is stored with the theme (section_css, keyed <module id>/<section id> and <module id>/<section id>/<group id>) and added at the end of every page, after the stylesheets modules link from their widgets, so it wins over both. Give a group a selector (the CSS selector for that element on real pages: ".shop-card .shop-price") and its box shows it as the example to start from; give the section a css_hint for the section-wide box.
Every token must start with the module's id and a dash (shop-…) and may not use core's own sets (captcha-, color-, font-, button- and the like). A section id is lowercase letters, digits and dashes, unique within the module, and each token is declared once. A wrong type, a missing field, an unknown key or a token outside the prefix answers Err with the reason, so ? stops init and the Modules screen shows it.
The section appears under Theme → Site parts → Styles (after core's Robot check, by label) and on the Tokens screen's Modules tab, its controls beside a preview of sample on a page inside the site's header and footer. What the site owner chooses is saved and published with the theme. A switched-off module's sections leave the screen; values already saved stay in the theme and do nothing until it comes back. The sample is cleaned before the admin shows it: markup, classes, inline styles, <style> and stylesheet links stay, scripts and event attributes go, so draw it static. Core styles no forms of its own: the forms module (gansey-module-forms, from 2.0) is the example to follow. Its Forms section declares the forms-* keys its stylesheet reads, so without it installed the theme has no Forms style at all.
No manifest permission is asked for: a section only describes controls, its keys are confined to the module's own prefix, and nothing changes until someone who may edit the theme saves it.
Templates
{{ post.title }} escapes; {{{ post.html }}} does not. {% if x %}…{% else %}…{% endif %} and {% for p in posts %}…{% endfor %} (with loop.index, loop.first, loop.last). Filters: | upper, | lower, | trim, | length, | default("—"), | truncate(80), | date("%d %B %Y"), | json, | slug. Templates hold no logic beyond that; it belongs in the script.
Assets
Files in the module's assets/ folder are served at /_modules/<id>/<path> while the module runs. ctx.assets.url("form.css") gives the address with a hash of the file (?v=1a2b3c4d); requests with it are cached by browsers for a year, so a changed file gets a new address. Hidden files and anything outside the folder are never served.