Test / test (pull_request) Failing after 1m5s
slideshow on a page: max_height in svh, slides with an image (a path in the content repo or https), a caption laid over the picture's foot, and optionally the picture's size so its place is held before it loads. Swiper upgrades it; before that it is a row that scrolls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GT37Z1Xtfd9pUuQtMTg6Yt
279 lines
10 KiB
Markdown
279 lines
10 KiB
Markdown
# Content reference
|
|
|
|
A content repo is the site. This is everything it may say.
|
|
|
|
```
|
|
site.yaml branding, language, mail
|
|
aggregates.yaml the state machine of every bucket
|
|
needs.yaml what the site is for (read by iris, never by portal)
|
|
questions/
|
|
index.yaml the front page, served at /
|
|
about.yaml served at /about
|
|
review/
|
|
_section.yaml applies to everything under /review
|
|
index.yaml served at /review
|
|
[record].yaml serves /review/<anything>
|
|
```
|
|
|
|
Nothing here is required except a `questions/` tree with something in
|
|
it. Portal's lint refuses content it cannot serve honestly, at boot,
|
|
on every hot reload, and in CI through `iris check`.
|
|
|
|
## site.yaml
|
|
|
|
```yaml
|
|
title: Vestmark kommune # the browser title and the header
|
|
wordmark: /site/logo.svg # an image instead of the title
|
|
wordmark_invert: true # flip it in dark mode
|
|
wordmark_height: 2.4 # rem
|
|
lang: no # the interface language: en or no
|
|
stylesheet: /site/theme.css # served same-origin from the repo
|
|
favicon: /site/icon.svg
|
|
hero: { kind: module, module: hero.js } # or { kind: plain }
|
|
mail:
|
|
from: Vestmark kommune <post@vestmark.example>
|
|
reply_to: post@vestmark.example
|
|
invite: { subject: "...", body: "..." } # rewords the built-in invite mail
|
|
```
|
|
|
|
`mail.from` is required as soon as any state declares a mail.
|
|
|
|
Any text a person reads (`name`, `description`, `label`,
|
|
`placeholder`, a mail's `subject` and `body`, `consequence`,
|
|
`encouragements`) may be a list instead of one string: one entry per
|
|
[storyline](storylines.md), all lists the same length.
|
|
|
|
## A question
|
|
|
|
One YAML file is one page.
|
|
|
|
```yaml
|
|
id: /apply # defaults to the file's path
|
|
name: Apply to build
|
|
description: What we need from you.
|
|
qualifies: case_officers # a Kanidm group may open this page
|
|
requires_chain: /start # or: an answer they must already have given
|
|
followup: true # keep it out of the nav until earned
|
|
responsible: { name: Solveig, contact: post@vestmark.example }
|
|
# or a group's: no account is made for the inbox, and any signed-in
|
|
# member of the group may suggest a change to the page
|
|
responsible: { name: The board, contact: board@vestmark.example, group: board }
|
|
event: # this page announces an occasion
|
|
starts: 2026-10-14T18:00
|
|
duration: 2h
|
|
place: The town hall
|
|
place: # this page is about somewhere
|
|
lat: 59.8938
|
|
lon: 11.0332
|
|
zoom: 15
|
|
name: The town hall
|
|
alternatives: [ ... ]
|
|
```
|
|
|
|
`qualifies`, `requires_chain` and `responsible` are inherited from the
|
|
nearest `_section.yaml`. `event` makes a page an occasion with a time;
|
|
`place` makes it a place, drawing a still map under the question and
|
|
opening any `type: location` field on the page there rather than on
|
|
the whole world.
|
|
|
|
### Pictures across the page
|
|
|
|
`slideshow` lays a strip of pictures under the question, the full
|
|
width of the screen: every picture as tall as the strip and as wide as
|
|
its own shape, swiped or arrowed through, its caption over its foot.
|
|
|
|
```yaml
|
|
slideshow:
|
|
max_height: 60 # svh; 60 unless set, between 20 and 100
|
|
slides:
|
|
- image: images/built/01.webp # a path in the content repo, or https://
|
|
caption: The canteen at the theatre, renewed
|
|
width: 3207 # the picture's own size, optional:
|
|
height: 1400 # holds its place before it has loaded
|
|
```
|
|
|
|
Pictures in the content repo are served from this origin under
|
|
`/site/`; `webp`, `avif` and `jpeg` may be kept by the browser for an
|
|
hour. Keep them web-sized: around 1400 pixels tall is plenty.
|
|
|
|
## An alternative
|
|
|
|
One choice. Never two actions behind one heading: if two things can be
|
|
done, they are two alternatives.
|
|
|
|
```yaml
|
|
alternatives:
|
|
- name: Send the drawings
|
|
description: We answer within twelve weeks.
|
|
action: /sent # the page to go to; /shape/{field} templates
|
|
record_as: applications # the bucket the answer lands in
|
|
disabled: false
|
|
consequence: [What happens next]
|
|
encouragements: [It takes two minutes]
|
|
images: [/site/drawing.png]
|
|
features: [ ... ]
|
|
```
|
|
|
|
Three things an alternative may do instead of, or beside, recording an
|
|
answer:
|
|
|
|
```yaml
|
|
self_transition: # the person moves their own record
|
|
bucket: applications
|
|
to: complete
|
|
from: [open, incomplete] # defaults to the initial state alone
|
|
label: Send them
|
|
invite: { group: case_officers } # onboard someone into Kanidm
|
|
```
|
|
|
|
`self_transition.from` is what keeps a person in dialogue with their
|
|
case: an applicant written to for a missing drawing answers from
|
|
`incomplete`, and may withdraw from wherever the case has got to.
|
|
Omitted, it is the initial state alone, which is what a one-shot
|
|
unsubscribe link means. Each state listed becomes its own access rule,
|
|
and the lint refuses a `from -> to` the state machine does not declare.
|
|
|
|
An `invite` needs a required `name` and `email` on the same
|
|
alternative, and the inviting group must be allowed to invite that
|
|
group by the policy.
|
|
|
|
## A feature
|
|
|
|
What an alternative offers, explained. A feature that is only layout
|
|
is a feature wasted: each one says something true about the choice.
|
|
|
|
```yaml
|
|
features:
|
|
- name: What you need
|
|
description: The case number from the letter.
|
|
icon: document
|
|
color: "#8ec2c0"
|
|
requirements: [ ... ] # form fields
|
|
resource: { ... } # live data
|
|
```
|
|
|
|
## A requirement (a form field)
|
|
|
|
```yaml
|
|
requirements:
|
|
- name: email # snake_case; this is the stored key
|
|
label: Your email
|
|
placeholder: you@example.no
|
|
type: email
|
|
optional: false
|
|
value: "{key}" # a preset; a dynamic page's segment fills it
|
|
```
|
|
|
|
`value: "{user.email}"` or `"{user.name}"` is the signed-in person's
|
|
own, and a field it fills is carried rather than asked: someone the
|
|
site knows is not asked who they are. A visitor gets the field, empty.
|
|
|
|
`type` is one of:
|
|
|
|
| type | what it is |
|
|
|---|---|
|
|
| `text` | the default |
|
|
| `email`, `tel`, `date`, `number`, … | anything else is passed straight through as the HTML input type |
|
|
| `textarea` | several lines |
|
|
| `prosekit` | a rich-text editor |
|
|
| `select` | one choice, or `multiple: true` for any number |
|
|
| `file` | an upload, with `accept:` and `multiple:` |
|
|
| `location` | a pin on a map, stored as `{lat, lon, zoom}` |
|
|
| `record` | the record this one belongs to |
|
|
| `gesture`, `voice` | a drawn stroke or a recording, through a redoal relay |
|
|
| `hidden` | a carrier value, no label row |
|
|
| `module` | a widget the content repo ships |
|
|
|
|
A `select` takes either inline `options: [...]` or a `resource:` to
|
|
draw them from, with `id_field:` naming each item's stable id.
|
|
|
|
A field may load its value from a resource when a sibling changes:
|
|
|
|
```yaml
|
|
- name: contents
|
|
type: textarea
|
|
bind:
|
|
field: path # the sibling to watch
|
|
resource: { source: { kind: url, url: "https://git.example/raw/{path}" } }
|
|
```
|
|
|
|
### type: record
|
|
|
|
How one case belongs to another — an objection to the application it
|
|
objects to, an appeal to the decision it appeals.
|
|
|
|
```yaml
|
|
- name: case
|
|
type: record
|
|
of: applications # the bucket the other record is in
|
|
value: "{key}" # arrives on the link, on a dynamic page
|
|
```
|
|
|
|
Nobody types one of these: the stored value is the parent's record id,
|
|
which is a chain hash. It arrives the way it was given out, so the
|
|
field carries rather than asks, and renders hidden. A submission
|
|
naming a record that is not there is refused, so a reference in a
|
|
stored record is one that resolved at least once.
|
|
|
|
The other half is a listing that asks for the children:
|
|
|
|
```yaml
|
|
resource:
|
|
source: { kind: kv, bucket: objections }
|
|
requires_group: case_officers
|
|
where: { field: case, equals: "{key}" }
|
|
```
|
|
|
|
And the way there from a list: `link` puts a link on every listed
|
|
record to a page about it, with `{id}` for the record's id and
|
|
`{field}` for any of its fields. A notice on a public board links to
|
|
`/notices/{id}`, the dynamic page `/notices/[key]` whose form carries
|
|
the notice's id back in with `value: "{key}"`:
|
|
|
|
```yaml
|
|
resource:
|
|
source: { kind: kv, bucket: notices }
|
|
public: true
|
|
link: { to: "/notices/{id}", label: I can lend or borrow this }
|
|
```
|
|
|
|
## A resource
|
|
|
|
Live data, declared by content and read by the server. What a resource
|
|
reads is never a client-supplied parameter.
|
|
|
|
```yaml
|
|
resource:
|
|
source: { kind: kv, bucket: applications }
|
|
key: abc123 # one record instead of the list
|
|
public: true # or requires_group: case_officers
|
|
states: [approved, refused] # only these; empty means all
|
|
where: { field: case, equals: "{key}" }
|
|
jq: ".[] | {name, url: .html_url}"
|
|
empty: Nothing decided yet.
|
|
link: { to: "/notices/{id}", label: Answer this } # a page per record
|
|
transitions: # the desk's buttons
|
|
- { from: complete, to: on_hearing, label: Send it out }
|
|
```
|
|
|
|
`source.kind` is one of `kv`, `gitea_starred`,
|
|
`gitea_releases`, `url`.
|
|
|
|
A spec with neither `public: true` nor `requires_group` is
|
|
unreachable: reads fail closed, and mutations always need a group
|
|
whatever `public` says.
|
|
|
|
`states` and `where` narrow a listing. They matter most for a register
|
|
that is public by law — public about what was decided, not about what
|
|
was withdrawn — while the desk working the same bucket is a different
|
|
spec and still sees all of it. A record that has no such field never
|
|
matches: "everything that did not say" is not something a page can ask
|
|
for by accident.
|
|
|
|
## needs.yaml
|
|
|
|
What the site is for, as a checkable spec: who wants what, which
|
|
bucket the answer must land in, which group carries it to a finished
|
|
state. The runtime never reads it. `iris check` proves the site meets
|
|
it, and `iris replay` runs cases through the real engine.
|