Documentation

# Reference

Every field type, every block and every operator the spec accepts — generated from the schema itself, so this page cannot describe a version that does not exist.

## A site is a directory
my-site/
  site.yaml            name, theme, header and footer, integrations
  content/*.yaml       one content type per file
  pages/*.yaml         one page per file
  logic/*.yaml         one automation per file
  data/*.yaml          rows, if you keep any in version control

npx @forinda/fcms-cli init my-site writes that, already valid and already canonical. The file a thing lives in is derived from its key, so fcms fmt may move a node into the file it belongs in — that is the bargain, and it is what keeps round-tripping exact.

## A content type
key: property
label: Property
labelPlural: Properties
titleField: name
permalink: /stay/{{ entry.slug }}
fields:
  - { name: name, label: Name, type: text, required: true }
  - { name: city, label: City, type: reference, to: city, filterable: true }
  - { name: stars, label: Star rating, type: number, min: 1, max: 5, filterable: true }
  - name: score
    label: Guest score
    type: aggregate
    of: review
    on: property
    field: score
    fn: avg

Every field takes name, label, and optionally required, unique, filterable, help. filterable is what makes a field searchable — it compiles to a partial index scoped to your site and type. unique is enforced by an index of its own.

Field types

TypeAlso takes
text One line of plain text. max
richtext Formatted prose, edited in the admin.
number A number, sortable and filterable. min, max
boolean Yes or no. An unticked box means no, not missing.
date A day, with no time on it.
datetime A moment, normalised to UTC on write.
email An address, checked for shape.
phone A number people ring.
url A link somewhere else.
asset An uploaded file. accept: image | video | document | any
geo One coordinate. Pasted from a map — nothing here geocodes.
select One of a closed list. options: [{ value, label }]
reference A relation to another content type. Integrity is declared, not implied. to, many
state Where a row is in a process, and which moves are allowed. values, initial, transitions
hours Opening hours as a week, not a document.

Worked out by the platform

Never written by a form, an API caller or an agent — accepting one would let a caller claim their own rating, or their own price.

TypeAlso takes
aggregate A number counted or averaged over rows of another type that reference this one. of, on, field, fn: count | avg | sum | min | max
computed A number this type works out for itself, from a declared formula. formula, precision
## A page
key: search
path: /search
title: Search results
blocks:
  - type: list
    data:
      from: property
      where:
        - { field: stars, op: gte, value: { param: stars } }
      sort: { param: sort, allow: [score, stars], default: score, dir: desc }
      limit: 25
    item:
      - type: card
        attrs: { heading: "{{ item.name }}", to: "/stay/{{ item.slug }}" }

A page with collection: { from: property } answers for one row at a time, and its lists can name that row with { entry: slug } — "the rooms of this property".

Blocks

Blockattrsstyle.variant
account Sign in, register, or sign out — for the site's own visitors. mode, submit
button A link styled as a button. label, to primary, secondary, outline
card A titled block of content, usually one row of a list. heading, body, meta, image, to plain, elevated, price
disclosure A label that opens to reveal what is inside it. label, open
divider A horizontal rule.
facets Filter options for one field, with how many rows each would match. for, field, param, title, order
field One input inside a form. name, label, type, required, placeholder
filters A search form for the parameters this page's list actually reads. for, submit
footer Page footer. text
form A form over a content type. Submits to the site when the type allows it. for, submitLabel
gallery Several pictures in a grid. images, alt
grid Children in a responsive grid.
heading A headline. text, level
image A picture. src, alt, width, height
list Renders one child template per row of a query.
map Where this is, on a map. at, zoom, title
nav Site navigation. links, label
pager Previous and next links for a paged list.
results-count How many rows the current filters matched. one, many
richtext Formatted text authored by the site owner. html
row Children in a horizontal row that wraps.
save-button Lets a signed-in visitor keep this entry. for, label, saved
section A full-width band of the page.
spacer Vertical space.
stack Children in a vertical column.
text A paragraph of plain text. text

disclosure is how a spec expresses a toggle — a mobile menu, an FAQ answer, a filter rail that starts collapsed. It renders <details>, so it is keyboard-operable, announces its own open state, and closes on Escape without a line of JavaScript. A header that wants a menu on a phone and a row of links on a desktop is two blocks gated by hideOn, with the links themselves in a components: entry so the list is written once.

Styling

Three tiers, and the order is the point: a theme first, then structured properties, then custom CSS as the pressure valve.

1. ThemeColours, fonts, a type scale, radius. One decision for the whole site.
2. stylepadding, gap, width, contentWidth, align, justify, background, textColor, radius, border, shadow, textAlign, fontSize, fontWeight, cols, hideOn, variant. Responsive where it makes sense.
3. cssDeclarations for the block, and one level of nested selectors for what it renders inside itself.

contentWidth measures a block's children and leaves the block alone — a full-bleed coloured band with contained content, which is the commonest layout on any site and which width cannot express, because constraining a section constrains its background too.

type: section
style: { background: brand, contentWidth: container }

type: card
style: { variant: price }
css: |
img { aspect-ratio: 1 }
h3 { font-size: 1.1rem }
&:hover { opacity: .95 }

A block author never writes a selector. Declarations apply to the block; a nested rule is prefixed with the block's own class, and & means the block itself. One level deep, and anything that is not a declaration is dropped — so custom CSS cannot reach an element the block does not own, whatever is typed into it. Site-wide CSS is the one explicit escape hatch, and it is gated to the developer role.

## Queries

data is the only iteration in the spec. There is no for: data plus item is the whole story.

  • limit is required and capped at 100. An unbounded query is a performance problem you cannot see until your business is doing well.
  • An absent parameter drops its condition, because a search page has to work before anything is typed. An unresolvable { entry: … } returns nothing instead — "this property's rooms" with no property is zero rooms, never every room on the site.
  • Facets count with their own filter lifted, so ticking "4 stars" does not make every other rating read zero.

Conditions

A condition is a triple, never a string. { field, op, value } is data — it diffs, it validates, and a model can propose one safely. item.price > 100 is code hiding in a value, and it is not accepted anywhere.

Operators: eq, ne, lt, lte, gt, gte, in, contains, exists, empty.

A value is a literal, a { param: name } from the request, or a { entry: field } from the row a detail page is for.

Formulas

Arithmetic is a declared tree, not an expression string, so you can read one and say what it depends on without running it. Six operations — add, subtract, multiply, divide, min, max, round — over a field, a request parameter, a literal, or one hop across a declared reference.

- name: deposit
  label: Deposit
  type: computed
  precision: 2
  formula:
    op: multiply
    of:
      - { ref: room, field: price }
      - { field: nights }

That hop is what lets a price come from the room rather than from the form — and a computed field is refused as input, so the amount a payment reads cannot be posted by whoever is paying it.

Where to go next. Build with it covers the CLI, the MCP server and the API. The architecture notes cover the database: what filterable compiles to, why dates are indexed as text, and where to look when a query is slow.