@port60/template-kit 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/bin/cli.mjs +26 -4
- package/package.json +1 -1
- package/src/commands/content.mjs +42 -0
- package/src/commands/create.mjs +6 -6
- package/src/commands/dev.mjs +97 -9
- package/src/commands/model.mjs +34 -0
- package/src/commands/packageCmd.mjs +3 -3
- package/src/commands/refresh.mjs +48 -0
- package/src/commands/upgrade.mjs +44 -0
- package/src/commands/validate.mjs +2 -3
- package/src/lib/agentsMd.mjs +52 -24
- package/src/vendor/contract/v1/behaviours.json +4 -4
- package/src/vendor/contract/v1/content-bounds.json +3 -3
- package/src/vendor/contract/v1/content-model.json +165 -0
- package/src/vendor/contract/v1/context.json +1497 -289
- package/src/vendor/contract/v1/dialect.json +3 -3
- package/src/vendor/contract/v1/fonts.json +1 -1
- package/src/vendor/contract/v1/imagery.json +4 -4
- package/src/vendor/contract/v1/islands.json +242 -235
- package/src/vendor/contract/v1/layout.json +4 -4
- package/src/vendor/contract/v1/manifest.schema.json +10 -12
- package/src/vendor/contract/v1/sections.json +189 -107
- package/src/vendor/contract/v1/tokens.json +1 -1
- package/src/vendor/contract/v1.lock.json +1 -1
- package/src/vendor/engine/content-footprint.mjs +79 -0
- package/src/vendor/validator/fixture-art.mjs +107 -0
- package/src/vendor/validator/model-reference.mjs +92 -0
- package/src/vendor/validator/platform-base.css +21 -0
- package/src/vendor/validator/preview.mjs +144 -59
- package/src/vendor/validator/site-context.mjs +97 -0
- package/src/vendor/validator/validate.mjs +70 -17
- package/starter/assets/theme.css +10 -10
- package/starter/layout.liquid +15 -16
- package/starter/manifest.json +2 -2
- package/starter/sections/campaigns.liquid +5 -5
- package/starter/sections/homeHero.liquid +11 -11
- package/starter/sections/people.liquid +1 -1
- package/starter/sections/values.liquid +1 -1
package/src/lib/agentsMd.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
// The scaffold's AI-agent briefing (T3, user requirement: the kit must let people put an AI
|
|
2
|
-
// engine on template work and be productive immediately). Written for ANY coding agent
|
|
3
|
-
// AGENTS.md is the cross-tool convention and CLAUDE.md carries the identical content for tools
|
|
2
|
+
// engine on template work and be productive immediately). Written for ANY coding agent, // AGENTS.md is the cross-tool convention and CLAUDE.md carries the identical content for tools
|
|
4
3
|
// that read that name. The briefing is the contract's rules + the iteration loop, so an agent's
|
|
5
4
|
// first move is always the same: read this, edit, validate --json, repeat until clean.
|
|
6
5
|
|
|
7
6
|
export function agentsMd(name) {
|
|
8
7
|
return `# Working on the "${name}" Port60 template
|
|
9
8
|
|
|
10
|
-
You are working on a **Port60 site template
|
|
9
|
+
You are working on a **Port60 site template**, a small, versioned artifact of Liquid renderers
|
|
11
10
|
and CSS that a charity's site is rendered through. It contains **no application code**: no
|
|
12
11
|
JavaScript, no API calls, no payment logic. Templates decide how a site *looks*; the platform
|
|
13
12
|
owns what it *does*.
|
|
@@ -22,25 +21,25 @@ npm run package # validate + produce the uploadable <name>-<version>.z
|
|
|
22
21
|
\`\`\`
|
|
23
22
|
|
|
24
23
|
**After every meaningful edit, run \`npm run validate:json\` and fix every error before moving
|
|
25
|
-
on.** The validator is the exact code the platform runs at upload
|
|
24
|
+
on.** The validator is the exact code the platform runs at upload, if it passes here, the
|
|
26
25
|
platform accepts it; if it fails here, the upload will fail identically.
|
|
27
26
|
|
|
28
27
|
## The file layout (nothing else is accepted)
|
|
29
28
|
|
|
30
|
-
- \`manifest.json
|
|
29
|
+
- \`manifest.json\`, identity + what you support. \`name\` and \`version\` are immutable
|
|
31
30
|
identity; bump \`version\` (semver) for every published change.
|
|
32
|
-
- \`layout.liquid
|
|
31
|
+
- \`layout.liquid\`, the page chrome (header/nav/footer). Must contain **exactly one**
|
|
33
32
|
\`{% content %}\` slot. Only needed when \`supports.layout\` is true.
|
|
34
|
-
- \`sections/<type>.liquid
|
|
33
|
+
- \`sections/<type>.liquid\`, one renderer per section type you declare in
|
|
35
34
|
\`supports.sections\`. Charities compose pages from a **closed catalogue** of section types
|
|
36
|
-
(see \`npm run validate\` output or the docs)
|
|
37
|
-
- \`pages/<page>.liquid
|
|
38
|
-
- \`assets/theme.css
|
|
39
|
-
allowed. **No images, no JS
|
|
35
|
+
(see \`npm run validate\` output or the docs), you cannot invent new types.
|
|
36
|
+
- \`pages/<page>.liquid\`, optional full-page templates for \`supports.pageTemplates\`.
|
|
37
|
+
- \`assets/theme.css\`, required, your entire look. More \`.css\` files under \`assets/\` are
|
|
38
|
+
allowed. **No images, no JS**, they are refused at upload.
|
|
40
39
|
|
|
41
40
|
## The rules the validator enforces (do not fight them)
|
|
42
41
|
|
|
43
|
-
1. **Escape-by-default.** Every output is HTML-escaped unless you use \`| raw
|
|
42
|
+
1. **Escape-by-default.** Every output is HTML-escaped unless you use \`| raw\`, and the only
|
|
44
43
|
values you may pass through raw are the contract's sanitised richtext fields.
|
|
45
44
|
2. **The dialect is a whitelist.** \`{% include %}\`, \`{% render %}\`, \`{% layout %}\` and
|
|
46
45
|
several other tags are excluded and fail at parse. Unknown filters throw.
|
|
@@ -55,7 +54,7 @@ platform accepts it; if it fails here, the upload will fail identically.
|
|
|
55
54
|
5. **Render budgets are real.** Runaway loops are killed (~1s per render). Keep renderers simple.
|
|
56
55
|
6. **Context is a whitelist.** Sections see \`{section, brand}\` plus only the collection named by
|
|
57
56
|
that section in the contract; layouts see \`{brand, nav, socials, worship, locale}\`; page
|
|
58
|
-
templates see their documented fixture + \`brand\`. Nothing else exists
|
|
57
|
+
templates see their documented fixture + \`brand\`. Nothing else exists, do not invent
|
|
59
58
|
variables.
|
|
60
59
|
7. **Capabilities are matching metadata, never entitlements.** Every value in
|
|
61
60
|
\`requiresCapabilities\` must have a declared section, page template or island that presents it.
|
|
@@ -77,15 +76,44 @@ platform accepts it; if it fails here, the upload will fail identically.
|
|
|
77
76
|
- Other platform routes keep their platform body and render inside your layout. Capability flags
|
|
78
77
|
expose documented optional context; they do not transfer transaction or route ownership.
|
|
79
78
|
|
|
79
|
+
## The content model
|
|
80
|
+
|
|
81
|
+
Everything you read comes from ONE tree: \`site\`, \`site.brand\`, \`site.nav\`,
|
|
82
|
+
\`site.socials\`, \`site.locale\` and the typed collections under \`site.content.*\`
|
|
83
|
+
(services, events, articles, campaigns, causes, courses, resources, locations,
|
|
84
|
+
schedules, about). Four rules it never breaks, so neither should you:
|
|
85
|
+
|
|
86
|
+
- Every collection is BOUNDED (a documented cap plus a \`moreHref\`), link onward, never
|
|
87
|
+
assume you have everything.
|
|
88
|
+
- Enum fields are OPEN, branch on the values you style and fall back for the rest; the
|
|
89
|
+
validator proves your template survives values it has never seen.
|
|
90
|
+
- Optional fields are explicitly nullable, always branch.
|
|
91
|
+
- The model only grows. The validator computes your content footprint from the paths you read
|
|
92
|
+
and stamps the minimum model version at publish; you never declare versions, and dynamic
|
|
93
|
+
indexing into \`site.content\` is refused so that stays decidable.
|
|
94
|
+
|
|
95
|
+
SEE the model: \`npm run dev\` serves the full reference with live example data at \`/model\`;
|
|
96
|
+
\`npx p60-template-kit model --json\` prints the machine-readable registry; the same reference
|
|
97
|
+
lives at https://developers.port60.com/reference/content-model/.
|
|
98
|
+
|
|
99
|
+
Bring your own content and imagery: \`npx p60-template-kit content .\` ejects every collection,
|
|
100
|
+
fully populated, as an editable JSON file; replace the copy and the imageUrl values, then
|
|
101
|
+
\`npx p60-template-kit dev . --content my-org.json\` renders YOUR data (the preview admits
|
|
102
|
+
exactly the image hosts your file names, nothing else).
|
|
103
|
+
|
|
104
|
+
You may replace the preview DATA with your own via \`preview-content.json\` beside the
|
|
105
|
+
manifest ({ collection: [items] }, schema-checked, hot-reloaded). The shape is fixed, packaging
|
|
106
|
+
excludes it, and conformance proofs always run on the canonical fixtures.
|
|
107
|
+
|
|
80
108
|
## What to build with
|
|
81
109
|
|
|
82
110
|
- Theme via CSS custom properties and the settings knobs you declare in
|
|
83
|
-
\`manifest.settings.schema
|
|
111
|
+
\`manifest.settings.schema\`, they surface in the charity's Appearance editor as
|
|
84
112
|
\`--p60s-<key>\` variables and \`data-p60s-<key>\` body attributes.
|
|
85
113
|
- Hero photographs (\`homeHero.images\`, when you declare \`supports.heroImagery\`): render one
|
|
86
114
|
photo directly as a TREATED backdrop (a scrim/tint built from your own palette variables via
|
|
87
|
-
color-mix
|
|
88
|
-
\`.hero-slide-scrim\`), and design the no-photo state as a gradient/colour
|
|
115
|
+
color-mix, never raw), place the \`hero_carousel\` island for two or more (style its
|
|
116
|
+
\`.hero-slide-scrim\`), and design the no-photo state as a gradient/colour, never a
|
|
89
117
|
placeholder. The starter's \`.lq-homehero\` is the reference; the validator checks all of this
|
|
90
118
|
behaviourally.
|
|
91
119
|
- "Looks" = named one-click bundles of knob values in \`manifest.looks\`.
|
|
@@ -93,19 +121,19 @@ platform accepts it; if it fails here, the upload will fail identically.
|
|
|
93
121
|
- Navigation can contain two levels below a top item. Render every supplied child and branch on
|
|
94
122
|
optional \`group\`, \`description\`, \`imageUrl\` and \`megaMenu\` promo metadata. Never hardcode
|
|
95
123
|
menu groups that are not in \`nav\`.
|
|
96
|
-
- Dynamic sections include appeals (\`causes\`), programmes (\`services\`), resources
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
- Submission and behaviour surfaces such as \`newsletter_signup\`, \`
|
|
100
|
-
\`
|
|
124
|
+
- Dynamic sections include appeals (\`causes\`), programmes (\`services\`), resources and
|
|
125
|
+
locations. Derive or omit when a collection is empty and use the supplied URLs rather than
|
|
126
|
+
constructing routes.
|
|
127
|
+
- Submission and behaviour surfaces such as \`newsletter_signup\`, \`form\`, \`search\`,
|
|
128
|
+
\`language_switch\` and \`next_prayer\` are platform islands. Place and style them;
|
|
101
129
|
never reproduce their API calls or consent behaviour.
|
|
102
130
|
- \`manifest.imagery.hero\` (optional but recommended): declare the photo shape YOUR hero
|
|
103
|
-
composes best with (\`idealAspect\`, \`minWidth\`, a one-line \`note\`)
|
|
131
|
+
composes best with (\`idealAspect\`, \`minWidth\`, a one-line \`note\`), the charity's editor
|
|
104
132
|
measures their actual upload against it and advises. Advice, never enforcement.
|
|
105
133
|
|
|
106
134
|
## Which contract this is
|
|
107
135
|
|
|
108
|
-
The section catalogue, islands and fixtures here are the **Charity Platform contract v1
|
|
136
|
+
The section catalogue, islands and fixtures here are the **Charity Platform contract v1**, the
|
|
109
137
|
platform's first product surface. The dialect, the rules above and this toolchain are
|
|
110
138
|
platform-wide; other Port60 products will ship their own contract packs. Do not assume the
|
|
111
139
|
current section list is universal.
|
|
@@ -113,7 +141,7 @@ current section list is universal.
|
|
|
113
141
|
## Reference
|
|
114
142
|
|
|
115
143
|
The full generated reference (sections, islands, context variables, tokens, dialect) lives at
|
|
116
|
-
https://developers.port60.com
|
|
144
|
+
https://developers.port60.com, also available in one file for agents at
|
|
117
145
|
https://developers.port60.com/llms-full.txt
|
|
118
146
|
`;
|
|
119
147
|
}
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"name": "stickyHeader",
|
|
65
65
|
"status": "available",
|
|
66
66
|
"label": "Sticky header condense",
|
|
67
|
-
"description": "State classes for scroll-aware headers: past a small threshold the engine adds is-condensed; with the \"auto-hide\" value, scrolling down past 160px adds is-hidden and scrolling up removes it. The engine only ever toggles classes (rAF-throttled)
|
|
67
|
+
"description": "State classes for scroll-aware headers: past a small threshold the engine adds is-condensed; with the \"auto-hide\" value, scrolling down past 160px adds is-hidden and scrolling up removes it. The engine only ever toggles classes (rAF-throttled), what condensing or hiding looks like, and whether it animates, is entirely the template's CSS (gate transitions behind prefers-reduced-motion yourself; the classes always apply because a condensed header is layout, not decoration).",
|
|
68
68
|
"attributes": [
|
|
69
69
|
{ "attr": "data-p60-sticky-header", "value": "none for condense-only; \"auto-hide\" adds the hide-on-scroll-down pair" }
|
|
70
70
|
],
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"name": "stickyCta",
|
|
76
76
|
"status": "available",
|
|
77
77
|
"label": "Sticky call-to-action bar",
|
|
78
|
-
"description": "Adds is-stuck once the visitor scrolls past the threshold (the attribute value in px, default 480) and removes it above. The bar should DUPLICATE an action that already exists in the page (a donate button, a ticket link)
|
|
78
|
+
"description": "Adds is-stuck once the visitor scrolls past the threshold (the attribute value in px, default 480) and removes it above. The bar should DUPLICATE an action that already exists in the page (a donate button, a ticket link); because of that, this is the one behaviour whose element may be hidden unconditionally in CSS rather than .p60-js-scoped: a no-JS visitor loses nothing; the original action is still on the page.",
|
|
79
79
|
"attributes": [
|
|
80
80
|
{ "attr": "data-p60-sticky-cta", "value": "optional scroll threshold in px (default 480)" }
|
|
81
81
|
],
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"name": "lightbox",
|
|
87
87
|
"status": "available",
|
|
88
88
|
"label": "Lightbox",
|
|
89
|
-
"description": "On a group container: each data-p60-lightbox-item is an anchor whose href is the full image (the no-JS render simply navigates to it
|
|
89
|
+
"description": "On a group container: each data-p60-lightbox-item is an anchor whose href is the full image (the no-JS render simply navigates to it, already functional). The engine intercepts the click and opens an injected overlay (.p60-lightbox) with the image, a caption from data-p60-caption or the thumbnail's alt, a close button, backdrop and Escape close, arrow-key prev/next within the group, and a focus trap. Reduced motion: no transition on the overlay (the engine sets none; style the steady state).",
|
|
90
90
|
"attributes": [
|
|
91
91
|
{ "attr": "data-p60-lightbox", "value": "none; the group container" },
|
|
92
92
|
{ "attr": "data-p60-lightbox-item", "value": "none; on each anchor to a full image" },
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"name": "tabs",
|
|
101
101
|
"status": "available",
|
|
102
102
|
"label": "Tabs",
|
|
103
|
-
"description": "data-p60-tab=\"key\" on each tab control and data-p60-panel=\"key\" on each panel, inside a data-p60-tabs container. The engine wires the tablist/tab/tabpanel roles, aria-selected, roving tabindex, arrow/Home/End keys, toggles is-active on the active pair and hides inactive panels with the hidden attribute
|
|
103
|
+
"description": "data-p60-tab=\"key\" on each tab control and data-p60-panel=\"key\" on each panel, inside a data-p60-tabs container. The engine wires the tablist/tab/tabpanel roles, aria-selected, roving tabindex, arrow/Home/End keys, toggles is-active on the active pair and hides inactive panels with the hidden attribute, so the no-JS render shows every panel stacked and complete, with no CSS scoping needed. The first tab (or the one authored with is-active) starts active.",
|
|
104
104
|
"attributes": [
|
|
105
105
|
{ "attr": "data-p60-tabs", "value": "none; the container" },
|
|
106
106
|
{ "attr": "data-p60-tab", "value": "the key of the panel this control activates" },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "The maximum lengths of every admin-entered display field a template renders. The admin screens stop input at these caps and the API rejects anything longer, so a template can design cards, headings and captions against known worst cases. Additive-only within a contract major: a bound may loosen, never tighten. Long-form bodies (article bodies, course About pages, service pages) are deliberately outside this table
|
|
2
|
+
"description": "The maximum lengths of every admin-entered display field a template renders. The admin screens stop input at these caps and the API rejects anything longer, so a template can design cards, headings and captions against known worst cases. Additive-only within a contract major: a bound may loosen, never tighten. Long-form bodies (article bodies, course About pages, service pages) are deliberately outside this table; they are sanitised HTML rendered in dedicated full-width regions, never inside cards.",
|
|
3
3
|
"groups": [
|
|
4
4
|
{
|
|
5
5
|
"name": "Brand (layout chrome)",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
{
|
|
69
69
|
"field": "Tag",
|
|
70
70
|
"max": 40,
|
|
71
|
-
"note": "Each
|
|
71
|
+
"note": "Each, rendered as chips."
|
|
72
72
|
}
|
|
73
73
|
]
|
|
74
74
|
},
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
{
|
|
110
110
|
"field": "Page name",
|
|
111
111
|
"max": 200,
|
|
112
|
-
"note": "The body is sanitised HTML rendered full-width
|
|
112
|
+
"note": "The body is sanitised HTML rendered full-width, not card content."
|
|
113
113
|
}
|
|
114
114
|
]
|
|
115
115
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "The CONTENT MODEL REGISTRY (docs/template-content-model.md, v1): the typed `site.content.*` tree every template reads. One realistic organisation, one shape. Four disciplines are baked in from birth because they cannot be retrofitted: every collection is BOUNDED (a documented cap and an explicit moreHref, so a big tenant can never break a render budget); every enum field is OPEN (values grow; templates must survive unknown values, proven behaviourally); every optional field is EXPLICITLY nullable (branch-on-it is the norm); and the model is ADD-ONLY (fields and collections gain `deprecated: true` markers, they are never removed within a major, deprecated means it stops being documented, never that it stops being served). Versioning is COMPUTED, never author-declared: every collection and field carries `since`, the validator derives a template's required minimum from its content footprint at publish, and authors never think about versions. New list or field = minor. Fixture or doc clarification = patch.",
|
|
3
|
+
"version": "1.0",
|
|
4
|
+
"root": "site",
|
|
5
|
+
"siblings": {
|
|
6
|
+
"description": "site.brand, site.nav, site.socials and site.locale carry the SAME shapes documented in context.json for the flat era; the tree re-addresses them, it does not reshape them.",
|
|
7
|
+
"keys": ["brand", "nav", "socials", "locale"]
|
|
8
|
+
},
|
|
9
|
+
"collections": {
|
|
10
|
+
"services": {
|
|
11
|
+
"since": "1.0",
|
|
12
|
+
"cap": 24,
|
|
13
|
+
"moreHref": "/services",
|
|
14
|
+
"description": "The organisation's published content pages (the services CMS), newest curation order first.",
|
|
15
|
+
"item": {
|
|
16
|
+
"id": { "type": "string", "since": "1.0" },
|
|
17
|
+
"title": { "type": "string", "since": "1.0", "max": 200 },
|
|
18
|
+
"slug": { "type": "string", "since": "1.0" },
|
|
19
|
+
"summary": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
|
|
20
|
+
"imageUrl": { "type": "string", "since": "1.0", "nullable": true },
|
|
21
|
+
"href": { "type": "string", "since": "1.0" }
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"events": {
|
|
25
|
+
"since": "1.0",
|
|
26
|
+
"cap": 12,
|
|
27
|
+
"moreHref": "/events",
|
|
28
|
+
"description": "Published events, soonest first. `registrationMode` is an OPEN enum: INFO, RSVP and TICKETED exist today and new modes will arrive; branch on the ones you style and fall back for the rest.",
|
|
29
|
+
"item": {
|
|
30
|
+
"id": { "type": "string", "since": "1.0" },
|
|
31
|
+
"name": { "type": "string", "since": "1.0", "max": 200 },
|
|
32
|
+
"description": { "type": "string", "since": "1.0", "nullable": true, "max": 200 },
|
|
33
|
+
"registrationMode": { "type": "string", "since": "1.0", "enumOpen": ["INFO", "RSVP", "TICKETED"] },
|
|
34
|
+
"startsAt": { "type": "string", "since": "1.0" },
|
|
35
|
+
"endsAt": { "type": "string", "since": "1.0", "nullable": true },
|
|
36
|
+
"venueName": { "type": "string", "since": "1.0", "nullable": true, "max": 200 },
|
|
37
|
+
"online": { "type": "boolean", "since": "1.0" },
|
|
38
|
+
"imageUrl": { "type": "string", "since": "1.0", "nullable": true },
|
|
39
|
+
"onSale": { "type": "boolean", "since": "1.0" },
|
|
40
|
+
"soldOut": { "type": "boolean", "since": "1.0" },
|
|
41
|
+
"priceFrom": { "type": "number", "since": "1.0", "nullable": true },
|
|
42
|
+
"currency": { "type": "string", "since": "1.0", "nullable": true },
|
|
43
|
+
"detailHref": { "type": "string", "since": "1.0" }
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"articles": {
|
|
47
|
+
"since": "1.0",
|
|
48
|
+
"cap": 12,
|
|
49
|
+
"moreHref": "/articles/all",
|
|
50
|
+
"description": "Published articles, newest first. Detail bodies stay editor-owned sanitised richtext on the article page BY DESIGN; the card model carries everything a listing needs.",
|
|
51
|
+
"item": {
|
|
52
|
+
"id": { "type": "string", "since": "1.0" },
|
|
53
|
+
"title": { "type": "string", "since": "1.0", "max": 200 },
|
|
54
|
+
"slug": { "type": "string", "since": "1.0" },
|
|
55
|
+
"excerpt": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
|
|
56
|
+
"imageUrl": { "type": "string", "since": "1.0", "nullable": true },
|
|
57
|
+
"authorName": { "type": "string", "since": "1.0", "nullable": true, "max": 120 },
|
|
58
|
+
"authorRole": { "type": "string", "since": "1.0", "nullable": true, "max": 120 },
|
|
59
|
+
"categories": { "type": "string[]", "since": "1.0" },
|
|
60
|
+
"publishedAt": { "type": "string", "since": "1.0" },
|
|
61
|
+
"readingMinutes": { "type": "number", "since": "1.0", "nullable": true },
|
|
62
|
+
"href": { "type": "string", "since": "1.0" }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"campaigns": {
|
|
66
|
+
"since": "1.0",
|
|
67
|
+
"cap": 12,
|
|
68
|
+
"moreHref": "/campaigns",
|
|
69
|
+
"description": "Live campaigns, curation order.",
|
|
70
|
+
"item": {
|
|
71
|
+
"id": { "type": "string", "since": "1.0" },
|
|
72
|
+
"title": { "type": "string", "since": "1.0", "max": 200 },
|
|
73
|
+
"href": { "type": "string", "since": "1.0" },
|
|
74
|
+
"summary": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
|
|
75
|
+
"imageUrl": { "type": "string", "since": "1.0", "nullable": true }
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"causes": {
|
|
79
|
+
"since": "1.0",
|
|
80
|
+
"cap": 12,
|
|
81
|
+
"moreHref": "/donate",
|
|
82
|
+
"description": "Causes currently accepting donations, quick-give first. `paymentOptions` mirrors the public cause contract.",
|
|
83
|
+
"item": {
|
|
84
|
+
"id": { "type": "string", "since": "1.0" },
|
|
85
|
+
"title": { "type": "string", "since": "1.0", "max": 200 },
|
|
86
|
+
"slug": { "type": "string", "since": "1.0" },
|
|
87
|
+
"description": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
|
|
88
|
+
"imageUrl": { "type": "string", "since": "1.0", "nullable": true },
|
|
89
|
+
"targetAmount": { "type": "number", "since": "1.0", "nullable": true },
|
|
90
|
+
"raisedAmount": { "type": "number", "since": "1.0", "nullable": true },
|
|
91
|
+
"progressPercentage": { "type": "number", "since": "1.0", "nullable": true },
|
|
92
|
+
"href": { "type": "string", "since": "1.0" },
|
|
93
|
+
"paymentOptions": { "type": "object", "since": "1.0", "nullable": true }
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"courses": {
|
|
97
|
+
"since": "1.0",
|
|
98
|
+
"cap": 12,
|
|
99
|
+
"moreHref": "/courses",
|
|
100
|
+
"description": "Enrollable courses. The long-form About body is sanitised richtext on the course page, never in cards.",
|
|
101
|
+
"item": {
|
|
102
|
+
"id": { "type": "string", "since": "1.0" },
|
|
103
|
+
"title": { "type": "string", "since": "1.0", "max": 200 },
|
|
104
|
+
"description": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
|
|
105
|
+
"imageUrl": { "type": "string", "since": "1.0", "nullable": true },
|
|
106
|
+
"venue": { "type": "string", "since": "1.0", "nullable": true, "max": 200 },
|
|
107
|
+
"audience": { "type": "string", "since": "1.0", "enumOpen": ["ADULT", "CHILD", "FAMILY"] },
|
|
108
|
+
"priceFrom": { "type": "number", "since": "1.0", "nullable": true },
|
|
109
|
+
"currency": { "type": "string", "since": "1.0", "nullable": true },
|
|
110
|
+
"soldOut": { "type": "boolean", "since": "1.0" },
|
|
111
|
+
"href": { "type": "string", "since": "1.0" }
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"resources": {
|
|
115
|
+
"since": "1.0",
|
|
116
|
+
"cap": 24,
|
|
117
|
+
"moreHref": "/resources",
|
|
118
|
+
"description": "Published documents, grouped by folder.",
|
|
119
|
+
"item": {
|
|
120
|
+
"id": { "type": "string", "since": "1.0" },
|
|
121
|
+
"title": { "type": "string", "since": "1.0", "max": 200 },
|
|
122
|
+
"folder": { "type": "string", "since": "1.0", "nullable": true },
|
|
123
|
+
"url": { "type": "string", "since": "1.0" },
|
|
124
|
+
"contentType": { "type": "string", "since": "1.0", "nullable": true },
|
|
125
|
+
"bytes": { "type": "number", "since": "1.0", "nullable": true },
|
|
126
|
+
"publishedAt": { "type": "string", "since": "1.0", "nullable": true }
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"locations": {
|
|
130
|
+
"since": "1.0",
|
|
131
|
+
"cap": 12,
|
|
132
|
+
"moreHref": "/locations",
|
|
133
|
+
"description": "The organisation's physical places, primary first.",
|
|
134
|
+
"item": {
|
|
135
|
+
"id": { "type": "string", "since": "1.0" },
|
|
136
|
+
"name": { "type": "string", "since": "1.0", "max": 200 },
|
|
137
|
+
"address": { "type": "string", "since": "1.0", "nullable": true, "max": 500 },
|
|
138
|
+
"latitude": { "type": "number", "since": "1.0", "nullable": true },
|
|
139
|
+
"longitude": { "type": "number", "since": "1.0", "nullable": true },
|
|
140
|
+
"primary": { "type": "boolean", "since": "1.0" },
|
|
141
|
+
"directionsHref": { "type": "string", "since": "1.0", "nullable": true }
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"schedules": {
|
|
145
|
+
"since": "1.0",
|
|
146
|
+
"cap": 4,
|
|
147
|
+
"moreHref": null,
|
|
148
|
+
"description": "TYPE-DERIVED recurring schedules. `type` is an OPEN enum; 'worship' is the first (its shape is the layout worship contract: times, labels, today, next). Other institutions arrive as new types; render the types you know, ignore the rest.",
|
|
149
|
+
"item": {
|
|
150
|
+
"type": { "type": "string", "since": "1.0", "enumOpen": ["worship"] },
|
|
151
|
+
"content": { "type": "object", "since": "1.0" }
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"about": {
|
|
155
|
+
"since": "1.0",
|
|
156
|
+
"cap": 24,
|
|
157
|
+
"moreHref": null,
|
|
158
|
+
"description": "The admin-authored page composition: the ordered sections of the home/about surface. `type` is the section catalogue's OPEN enum; `content` is that section type's fields. This is what `section.*` re-addresses; a mega-page template can walk it directly.",
|
|
159
|
+
"item": {
|
|
160
|
+
"type": { "type": "string", "since": "1.0", "enumOpen": [] },
|
|
161
|
+
"content": { "type": "object", "since": "1.0" }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|