Space front pages

Embed
Preview of the card
<a href="https://forumulate.com/guides/space-front-pages"><img src="https://forumulate.com/guides/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/36aa4df0-1e95-4cef-8ae9-795169441345/pages/652e4d5d-fe20-4f20-b811-fde19561a891/social-card.svg?v=3e7ae33eec8b" alt="Space front pages" width="600" height="315"></a>

A space's front page is the address a reader lands on: /b/… for a blog, /f/… for a community, the root of a custom domain. It used to be a fixed arrangement per type — an intro on top, the type's listing under it, a few switches. It is now a block document you compose like any other page: headings, sections with backgrounds, columns, images, forms, and three live blocks that bring the space's own content in.

This page covers what is stored, how the studio edits it, the three live blocks, and the API. The block attributes in full are in the authoring reference.

Nothing changes until you change it

A space with no stored front page renders a document generated from its current preset and front-door settings. Its blocks are the space's own bands, in the order the reader always drew them, so every existing space looks exactly as it did the day before front pages existed.

That generated document is also what you start from. Open it, move a band, add a heading above the feed, and save — only then is a front page stored. Reset to default layout deletes the stored document, and the space goes back to the generated one (which follows the preset and settings you have now, not the ones you had when you first edited).

Type

The generated front page, top to bottom

BLOG

intro, then search and the posts feed (in your chosen order), then subscribe and pagination

FORUM

title, intro, search, identity, the category tiles, filters, category, the topics feed

CHANGELOG

intro, the releases feed, archive, subscribe

ROADMAP

intro, then submit and lanes (in your chosen order)

STATUS

intro, board, subscribe

FAQ

intro, questions

CAREERS

intro, roles

CONTACT

intro, form

COURSES

intro, outline

EVENTS

intro, events

DOCS

none — a docs space opens its first page. The Landing preset generates intro, search, sections; a list-index shape intro, list

LEGAL

intro, documents; with the No front door preset, none — each document stands alone

CUSTOM

as DOCS

site

search, sections (the site's spaces, or its tabs)

"None" means the type has no front door of its own in that configuration. A stored document still gives it one: compose a front page for a docs space and readers land on it instead of being sent to the first page.

The listing in the generated document is a spaceFeed with controls on, and its layout follows your preset — a blog's Grid and Magazine presets are the grid and magazine layouts, a community's Dense preset is rows, a changelog's Releases preset is rows. Picking a preset and picking a feed layout are the same choice.

Editing in the studio

On the space root in the studio, Edit front page opens the block editor on the front page. Everything the page editor has is here — type / for a block, or open Blocks and drag one onto the page — plus three more under Front page: Feed, Categories and Band. A feed's category filter and a category rail's parent list the chosen community's categories by name.

  • Layout presets insert a generated document for one of the ready-made arrangements. It replaces the document in the editor; nothing is stored until you save.

  • Sidebar adds a rail beside the page — left or right, narrow, default or wide — that takes the same blocks as the page.

  • Page look & sharing in the details rail is the page's own CSS, theme and share card (see below).

  • Reset to default layout removes the stored front page.

The canvas renders feeds live, from the same endpoints the reader uses.

The live blocks

spaceFeed — a feed

Live content of a space: this one (space: "self", the default) or another space in the same organization. The document stores only the question — which space, which filter, which sort, how many, laid out how — and the reader asks it on every render, through the same public endpoints the space's own list pages use. A feed can never show a reader anything that space's list page would not.

Fourteen layouts:

Layout

What it draws

feed

One column of full cards — a blog's single column. The default

grid

Cover-led cells, three per row on a wide screen

magazine

One opener drawn large — your featured post if the feed holds one, else the newest — then a grid

rows

A divided list, one line each — a community's stat rows

compact

Title and date, nothing else — a sidebar's "Latest"

tiles

Square tiles with the picture behind the title

cards-mixed

A bento: the opener spans two by two, every fifth card runs wide

split

One item per row, the picture beside the text — editorial rows, or a screen-tall hero

bento

Rows of N from pattern ([1,2,3]: one hero, two halves, then thirds; the last number repeats)

carousel

A scroll-snap row with arrows and a "1 of 4" count; perView items at a time

timeline

A grid on a line: each item a dot on one rule across the row (timeline styles the line and dots)

table

A ruled table, one row per item, its columns the feed's fields (a careers board, a changelog)

accordion

One disclosure per item, the title its summary; the only one on a page is the page's FAQPage

lanes

ROADMAP only: the board's status lanes as columns, read across space and the boards in spaces

A documentation space's feed lists its pages. excerpt is the page's metadata.subtitle, else its seoDescription, else the first 200 characters of its body. date and updated ("Updated Mar 4, 2026") are when it last changed, tags its metadata tags, and the card kicker category is its collection, linked to the collection's page.

split, bento and carousel draw the composed card; the first seven draw the type's own card unless you give the feed a card.

Columns are per breakpoint (columns.sm phone, .md tablet, .lg desktop, 1–6 each). Each type answers the sorts and filters its public list honours:

Type

Sorts

Filters

BLOG

latest, top, oldest

tags, author, pinned, featured, section

FORUM

latest, top, hot, oldest

category, tags, pinned, featured, status, kind, bountied

CHANGELOG

latest, oldest

tags, version, pinned, featured

ROADMAP

latest, top

lane, tags

EVENTS

latest, oldest

when (upcoming or past), tags

COURSES

latest

section

CAREERS

latest

section (a department)

FAQ

latest

section

DOCS, CUSTOM

latest

section (a collection)

STATUS, CONTACT, LEGAL

—

no list to feed from; a feed of one renders nothing

A filter the target type does not honour is ignored rather than refused, so a feed you point at a space of another type keeps working with what applies.

  • pinned and featured take true (only marked items) or false (only items that are not marked) — "pinned": false under a pinned hero keeps the hero out of the feed below it. Absent means both.

  • Several tags combine by tagMatch: "any" (the default) shows an item carrying at least one of them, "all" only an item carrying every one.

  • A forum feed also takes status (solved or unanswered — questions only), kind (question or discussion) and bountied (true: only topics with an open bounty; false: only those without). On a forum category page, "category": "$current" means the category being read; on any other page that filter is dropped.

A forum feed's fields and slots

Besides title, author, replies, views, date, category, tags and excerpt, a FORUM feed's fields (and a composed card's slots) take:

Field / slot

What it draws

posters

The thread's faces, overlapping: the opener, the latest replier, the most frequent

lastPoster

The last poster's face and name, and how long ago ("Ana · 3h")

activity

When the thread last moved (its last post), relative

likes

Every reaction on the thread, summed

askedIn

"Asked in {category}" on a question, "Posted in {category}" otherwise

replyPreviews

The last two replies, one line each

votes, answers

The stats slot: votes, answers (outlined once one is accepted) and views

avatar

On rows: the opener's face before the title

card.statIcons: true draws replies, views and likes as a glyph and a number instead of words. card.kickerStyle.swatch: true puts a small square in the category's colour before a category kicker, and card.image.source: "authorAvatar" makes the author's face the card's picture. A forum feed's rows draw the forum list's own row (the same tags, counts, faces and excerpt as the forum, in its theme.forum.list), not the generic row. Before tagMatch existed several tags meant all; set "tagMatch": "all" to keep that.

Every object in a front-page block is strict: an unknown key (columns.desktop, a misspelt filter), a band the space does not have, or a card field its cards cannot show is a 422 naming the path and listing what it accepts. GET /docs/block-schema publishes the keys of filter, columns and moreLink as properties; the block schema reference lists every spaceFeed attribute path with its type and accepted values. top and hot are ranked over period (day, week, month, year, all).

Without limit a feed shows six items; limit is 1–50. With paginate it pages instead, ten to a page unless pageSize says otherwise. paginate: "more" is one Show more button that appends the next page in place (and is a plain link to it with no script). moreButton styles that button: {style, arrow, label, color} — style solid (filled in color, the ink by default), outline (today's look) or link; arrow: true puts a down arrow after the words; label replaces "Show more".

Designing the card

A feed with a card draws every item with the composed card. Everything in it is optional; what you leave out keeps today's look.

{
  "type": "spaceFeed",
  "attrs": {
    "layout": "split",
    "card": {
      "slots": ["kicker", "title", "standfirst", "byline", "date", "readMore", "cover"],
      "image": { "position": "end", "span": "1/3", "aspect": "1:1" },
      "kicker": "firstTag",
      "tagStyle": "outline-mono",
      "dateStyle": "mono-caps",
      "metaLayout": "split",
      "byline": { "avatars": true, "role": true },
      "readMore": { "label": "Read the story" },
      "titleSize": "lg",
      "titleWeight": 600,
      "excerptLines": 3,
      "surface": {
        "background": "#ffffff",
        "backgroundDark": "#101113",
        "border": "none",
        "radius": 18,
        "padding": "24px",
        "shadow": "sm",
        "hover": "lift"
      }
    }
  }
}

Key

Takes

slots

The parts, in order: kicker, cover, date, title, subtitle, standfirst, byline, tags, readTime, replies, readMore, index. Unset: today's order, the feed's fields deciding what shows. Neighbouring byline/date/readTime/replies share one line

image

position: top (default), start, end, alternate (sides swap row by row), overlay (text on the picture), none. span: 1/3, 1/2, 2/3 of the row beside the text. aspect: 1:1, 4:3, 3:2, 16:9, 21:9, 2:1, original. size: a fixed thumbnail width (it stays beside the text on a phone). radius. positionSm: where the picture goes on a phone — top (above, the default), bottom (after the words) or none

image.with

Slots drawn in the picture's column, above it: any of date, byline, kicker; withRule: "start" puts a hairline above them

image.overlay…

overlayAlign: where an overlay card's words sit — end (the foot, the default), center, start; overlayShade: how dark the shade is, 0–1 (0.7)

image.bleed

true: the picture runs to the card's edges and the surface padding wraps only the words

gap

The gutter between the words and a picture beside them

kicker

The words above the title: firstTag, series, category, none

kickerStyle

{case, font, size, color, weight, tracking, rule} — case none or upper (the default small caps), font mono, body or heading, tracking in em, rule: "start" a short rule before the words

tagStyle

chip, outline, outline-mono, text, eyebrow

dateStyle

default, mono-caps, eyebrow. dateFormat spells it: short, medium (the default, "Oct 2, 2026"), long, full, day-month ("2 October" / "October 2", the reader's locale orders it), day-month-short, month-year, iso ("2026-10-02")

metaLayout

inline, or split — the byline left, date and read time right

metaLine

Slots that join one meta row, where the first of them falls: any of date, kicker, tags, byline, readTime, replies — "Product · Oct 3" on one line

byline

{avatars, role, stack, initials, names, color} — pictures, roles, and stack: co-authors' faces overlapping with one line of names ("Ann, Bo and 2 others"; the role shows for a single author only). An author without a picture is an initials disc unless initials: false. names: false: faces only — each portrait is its author's link, the name kept for a screen reader (an author with neither picture nor initials disc keeps the name). color: the names' ink

readMore

{label, weight, color, arrow} — a real link to the post; arrow: false drops the › (a label already ending in an arrow gets none)

titleSize

sm, md, lg, xl, or a length — or one per breakpoint, {sm, md, lg}; titleWeight any 100–900; titleFont heading, body, mono or a family name

subtitleColor

The subtitle's colour

excerptLines

Lines of standfirst before it is clamped, 0 (never) – 6; excerptSize a length or one per breakpoint

align

spread: what follows the title sits at the card's foot (a standfirst pinned to the bottom); meta-end: only the first meta row after the title sits at the foot — the standfirst stays under the title

verticalAlign

start, center, end — the words in a card taller than they are (a hero). Honoured on a card with a surface at every width (before October 2026 a surfaced card ignored it above 640px)

divider

below: a hairline under each card

index

true: a numeral, 1…n across pages, before the words — a numbered "most read"

surface

The card as a box: background, backgroundDark, border (a colour or none), radius, padding (one to four lengths, "20px 24px"), minHeight (a length or one per breakpoint), shadow (none–lg), hover (none, lift, border, underline)

meta

The meta rows' type: {size, weight, color, tracking} — size a length or one per breakpoint (unset 13px), tracking in em. dateColor inks the date alone. For every meta row on the site use the theme's components.meta (tracking, font, case); the card's keys beat it

metaJoin

false: every meta slot is its own row (the byline over the date). Unset: neighbours share a line

byline (round 4)

Also prefix (words before the names, ≤20 chars: "By"), weight, size (the names' type), avatarSize (12–96px; unset 28, 24 in a stack), layout: "stacked" (name over role, one author per row). The card's color/weight beat the theme's components.byline.link

excerptColor

The standfirst's ink (unset: muted); excerptWeight 100–900; subtitleWeight 100–900

slotGap

The space between the card's parts (title, standfirst, meta…), a length or one per breakpoint; unset 6px. Not gap, which is the gutter between the words and a picture beside them

titleLineHeight

0.8–2 (unset 1.25), and titleTracking −0.1–0.5em (unset −0.01em), each or one per breakpoint. lead.titleLineHeight/lead.titleTracking beat them on the opener

titleStyle

run-in: the title as a bold lead-in inside the standfirst paragraph (a "quick post"); alone if the item has no standfirst

tagSize

sm, md, lg (12/14/16px type), or {size, padding, borderWidth (0–4), height}; unset, today's 12px pill

image (round 4)

aspect, size take one per breakpoint (size: {sm: 96} is a thumb on a phone only). An aspect holds beside the text (verticalAlign places a picture shorter than the card) and sets an overlay card's shape (else surface.minHeight, 22rem). positionSm also takes start/end (a thumb beside the words on a phone, size.sm wide, 96px unset) — an explicit positionSm beats a thumb's phone default. shadow (sm–lg; a flush picture in a surface clips it). startAfter: "title": the parts up to the title run across the top, picture and rest under them. withAlign: "end", withSpan (1/4, 1/3, 1/2, 2/3, 3/4) place what rides with the picture. image.bleed: side pictures run flush; bleed: false insets them; a top picture is inset unless bleed: true. An explicit image.radius now holds in a surface

surface.radius / .padding, kickerStyle.size

Each also one per breakpoint, {sm, md, lg}

divider: "row"

One rule between the rows of a grid, across the gutter, none after the last row

dateFormat: "relative"

"3 days ago" under 30 days, the medium date after. dateLocale (en-GB, a BCP-47 tag) spells dates in that locale rather than the reader's; dateIcon: "clock" draws a clock before the date

indexStyle

The numeral: {placement, color, font, size, weight, suffix} — placement: "inline" puts it inside the title ("1. The title"), suffix ≤4 chars after it

slotsSm

A phone's order of the parts; a part it leaves out is hidden on a phone, and the picture takes cover's place. Byline and date on separate lines need metaJoin: false

readMore (round 4)

Also style: link (default), button (outlined), button-solid; align: start, center, end. The button reads the theme's components.button radius/border/weight

when

Per-tag variants: [{tag, card}], up to 4, first match wins (by tag slug or label). Each card is a PATCH over the feed's card (objects key by key, lists whole) and cannot hold its own when

Colours are a token (bg, surface, panel, ink, muted, brand, accent) or a #hex; lengths are a number of pixels or a string in px, rem, em, vh, vw or %, a bare 0, or clamp(), min() or max() of those ("clamp(1.5rem, 3vw, 2.5rem)"). A value per breakpoint is {sm, md, lg} — phone (≤640px), tablet (≤1023px), desktop — each falling back to the nearest one you set: "titleSize": {"sm": "22px", "lg": "xl"}. A font is heading, body, mono or a family name. An unknown key anywhere in card is a 422 naming its path. A picture the card asks for and the item does not have is no picture — never an empty box.

Round-5 card keys (2026-10-11)

Every key is optional; unset draws what it drew before.

Key

What it does

titleColor

The title's ink (a run-in title's too). Unset: the surface's ink, else the heading ink. Takes when like any key

titleMaxWidth

The title's widest line, a length ("18ch")

surface.ink

The card's text colour; the title takes it and the muted parts (meta, standfirst, what rides with the picture) take it softened

image.withPlacement

head (with startAfter: "title"): what rides with the picture (image.with) sits beside the title row, withSpan wide (unset 25%), rather than above the picture. One column on a phone

image.bleedSm

gutter: on a phone the picture (or a surfaced card) runs out to 16px from the screen's edge, past the page's wider gutter

byline.roleSize

The author role's type size (unset 0.85em)

byline.roleSeparator

Between a name and its role on one line, ≤8 chars (unset ", "; "" leaves only a gap)

metaSeparator

Between the joined meta parts, ≤8 chars (unset ·; "" leaves only the row's gap)

runInSeparator

After a run-in title, before the standfirst, ≤4 chars (". "); a space always follows the title

tagColor

The tag pills' ink, and an outline pill's border unless tagSize.borderColor says otherwise

tagSize.borderColor / .radius

The pill box's border colour and corner

tagLimit

1–12: at most this many tags, the rest behind a "•••" chip (the post head's)

slotGapAfter

EXTRA room after named parts, on top of slotGap: {"title": 12, "byline": "8px"}, up to 8. A meta row takes the one named for its last part

slotGapBefore (round 6)

The room BEFORE named parts, in place of slotGap, only when that part renders: {"subtitle": 2} hugs a subtitle to its title while a card with no subtitle keeps slotGap from title to excerpt. A meta row takes the one named for its first part

image.bleedSm: "media" (round 6)

On a phone only the picture runs out to 16px from the screen's edge — through a surface card's padding too; the card and its words keep the page gutter

image.withSpan / image.span (round 6)

Also take 1/4 and 3/4

byline.lineHeight (round 6)

The names' and roles' line height, a multiple 0.8–3 (unset: 1.25 for a stacked name over role)

Fixed in the same round: withSpan/withAlign no longer push what rides with the picture to the end on a phone; slotsSm now orders those parts too; a carousel card's readMore sits at the card's foot; withRule: "bar" marks the first row only; an overlay card with an aspect and a surface.minHeight no longer grows wider than its track; divider: "row" stops at the grid's outer edges; layoutSm: "rows" with a slotsSm that drops the picture no longer keeps an empty picture column.

Mixed cards in one feed

card.when draws the items carrying a tag differently, without a second feed:

{ "card": { "image": { "position": "top" },
  "when": [
    { "tag": "video", "card": { "image": { "position": "overlay", "aspect": "4:3" } } },
    { "tag": "quick", "card": { "slots": ["byline", "date", "standfirst"], "titleStyle": "run-in" } }
  ] } }

The matching card wears data-card-when="<n>" for a theme to aim at.

An overlay card's words sit at the foot of the picture. Before October 2026 they sat at the top of the card; set image.overlayAlign: "start" to keep that look.

Tag labels and colours, several bylines with roles and avatars, and a subtitle come from the post when it has them (the tag registry and bylines); without them the card shows the tag as written and the author's name.

Arrangement

Key

What it does

lead

The opener of a magazine, bento or cards-mixed: {style, titleSize, titleFont, span, divider, coverSpan}, style stack, split (picture 2/3 beside a panel), overlap (a headline card over the picture), overlay, left-column, stack-split (the opener stacked in span columns, the rest listed beside it). divider: false drops the rule under a magazine's opener. coverSpan: "all" (with stack-split): the opener's picture runs across the whole row, then its words and each other story take a column — a magazine hero

rest

Everything after the lead: cards, or text-only (no pictures)

pattern

A bento's items per row, 1–6 each, the last repeating

perView

A carousel's items in view: {sm, md, lg}

layoutSm

The arrangement on a phone, when it differs: rows (text with a small picture), compact, grid/tiles (columns.sm across), anything else one column

columns.offsets

A staggered grid: pixels each desktop column is pushed down, e.g. [90, 180, 0]

heading

Words, or {text, subtitle, style, ruleColor, ruleWidth, underline, size, color, background}: style plain, rule-top (a rule above, in ruleColor/ruleWidth) or pill (a filled label in color on background); underline a rule under the head; size a length or one per breakpoint (default: the theme's h2 size); subtitle words, or {text, inline: true} beside the heading. {tag} in the words is the tag the feed reads — on a post, its first tag ("More in {tag}"). font (heading, body, mono or a family) and case (none, upper, lower, capitalize) set the head's face and case in any style — a mono capitals pill

moreLink

{label, href, position, style} — position top (beside the heading, the default) or below, or one per screen ({"lg": "top", "sm": "below"}); style link or button

tabs

Tag tabs over this feed that filter it in place (?fp<slot>t=<tag>): {tags, limit, allLabel, style, activeColor, activeTextColor, overflow, inactive, phone} (the last three as on the tags band); without tags, the most-used

spaceBefore

The room above this feed (round 5) — the section's gap is one value for every block; a length or {sm, md, lg}

spaceAfter

The room under this feed before what follows it — stacked feeds in one section (a length, or {sm, md, lg} since round 5)

interstitials

Notes between the rows: [{after, text, size, align, weight, color, font, case}] — after the after-th item, a full-width line of words (never after the last); weight, color, font and case give a note its own type (unset: muted body text)

Round-4 keys: the frame (2026-10-11)

Every key below is optional, and unset draws exactly what it drew before.

Key

What it does

gap

The column gap between a feed's cards — and the row gap too, unless rowGap says otherwise. A length or {sm, md, lg}. Works on every layout: grids, rows, split, the magazine's rest, bento rows, stack-split and the carousel's slides. A screen you leave out keeps the layout's own gap

rowGap

The gap between rows, a length or {sm, md, lg}

columnRule

{style, color, width} — a rule in the middle of the column gap, beside every card that does not start a row; style is solid, dashed or dotted

rest

Also {sm, lg}: {sm: "cards", lg: "text-only"} keeps pictures on a phone and drops them from the tablet up (the cards are drawn once; the screen hides the picture)

lead.minHeight

The opener's own min height, a length or {sm, md, lg} — beats card.surface.minHeight for it

lead.maxWidth

The opener's words column at most this wide

heading.weight / lineHeight

The head's weight (100–900) and line height (0.8–3). Unset, the head follows the theme's typography.levels.h2 — its size, weight, tracking and line height — then 600 / -0.025em / 1.2em

heading.align

start (today) or center — "See all" goes under the words

heading.spaceBelow

The room under the head, a length or {sm, md, lg} (unset 1.25rem)

carousel

{indicator, controls, arrows, bleed}: indicator count (today), dots or none; controls below-end (today), below-split or top-split (arrows at both ends, above the track); arrows {background, border, color, size}; bleed: "end" runs the track past the page's trailing edge so the next card peeks; bleed: "both" past both edges (a phone row to the screen's sides). A bento with layoutSm: "carousel" and a carousel look is drawn as this carousel on a phone — dots, arrows, bleed — rather than the bare scroll row

perView

Now in twentieths: {lg: 3.25} shows three cards and a quarter

moreLink / moreButton look

background, textColor, border, radius, size, weight, width, height, arrowPosition (end, start) and arrowBox (the arrow in a square of its own). Flat beside the existing keys. A category rail's moreLink takes the same look

interstitials[].overlay

The note floats over the row after it instead of taking a row: offsetY down from that row's top, width its widest

tabs look

size, weight, radius, inactiveColor, inactiveWeight, activeFill (fills the active tab in any style), inactivePadding (tight or pill), phoneFill, font, case, trackColor; style segmented (one filled track, the active tab raised)

tabs.lanes

Up to six tabs that SORT or FILTER rather than pick a tag: [{label, sort, filter}], the first the default; the active one is ?fp<slot>t=<index>. Exclusive with tabs.tags. Lanes sort and filter only — a "Following" lane would need a reader identity a feed does not have

The cover of lead.coverSpan: "all" takes card.image.aspect and card.image.radius (it used to stay 21/9). Tab labels come from the tag registry whether or not you list tabs.tags (they used to show the slug).

In markdown, gap, rowGap and rest are plain attributes (a number is px; an object is JSON), columnRule and carousel are JSON, and the moreLink look rides as moreLook="{…}".

Key

What it does

lead.card

A card patch for the opener alone, merged over the feed's card (and any when it matched) as a when is: {"lead": {"style": "overlay", "card": {"titleColor": "#fff"}}}

tabs.spaceBelow

The room under the feed's tabs, a length or {sm, md, lg}

filter.excludeTags

Up to 10 tags (slug or label): items carrying any of them are left out. Lanes inherit it

carousel.controls

Also none (no arrows; the indicator stays), top-end (above the track, the count first and both arrows together at the end) and dots-start (the dots at the row's start, the arrows grouped at its end)

layout: "timeline"

A grid whose items are dots on one line across the row (on a phone the line runs down the start). Columns default {sm: 1, md: 2, lg: 4}. timeline: {color, dot} — the line's colour (unset the signal colour) and the dot's size (unset 9px)

layoutSm: "carousel"

Now honoured on a bento (the rows stacked before); perView.sm sets the cards in view on the phone (unset 85% a card)

interstitials[].overlay / offsetY

Each also {sm, md, lg}: {"overlay": {"lg": true, "sm": false}} floats the note on a desktop and gives it a row of its own on a phone. An overlay note is drawn inside the first card of the row after it, so it no longer costs a row gap

Round-6 keys (2026-10-11)

Key

What it does

heading.underlineColor / underlineWidth

The underline rule's own colour and width — a 4px rule above (rule-top) and a 1px grey one under. Unset: rule-top's, as before

timeline.dotStyle

fill (unset) or ring: each dot hollow, a ring in the line's colour on the page

timeline.colorEnd

The line as a gradient from color to this, along each item's run (down the start on a phone)

carousel.arrows.radius

The arrows' corner — 999px for circles. Unset: the theme's button radius

carousel.count

{size, color} — the "1 of 4" count's size and ink (unset 13px, muted)

card.tagMore

tag: the "•••" chip drawn like the tags (unfilled, their border and ink), round. Unset fill

tabs.phoneHoist

true: on a phone the feed's tabs go to the top of the page, above the main column. For a feed placed directly in the front page's sidebar; they still switch their own feed

tabs.phoneHeight

The phone dropdown face's exact height (also on the tags band)

Round-7 keys (2026-10-11)

Key

What it does

card.image.withInset

A length: pads the whole with column (every row starts at it), and a withRule: "bar" hangs at the column's start. Unset: the bar's 12px on the first row only

card.kickerStyle.ruleGap

A length: the gap between the kicker's rule and its words. Unset: 12px (bar), 8px (start)

card.image.inset

A length or {sm, md, lg}: the picture's own inset inside a surface card, apart from surface.padding (a top picture, or a side one with bleed: false). Unset: the padding

spaceBand tags: searchWidth

A length: the search field's width on a desktop (a phone glyph style keeps its glyph on a phone). Unset: the field's own

spaceBand tags: searchIconSize

A length or {sm, md, lg}: the search glyph's size in the compact/icon styles. Unset: 14px

chrome.footer.newsletter.headingWidth

px (80–1200): the newsletter heading block's measure — the heading and subheading wrap at it. Unset: the column's

chrome.footer.art.glowWidth / glowAlign

The glowBelow wash's width (px, or a share like "35%"; unset the whole band) and where it sits: left, center, right, art (the art's own position) or its centre as a % of the band (0–100)

theme.components.list.hang

Also {"scope": "front"}: hang the markers on the front page's lists only, so a post's body keeps its indent. {"scope": "all"} is true

card.subtitleSize / card.subtitleLineHeight

The card's subtitle (dek) size, a length or {sm, md, lg}, and its line height (0.8–3, or per breakpoint). Unset: 1rem, inherited line

card.readMore.size

A length: the "Read more" link's type size. Unset: 14px

card.gap

Now also {sm, md, lg}. Its phone step is the room above a picture image.positionSm: "bottom" stacks under the words. Unset there (or a single length): 14px

heading / list spaceBefore / spaceAfter

A length or {sm, md, lg}: the room above/below that block, in place of its margin (and of a section's gap above it). Markdown: {spaceBefore=lg:60,sm:40}

list size

A list's own type size, as a heading's or paragraph's size (px or a length, or per breakpoint). Markdown: {size=19}

Round-3 forum keys (2026-10-12)

On the card (spaceFeed.card, and a when variant's card):

Key

What it does

stats

{position: "end", fields, heat} — a column of counts at the card's end, stacked: fields up to three of replies, views, likes, activity (a compact age), unset replies then activity. heat: true tints a count past the forum's own theme.forum.list.heat thresholds.

surface.topStrip

{fill, gradient, height} — a bar across the card's top edge: fill gradient (between gradient's two colours), categoryColor (the topic's category, else the brand) or a colour; height 2–12 px, unset 4.

statusGlyph

true: a forum question's state before its title — "?" in a ring while open, a filled check once solved.

On the feed:

Key

What it does

frame

{border, radius, rules} — the rows in one panel: border a hairline round the feed, radius its corner (0–24 px), rules a hairline between rows (a card's own divider: "below" steps aside).

{ "type": "spaceFeed", "attrs": {
  "layout": "feed", "rowGap": 0,
  "frame": { "border": true, "radius": 12, "rules": true },
  "card": {
    "slots": ["byline", "askedIn", "title", "standfirst"],
    "statusGlyph": true,
    "stats": { "position": "end", "fields": ["replies", "activity"], "heat": true },
    "surface": { "border": "none", "padding": "16px 20px", "topStrip": { "fill": "categoryColor", "height": 3 } }
  }
} }

A surfaced card with divider: "below" no longer adds the divider's own 1.25rem under its padding — that doubled space was the ~40px gap under every activity row's stats.

Round-8 keys (2026-10-11)

Key

What it does

tabs.phoneStretch

true: on a phone (≤ 640px) the feed tabs' track spans the column and each tab takes an equal share. Unset: the track hugs its tabs

timeline.endDot

true: a dot where the timeline's line ends — at the end of each row on a desktop or tablet, at the foot of the last item on a phone. The dot's size and style (dot, dotStyle), in colorEnd when set. Unset: no end dot

card.divider

Now also {sm, md, lg}, e.g. {"sm": "below", "lg": "none"}: a rule under each card on a phone only. An unset step after filling (md from lg, sm from md) draws none

The front page's sidebar.phone also takes {"at": N} (1–20): on a phone the rail sits after the main column's Nth top-level block — the lead first, then the rail, then the rest — with the page's own gap between them. With tabs.phoneHoist the sidebar feed's tabs go first: tabs, lead, rail, rest.

Round-4 package D keys (2026-10-12)

These are all block attributes. A block's value wins over the space theme and over the look.

Key

Values

Unset

What it does

spaceFeed.width

column, wide, full

column on a documentation front page; the old width everywhere else

On a docs front page a feed stays in the same 768px column as the blocks around it. wide caps it at 64rem; full removes the cap

spaceFeed.listPatch

any theme.forum.list keys

the space's list

That feed's forum rows wear these list keys, for example anatomy, stats.show or phone. On a category page, a feed with filter.category: "$current" first takes the category's categoryOverrides.<slug>.list and then this

spaceFeed.card.stats.icons

boolean

follows card.statIcons

The glyph before the numbers in the card.stats end column. card.statIcons: false now also removes the speech bubble before the reply count

spaceFeed.card.replyPreviews.avatar

boolean

off

An 18px face before each reply preview. A preview with no picture shows the author's initials

card field / slot updated

—

—

Documentation (and custom) feeds accept it. It reads "Updated 3 days ago" from the page's last change, and becomes a date once the change is more than four weeks old

spaceLeaderboard.fields adds handle

—

—

"@username" in place of the display name, for rows that have a username

spaceLeaderboard.rank

true, false, start, end

off

end puts the rank number at the row's end. true is start

spaceCategories.topTopics with layout: "rows"

{sm, md, lg} counts

none

The rows layout draws each category's latest topics, up to 6 per category. It works with any fields, and also on phones, where "12 total · 3 / week" sits under the topics

{ "type": "spaceFeed", "attrs": { "space": "self", "layout": "rows", "width": "wide", "listPatch": { "anatomy": "cards", "stats": { "show": "none" }, "phone": { "avatar": false } }, "card": { "statIcons": false, "stats": { "icons": false }, "replyPreviews": { "avatar": true } } } }
{ "type": "spaceLeaderboard", "attrs": { "period": "month", "fields": ["avatar", "handle", "points"], "rank": "end" } }

In markdown, both feed attributes take JSON or a word: listPatch='{"anatomy":"cards"}' and width="wide". The leaderboard takes rank="end".

A front page whose first block is a full-bleed section, and which sets no paddingTop, now starts flush under the header, so the section meets it.

Round-5 package C keys (2026-10-12)

All of these are block attributes (scope: block). None is set by a default layer; unset draws what the block drew before, except the two rows marked changed.

Key

Values

Unset

What it does

card slot updated (changed)

—

—

"Updated 2h ago" now sits on the card's meta row, in the date's muted 13px type (and card.dateColor), instead of a body-size line of its own

spaceFeed.headGap (changed for width: "column")

none 0, xs 8, sm 16, md 24, lg 32 (px), or a length (20, 1.5rem)

24px on a width: "column" feed; the page's own gap otherwise

The room between whatever stands above the feed (a lede paragraph) and the feed's "View all" row or first card. A spaceBefore on the same feed wins

spaceFeed.card.image.focus

start, center, end

center

Which part of a cropped picture stays in its box (start = top-left, the CSS object-position: 0% 0%). Also on the hero cover

spaceFeed.card.image.fit

cover, contain

cover

contain shows the whole picture inside its box instead of filling it

spaceCategories.tile.nameSize

12–28 (px), or a type level (h3, body, …)

text-sm

A tile's (and grid/list card's) category name size. A level follows typography.levels

spaceCategories.tile.nameWeight

400–800

600

The name's weight

spaceCategories.topTopicsRow

{ink: "muted"|"ink", size: 12–18, meta: ["age", "replies"]}

bare muted titles

Each top topic under a tile or a rows category as "Title · 15h … 3": the title clamps to its line, age follows it, replies sits at the row's end. A topic the API sends without a time or count draws without it

spaceLeaderboard.fields adds name

—

—

The display name. With handle too, each row is "@handle" over the display name

spaceLeaderboard.handleStyle

replace, over-name

replace

over-name: "@handle" over the display name, the same as naming both fields

spaceLeaderboard.rankSize

10–48 (px)

12px

The rank number's size, for a board whose numbers are the design (markdown rankSize="28")

heading placement

flow, page-head

flow

On a community's composed front page, the top-level heading marked page-head becomes the page's H1 in place of the community's name — above the sidebar under theme.forum.pageHead.placement: "above-sidebar", at the top of the column otherwise. It is shown even where the layer hides the name (layout.indexHeader). Anywhere else the heading draws where it is. One per page, top level only (else 422)

heading link

{label, href}

none

A link beside the page-head heading ("Read the full recap"). Refused (422) on a heading that is not page-head

heading link.placement

inline, below

inline

below: the link on its own line under the heading instead of beside it (markdown linkAt=below after link=…)

{ "type": "heading", "attrs": { "level": 1, "placement": "page-head", "link": { "label": "Read the full recap", "href": "/f/acme/community/weekly-oct-5" } }, "content": [{ "type": "text", "text": "Weekly — Oct 5" }] }
{ "type": "spaceCategories", "attrs": { "layout": "tiles", "fields": ["logo", "topTopics"], "topTopics": 3, "tile": { "nameSize": 18, "nameWeight": 600 }, "topTopicsRow": { "ink": "ink", "size": 14, "meta": ["age", "replies"] } } }
{ "type": "spaceFeed", "attrs": { "layout": "grid", "width": "column", "headGap": "sm", "card": { "image": { "focus": "start", "fit": "cover" } } } }

In markdown: headGap="md" on :::feed, topTopicsRow='{"meta":["age","replies"]}' on :::categories, handleStyle="over-name" on :::leaderboard; the picture's focus/fit ride in the feed's card JSON and the tile's name in tile. The heading's placement and link are set through the API or the studio's JSON (the markdown heading line has no spelling for them yet).

Mix and match

A look is a set of defaults for individual keys. Every key works under every look, and a key you set wins over the look. Keys come in three scopes:

  • block: a block's own attributes, such as listPatch, card.*, width and layout.

  • category: theme.forum.categoryOverrides.<slug>.

  • theme: theme.forum.*.

The block wins over the category, the category over the theme, and the theme over the look. Neither page below is a replica of any other forum.

A community home under the qa look: one Q&A feed of cards for questions, a table of announcements, and logo tiles above both.

{
  "doc": {
    "type": "doc",
    "content": [
      { "type": "spaceCategories", "attrs": { "layout": "tiles", "columns": { "lg": 3 } } },
      { "type": "spaceFeed", "attrs": { "layout": "rows", "heading": "Questions", "filter": { "category": "help" }, "listPatch": { "anatomy": "cards", "stats": { "show": "both" } } } },
      { "type": "spaceFeed", "attrs": { "layout": "rows", "heading": "Announcements", "filter": { "category": "announcements" }, "listPatch": { "anatomy": "table", "stats": { "show": "none" } } } }
    ]
  }
}

A documentation hub whose feed stays in the reading column, with the update date on each card and a leaderboard ranked at the row's end beside it.

{
  "doc": {
    "type": "doc",
    "content": [
      { "type": "spaceFeed", "attrs": { "space": "self", "layout": "rows", "fields": ["title", "excerpt", "updated"], "width": "column" } },
      { "type": "spaceLeaderboard", "attrs": { "space": "f0f0f0f0-0000-4000-8000-000000000000", "fields": ["handle", "points"], "rank": "end" } }
    ]
  }
}

Round 5 adds four more building blocks: the page-head heading, the top-topic row, the tile's name type and the leaderboard's handle over the name. A release-notes community under the board look whose home is titled by the week's issue, with compact category tiles that each list their busiest topics with ages and reply counts, and a contributors list that shows who people are as well as their handles — no forum the showcases copy is built this way:

{
  "doc": {
    "type": "doc",
    "content": [
      { "type": "heading", "attrs": { "level": 1, "placement": "page-head", "link": { "label": "All issues", "href": "/f/acme/releases/c/issues" } }, "content": [{ "type": "text", "text": "Release week 41" }] },
      { "type": "spaceCategories", "attrs": { "layout": "grid", "fields": ["color", "topTopics"], "topTopics": { "sm": 2, "lg": 3 }, "tile": { "nameSize": "h4", "nameWeight": 700, "surface": "none" }, "topTopicsRow": { "ink": "muted", "size": 13, "meta": ["replies", "age"] } } },
      { "type": "spaceFeed", "attrs": { "controls": true, "layout": "rows", "card": { "image": { "focus": "end", "fit": "contain" } } } },
      { "type": "spaceLeaderboard", "attrs": { "fields": ["avatar", "handle", "name"], "handleStyle": "over-name", "rank": "start" } }
    ]
  }
}

spaceCategories — category tiles

A community's categories as tiles (tiles), cards (grid, the default) or a list: colour, picture, logo, description, topic count, and — with the topTopics field — a few of each category's top topics (3 unless topTopics says 0–5). parent narrows it to one category's subcategories. Pointed at a space that is not a community, it renders nothing.

Forum wave (2026-10-12). Three more layouts, and the tile's own look:

Key

What it does

layout: "nav"

A compact link tree — 32px rows, a 10px colour square, the count on the right, the category being viewed marked. nav: {marker: "swatch"|"icon"|"none", nested, collapsible, counts}: icon uses the category's icon or emoji; nested (default on) puts subcategories under their parent; collapsible folds them, open when the reader is inside; counts (default on). The forum sidebar draws the same tree.

layout: "rows"

Ruled rows with the category's colour as a 4px left edge: name, description, its subcategories as chips under it (never listed again), and on the right the topic count and "N / week". Fields: the usual ones plus perWeek and children (both on when you choose no fields).

layout: "table"

Each category beside its latest topics (Discourse's "Categories + latest"). table: {columns, latestLimit, titleSize, descSize, topicColor, headers} — columns from category, count, perWeek, latest (default category, per week, latest), latestLimit 1–6 (default 4), topicColor the topic titles' colour (default the page ink), headers (default on). Each latest topic has a solved mark and its age. Below 768px a row stacks. Until the API sends the weekly rate and several latest topics, it shows the count and the one latest topic.

tile

{logoSize: 16–128, align: "start"|"center", surface: "card"|"none", height: "square"|"auto"} on tiles/grid/list: the logo's size, the logo stacked over the centred name, no card, a tile that hugs its content.

topTopics

Also one per screen: {"sm": 1, "lg": 3}.

moreLink: false

No "Categories →" link.

parent: "$current"

The category being viewed on a community's category view; the top level everywhere else.

spaceLeaderboard — a forum's top people

{ "type": "spaceLeaderboard", "attrs": { "period": "week", "limit": 5, "fields": ["avatar", "likes", "solutions"], "rank": true } }

The community's own leaderboard as a block: space (self or a community's id), period (week default, month, all, points), limit 1–20 (default 5), heading, fields from avatar, level, likes, solutions, posts, points (default avatar, likes, solutions), rank (a number before each row) and moreLink (default "Leaderboard →" to the full page; false draws none). Each name links to the member's profile. Pointed at a space that is not a community, or one the reader cannot see, it renders nothing. In markdown: :::leaderboard{period="week" limit="5" rank}:::.

What each period ranks, and by what:

period

Who is on it, and in what order

week (default)

Members who posted, were liked or had an answer accepted in this community in the last 7 days — most posts first, then likes received, then solutions

month

The same over the last 30 days

all

All time — the hall of fame — ranked by likes received, then solutions, then posts

points

The organization's gamification points: everyone's balance, highest first. Points are org-wide by design, so this board is the same in every community of the organization

week, month and all count this community's posts, likes and accepted answers only. The row's numbers come from the same counts:

Field

What it shows

avatar

The member's picture (fetched through us), or an initials disc

name

The display name

handle

"@handle" in the name's place; with name too (or handleStyle: "over-name"), "@handle" over the name

level

The trust level's label: New, Basic, Member, Regular or Leader

likes

Likes received in the period

solutions

Answers accepted in the period

posts

Posts in the period

points

Points — filled only on period: "points"

Unset fields is avatar, likes, solutions on week, month and all. On period: "points" the board carries one number, the points, and no counts: unset fields is avatar and points there, and the counting fields (likes, solutions, posts, level) are left out even when you list them, so a points board never prints "0 likes · 0 solutions". The name is always drawn.

rank: true (or "start") puts the number first, "end" after the row; rankSize sets it, 10–48 px (unset 12). Badges are not a field of the board: they are earned on the member's profile (badges).

Where a block shows on a community: showOn

A community's category view (?category=) is drawn from the same front page. Every live block and every community band takes showOn: front (the front page only), category (category views only) or both (unset — what every block did before). {"type": "spaceCategories", "attrs": {"parent": "$current", "showOn": "category"}} lists a category's subcategories on its own view and nothing on the front page.

Round 3 (R3-087) adds two values for the front page itself: landing — the front page as a bare visit opens it (no ?sort, tab, filter or page in the address) — and sorted — the front page once one is chosen. A home can then be the categories table alone, and its Latest tab the topic list alone: spaceCategories{layout:"table", showOn:"landing"} and spaceFeed{controls:true, showOn:"sorted"}. Neither draws on a category page.

spaceBand — one of the type's own bands

{ "band": "search" } is the space's own search box, lanes a roadmap's lanes, board the status board, subscribe the subscribe form — rendered by the reader exactly as the fixed front door drew it. The band names each type offers are the ones in the table above, less the listing and the category tiles (which are the two blocks above). A band the space's type does not have renders nothing.

These bands take options, set beside band; an option on a band that does not take it is a 422 naming its path. Which band takes which depends on the type: a blog's tags is the one below, a changelog's tags and filters take the tab keys of its filter band, and a community's bands take the keys in the community table:

Band

Options

tags

A blog's tag pills or category tabs. tags (default: the limit most used, 8), style (pills, tabs, text, pills-filled — each pill in its tag's own colour and text colour from the tag registry), activeColor (the active tab's colour), activeTextColor (the active tab's text colour), inactive (pill, or text: the tags you are not on are plain words and only the active one is a pill), phone (row, or dropdown: below 641px the row folds into one dropdown showing the tag you are on, which lists every tab as a link and works without script), overflow (wrap, or scroll: one row that scrolls sideways), showAll (default on), allLabel, showSearch, searchStyle (compact: the icon inside the field, no button), rss (none, icon, link). Round 6: searchStylePhone (icon or icon-round: the search as a glyph on a phone whatever searchStyle draws on a desktop; pair it with compact or icon) and phoneHeight (the dropdown face's exact height). The active tab is the tag page the reader is on, or ?tag= on the index

subscribe

layout (stacked, inline-row), heading, body, buttonLabel, placeholder, inputStyle (box, underline), frame (card, none); round 6: inputFill (the field's fill) and fieldLabel (a visible label above the field)

pagination

style: links (newer / older, the default) or button (one centred "Older" button)

title

showArchive, showRss — false hides the blog's Archive link or its RSS link (the changelog's Subscribe link) where the band is placed. Unset or true follows the front door's own switches, which still decide whether there is a feed at all

Every band also takes spaceBefore and spaceAfter (round 6): a length or {sm, md, lg}, the room above or below that band in place of the page's gap there ({"band": "tags", "spaceBefore": {"sm": "24px"}, "spaceAfter": {"sm": "64px"}}). Set one side of a pair of neighbours, not both.

{
  "type": "spaceBand",
  "attrs": { "band": "tags", "style": "tabs", "showSearch": true, "rss": "icon" }
}

A community's bands

Band

Options

title

The community's name as the page's <h1> (on a category view, the category's heading or banner). Placed first by the generated page; move it or remove it like the blog's. Takes spaceBefore, spaceAfter, showOn only.

welcome

Offered, never generated: a greeting and a large search. greeting (120 characters; {name} is the signed-in reader's name), greetingSignedOut (what a signed-out reader reads; unset, the greeting without the name), showSearch (default on), searchSize (md, lg default), searchWidth, placeholder, shortcutHint (a / hint, and / focuses the field), image / imageDark (a background picture, its dark twin), align (start, center).

search

searchSize (md, lg), searchWidth, placeholder, shortcutHint.

tags

The blog's tag band options; the tabs link to the community's tag pages, and with no tags named it shows the most-used (trending) tags. The rss link is the whole community's feed.

intro, identity, filters, category

spaceBefore, spaceAfter, showOn. The filter row's tabs and look are the forum theme's (theme.forum.front.filters, Theme a forum), because a category page draws the same row.

tags is never on a generated page — you place it. GET /space-types/{type}/presets lists every band a type offers, with the options each takes (frontPage.bands and frontPage.bandOptions).

A community's category band is what a category view (?category=) shows above its topics: that category's subcategories (when the category lists them) and its featured topics. It renders nothing on the community's front page itself. Because it is a band and not part of the topic list, a front page that swaps the community's own list for a feed in another layout (grid, tiles, …) keeps it. A front page stored before the band existed, which does not place it, still shows the two in the topic list.

A blog's, a changelog's and a community's title band is the space's name as the page's <h1> — with the blog's Archive and RSS links, or the changelog's RSS link and subtitle. On a community's category view the category's heading (or its banner) is the title. With the title hidden and no level-1 heading of your own, the page keeps a visually hidden <h1> for search engines and screen readers. The generated page places it first. Move it like any band (into a column, into the sidebar), or remove it: delete the band in the studio, or send "hideTitle": true with the front page. A front page stored before the band existed, which places it nowhere, keeps the title at the top until you do one of those. {"band": "title", "showRss": false} keeps the title and drops its RSS link; the front door's showRss / showArchive switches (on PUT …/preset) do the same for every page, and a band's true never brings back a feed the front door turned off.

Round-4 band keys (2026-10-11)

  • tags: the tabs' look — size, weight, radius, inactiveColor, inactiveWeight, activeFill, inactivePadding (tight/pill), phoneFill, font, case, trackColor — style segmented, searchStyle: "icon" (compact on a desktop, a glyph that opens into the field on a phone) and rss: "button" (an outlined RSS button).

  • subscribe: heading: "" / body: "" hide the words (the field keeps a screen-reader label); layout: "field-row" (field and button on one row, no words); note (a line under the form); consent: {text, checkbox} — with checkbox: true the box is required before the form sends; the double opt-in stores nothing more; buttonStyle solid (today), link or inside (in the field's end); headingSize (a length or {sm, md, lg}); inputSize (the field's and button's height). Every surface that draws the band — the blog's front page, a post's piece page, the changelog — takes them.

  • filters (changelog): style, overflow, activeColor, activeTextColor, inactive, limit, allLabel. search takes none.

Round-5 band keys (2026-10-11)

  • tags: phoneInk (the filled phone dropdown's ink; unset, black or white by phoneFill's lightness — it was the page ink, unreadable on a dark fill), height and padding (each tab's height — exact, padding included — and its padding), rss: "icon-round" and searchStyle: "icon-round" (a round glyph button; the search one opens into the field when it takes focus). The phone dropdown's face now shrink-wraps its words, and a scrolling row keeps its search and RSS out of the scroll.

  • subscribe: headingFont, headingLineHeight (0.8–2; unset 1.1 — a large heading no longer overprints itself), buttonColor, buttonInk, buttonRadius, inputFontSize, noteLink: {label, href} (a link after the note) and buttonStyle: "inside-link" (inside the field's end, drawn as a link).

A documentation space's bands

A DOCS space's front door — the Landing preset, or any docs space whose owner stored a front page — has three bands of its own:

Band

What it draws

intro

The space's name as the page's <h1>, its description, and its authored intro (homeDoc) under it

search

The space's search box. Hidden when the front door's showSearch is false

sections

One card per collection that has pages, in the space's collection order. A collection with a cover (coverUrl) shows it across the card's top in a 2:1 box (cropped to fill); then its icon (nav.icon — a glyph name, an image or an emoji, as the docs nav draws it) beside its name (else its first page's title); its description (else the titles of its first three pages, joined by " · "); and "N pages". Each card links to the collection's page (/c/<slug>), or to its first page when the collection has no slug. Three across on a desktop, two on a tablet, one on a phone. With no collection holding a page, the band draws nothing

The sections band takes no options. Any key beside band — even spaceBefore/spaceAfter, which the blog's and community's bands take — is a 422 ("The sections band takes no options."). What a card shows is set on the collection itself: its name, description, nav.icon and coverUrl (PATCH …/collections/{id}). For a different column count or order, use a spaceFeed with filter.section per collection, or write the cards yourself with the cards block and link each to /c/<slug>.

Several feeds on one page

A front page may hold any number of feeds — "Featured" across the top, "Latest" under it, "From the community" in the sidebar. Each pages on its own:

  • The feed with controls on is the space's own reading list. It follows the reader's ?sort=, ?page=, ?category= and (on a forum) ?solved=, and carries the type's list chrome (sort tabs, filters, theme outlets). Only one is meaningful, and only on the space's own front page; anywhere else it draws as a plain feed. The block keeps its own heading, moreLink, fields, filter, limit, emptyText and dedupe — the URL's choices go over its filter and sort. Give it a card (or any composed-card key) or columns and it draws the feed you designed, still following the same URL parameters — the type's own list cannot wear them.

  • Every other paginated feed writes its own parameter, fp<slot> — fp0, fp3 — where the slot is the block's position among the page's live blocks. A sidebar's feeds count from 1000 (fp1000), so a page's pager and its sidebar's never collide. Paging one feed keeps every other parameter as it was.

Feeds over one space can keep out of each other's way:

  • dedupe: true leaves out every item an earlier feed on the page drew — "earlier" in document order, the sidebar after the page. A hero of one featured post, then a "Latest" grid with dedupe, never shows the hero twice.

  • offset (0–50) skips that many items first.

  • filter.exclude leaves out pinned, featured, or items by id.

  • filter.pinned: false / featured: false keep only the rest.

The blog's public list takes the same narrowing as query parameters: exclude=<id>,<id> and offset=<n>, beside tags=a,b&tagMatch=any|all.

spaceFeed at a glance: every option

The sections above grew a round at a time. This is the whole block on one page, grouped by what each key decides. Every object is strict (an unknown key is a 422 naming it), and GET /docs/block-schema lists every key with its type and its accepted values.

What it reads

Key

Takes

What it does

space

"self" or a space id

The space read. Unset: the space this front page belongs to

spaces

up to 8 space ids

More spaces read with space, merged into one list by date. The first six different spaces are read (the schema takes eight ids; a merged feed reads six). A space this reader may not open, or of a type with no list, adds nothing. On a ROADMAP with layout: "lanes" the boards share lanes instead

source

space, courses

courses: the organization's public course spaces, one card per course (the catalogue). path narrows it to one learning path, in its order

filter

object

Which items: category, tags + tagMatch (any/all), pinned, featured, author, lane, version, when (EVENTS: upcoming/past), section, exclude, excludeTags, context (sameTag/sameSeries, piece pages only), and on a forum status, kind, bountied. A key the target type does not honour is ignored (the table)

sort

latest, top, hot, oldest, helpful, views

Each type answers its own subset; a merged feed (spaces) always reads by date — oldest ascending, anything else newest first

period

day, week, month, year, all

The window top/hot rank over

limit

1–50

How many items. Unset: 6

offset

0–50

Skip this many first

dedupe

boolean

Leave out every item an earlier feed on this page drew (document order; the sidebar counts after the page). Merged feeds take part like any other

paginate, pageSize

true/"more", 1–50

Pages instead of a fixed limit

controls

boolean

The space's own reading list, following the address's sort/page/category (above)

How it is laid out: layout (the fourteen above), layoutSm, columns ({sm, md, lg} 1–6, offsets), pattern (bento rows), perView (carousel), gap/rowGap, columnRule, carousel, timeline, frame, width (column, wide, full), group, facets/facetStyle, interstitials, tabs, heading, headGap, moreLink, moreButton, emptyText (the empty message; "" hides an empty feed, heading included), spaceBefore/spaceAfter, showOn, and on a forum listPatch.

What each card is made of — the card's composition:

  • fields names what a card shows, in order. Unset: the space shape's own summary. Each type offers its own fields (the API refuses a field the space cannot fill).

  • card is the composed card (Designing the card): slots (the parts in order — kicker, cover, date, title, subtitle, standfirst, byline, tags, readTime, replies, readMore, index, and a forum's own), slotsSm (a phone's order), and the look of each part. A feed with no card draws the type's own card, except split, bento and carousel, which always draw the composed one.

  • card.image places the picture: position (top, start, end, alternate, overlay, none, …), span — how much of the row the picture takes beside the words: 1/4, 1/3, 1/2, 2/3 or 3/4 — aspect (1:1, 4:3, 3:2, 16:9, 21:9, 2:1, original), fit (cover, contain), focus (start, center, end) and source (cover, or authorAvatar).

  • card.when is up to four per-tag variants, [{tag, card}]: an item carrying that tag wears the patch over the feed's card (a "Podcast" post drawn differently from an essay). Not to be confused with filter.when, an events feed's half of the calendar.

The opener — lead and rest, for magazine, bento and cards-mixed:

Key

Takes

lead.style

stack, split (picture 2/3 beside a panel), overlap (a headline card over the picture), overlay (words on the picture), left-column, stack-split (the opener stacked in lead.span columns, the rest listed beside it)

lead.span

1–5: the desktop columns a stack-split opener takes. Unset: 2

lead.coverSpan

lead (unset) or all: with stack-split, the opener's picture runs across the whole row above it

lead.source

auto (unset) or featured: where the opener comes from — below

lead.divider

false drops the rule under a magazine's opener

lead.titleSize, titleFont, titleLineHeight, titleTracking, minHeight, maxWidth

The opener's own type and box, each beating the card's for it

lead.card

A card patch for the opener alone, merged over card

rest

cards (unset) or text-only — or {sm, lg}, one per screen

Which post opens a magazine

lead.source decides, on magazine, bento and cards-mixed:

  • auto (unset): the opener is the first featured item among the items the feed fetched, else the first item (on bento and cards-mixed, always the first item). The feed fetches limit items in its sort order, so a featured post older than the newest limit is not the opener — and not on the page at all.

  • featured: the opener is the newest featured item of the feed's filter, whatever limit is. The feed asks for it on its own (server-side, before the page is drawn), puts it first, and leaves it out of the rest, so the feed still shows limit items in all. With nothing featured, it is auto. Earlier feeds' items (dedupe), filter.exclude and filter.excludeTags hold for the opener too, and a feed whose filter already leaves featured items out (filter.featured: false, exclude: ["featured"]) keeps auto. Page two of a paginated feed has no opener and is unchanged.

{
  "type": "spaceFeed",
  "attrs": {
    "layout": "magazine",
    "limit": 7,
    "lead": { "source": "featured", "style": "overlay" }
  }
}

One feed, the featured post across the top and the six newest others under it. Feature a different post and the opener changes; feature none and the newest post opens. lead.source works on every type whose list knows featured items: a blog, a community, a changelog, a roadmap and an FAQ.

Two feeds still make sense when the featured strip and the list should look unrelated — a hero of featured posts, then a plain list that skips them: the first with filter: {"featured": true}, the second with dedupe: true (or filter.featured: false to keep every featured post out of it). An empty feed can hide itself: emptyText: "" draws nothing at all, heading included, when there is nothing to show — so a featured strip with nothing featured disappears instead of saying "Nothing here yet".

Sections with backgrounds

Live blocks can sit anywhere a block can: at the top level, inside a section with a background colour, gradient, image or video, inside columns. A full-bleed section (fullBleed: true) reaches the edges of the window whatever it holds, and two full-bleed sections stacked one after another meet with no gap. A full-bleed dark section holding a magazine feed is a hero; a light surface section holding a compact feed beside a newsletter form is a band. A fullBleed section reaches the screen's edges with a feed inside it too, and two bled sections in a row meet with no seam.

Width, rhythm and the masthead

Three keys beside doc shape the page itself:

  • width: list (the reading width, about 48rem — the default, with or without a sidebar), board (about 72rem), max (the site's width) or full. Each is the reader's own width token, so a theme's widths decide what they measure. A documentation space honours it too (unset there keeps the docs landing's own 56rem column).

  • gap: the space between top-level bands — none, sm, md (the default) or lg. none lets full-bleed bands stack edge to edge.

  • paddingTop: the space above the page, under the header — "none" or a length (24, "24px", "1.5rem", "4vh"; at most 400px / 25rem / 50vh). Unset, the reader decides: nothing when the page opens with a full-bleed block (so a hero band meets the header) or when the header overlays the page (chrome.header.overlay), and 3rem otherwise. This changes one default: a front page that opens with a full-bleed section used to sit 3rem below the header; send "paddingTop": "3rem" to keep that. null clears the key.

  • paddingBottom: the space under the page, above the footer — the same values as paddingTop. Unset is 3rem, as it always was (it is never decided for you). "none" lets a last full-bleed band meet the footer. null clears the key; a misspelling (paddingBotom) is a 422 naming it.

A blog's index draws its title band — the space's name, the archive link and RSS — which a composed page moves as spaceBand{band:"title"} or hides with hideTitle. frontDoor.title (on PUT …/preset) replaces the name's words. The masthead is [data-fx-index-head], its title [data-fx-page-title] and [data-fx-index-title], so typography.levels.h1 reaches it — and typography.levels.indexTitle (size, lineHeight, tracking, weight, color, colorDark) sizes the index title on its own, apart from the posts' and releases' titles.

The blog's list pages (the archive, a month, an author, a series, a tag) keep their 3rem above the content; under an overlaid header they add the header's height (chrome.header.height, else about 64px), so their first line is not drawn under it.

width, paddingTop and paddingBottom apply to blogs, changelogs and communities (forum wave, 2026-10-12: a community's page ignored all three). With a sidebar, width is the whole page, rail included. A community keeps its fixed 3rem top until you set a padding key or hide the title.

Round 4 (2026-10-11). width also takes a px cap, a whole number from 480 to 1920 ("width": 1300). paddingTop also takes one per screen — {"sm": "48px", "lg": "72px"} — and a screen you leave out keeps what the reader decides there; lengths may now be vw (up to 20).

The sidebar

{ "position": "right", "width": "narrow", "doc": { "type": "doc", "content": [] } }

position is left, right or none; width is narrow (about 16rem), default (about 20rem) or wide (about 24rem). A front page with a sidebar runs wider than the surface's own width by the sidebar and its gap, so the page's column keeps the width it has without one. startAfter: 1 starts the sidebar beside the page's third block: the blocks up to it run full width above it, so a hero band can span the page and the sidebar begin under it. The sidebar's document takes the same blocks as the page, live ones included. From 1024px wide the sidebar sits beside the page and stays in view as the page scrolls; narrower than that, it stacks — a left sidebar above the page, a right one below it.

Round 4 (2026-10-11). gap sets the room between the page and the sidebar (px up to 200, or rem up to 12.5; unset 2.5rem) — the page widens by it rather than squeezing the column. divider: {style, color, width} draws a rule between them from 1024px up (style solid, dashed or dotted, color a #hex, width 1–4 px), half the gap either side.

Round 5 (2026-10-11). gap and divider were stored but never reached the page; they are drawn now. phone places the sidebar under 1024px: after the page (unset), before it, hide, or {"at": N} after the main column's Nth top-level block (round 6).

The docs tree as the sidebar. On a documentation space, "source": "docs-nav" draws the space's own page tree in the sidebar — the same nav column every page carries, search and version switchers included — and lays the front page out in the docs frame beside it, instead of a bare landing. doc is still required; send an empty one:

{ "position": "left", "source": "docs-nav", "doc": { "type": "doc", "content": [] } }

Forum wave (2026-10-12). phone: "drawer": under 1024px the sidebar folds behind a Menu button at the top of the page, and opens in place — no script.

A second rail

{ "sidebar": { "position": "left", "doc": { … } }, "aside": { "width": "narrow", "phone": "after", "doc": { … } } }

aside is a second rail, always on the right, beside a sidebar on the left — with the sidebar anywhere else it is a 422 ("Put the sidebar on the left to add an aside"). width as the sidebar's; phone is after (unset) or hide. From 1280px the page is three columns; from 1024px the aside drops under the main column; under 1024px it follows the page. The page widens by both rails. In the studio: Second rail under the sidebar settings.

A site's front door

A site has a front page of its own, stored on the site. Its generated default is the site's search box and its spaces' sections — the two bands a site front page accepts (search, sections); any other band name is refused. Its feeds, rails and leaderboards read only the spaces in the site (its spaceIds): a block that names any other space of the organization draws nothing, so a site never shows a space it does not publish. The usual composition is a magazine feed of the site's blog, a compact feed of its changelog and a spaceCategories of its community, all on one page. How a site front page relates to each space's own front page and homeDoc is in Sites.

A theme bundle imported into a site brings its front page as part of the site's theme draft, and it goes live when the draft is published. To see it first, mint a site preview link (POST /orgs/{orgId}/sites/{siteId}/preview-tokens, or Sites → Preview links → Preview draft in the console) and open the site with ?preview=<token>: its front door renders the draft's front page under the draft theme, with a Preview ribbon, uncached and noindex. Details in Draft a theme, and preview it.

Customization, share card and JSON-LD

A front page takes the same vocabulary as a single page or post (A page's own look and share card):

Key

What it does

customCss

CSS for this front page only, after the space theme; sanitised and scoped like customCss

theme

A partial theme layered over the space's — colours, palettes, fonts, scheme, widths, motion

shareImageUrl

The link-preview image for the front page

ogTitle, ogDescription

The link-preview title and description

twitterCard

The X/Twitter card type

jsonLd

Your own structured data, replacing the automatic markup on this address. Must be valid JSON

What a reader who cannot see a space gets

Nothing. A feed (or category rail) resolves its target through the public overview — the reader's own gate. When that refuses — a private space, a gated space this reader has not passed, a deleted one, one in another organization — the block renders nothing at all: no frame, no heading, no "see all" link. A front page never tells a reader a space exists that its own address would refuse them.

A share token in the address unlocks the page's own space only. It was minted for that space; a feed of a second private space on the same page still renders nothing to the token holder.

Pinning and featuring posts

Blog posts and changelog releases have two switches, Pin to top and Feature, in the studio's post card, or over the API:

curl -sS -A 'my-app/1.0' -X PATCH \
  -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  https://forumulate.com/api/orgs/$ORG/spaces/$SPACE/blog/$POST \
  -d '{ "featured": true, "pinned": false }'
  • Pinned keeps a post at the top of the default (latest) order.

  • Featured marks it for a featured strip: a feed with filter.featured shows only featured posts, and a feed with lead.source: "featured" opens on the newest featured post wherever it is.

The public blog and changelog lists accept featured=true|false and pinned=true|false (absent means both), and sort=oldest. A community's public list accepts pinned and sort=oldest; topics are pinned the way they always were, by a moderator.

Theming hooks and outlets

Hook

The element

[data-fx-feed]

A feed's (or a category rail's) wrapper

[data-fx-feed-item]

One card in it

[data-feed-layout='magazine']

On the wrapper: the layout it wears

[data-feed-type='BLOG']

On a feed's wrapper: the type of the space it reads

[data-feed-featured]

On a card: the item is featured

[data-feed-categories]

On the wrapper: it is a category rail, not a feed

[data-feed-card='composed']

A composed card; .fc-title, .fc-meta, .fc-media inside

[data-card-image='end']

On a composed card: where its picture sits

[data-card-surface]

On a composed card: it is drawn as a box

[data-card-lead='split']

On a composed card: it is the lead, and how

[data-fx-tags-band]

The tag pills / category tabs band

[data-fx-index-head]

A blog index's masthead

.forumulate-reader [data-fx-feed][data-feed-layout='magazine'] [data-feed-featured] h3 {
  font-size: 2.5rem;
}

feed, feed-item, index-head and tags-band are also theme.motion.targets keys. frontDoor.badgeStyle (pill, outline, text) restyles the tag chips on cards as well as the badges. Two theme component outlets frame a front page — a space's, and a site's front door too: front-page-top above the composed document and front-page-bottom below it. On a space's front page they show the components that space wears; on a site's front door, the components of the site's theme.

A theme bundle may carry frontPages — one stored front page per space type, plus SITE. Installing the bundle applies the entry whose target matches the address and ignores the rest. The layout and the page's own look travel; the share card and JSON-LD describe one address, so they are stripped with a local notice.

The API

Call

What it does

GET /orgs/{orgId}/spaces/{spaceId}/front-page

{ frontPage } — the stored front page, or null (the default)

PUT /orgs/{orgId}/spaces/{spaceId}/front-page

Store one. Replaces the whole stored front page

DELETE /orgs/{orgId}/spaces/{spaceId}/front-page

Reset to the generated default

GET /orgs/{orgId}/sites/{siteId}/front-page

The same for a site

PUT /orgs/{orgId}/sites/{siteId}/front-page

DELETE /orgs/{orgId}/sites/{siteId}/front-page

The PUT body is { doc, sidebar?, customization?, preset?, hideTitle?, width?, gap?, paddingTop?, paddingBottom? }. doc is a block document validated by the same block union as every page, so a block or attribute that does not exist is a 422 naming it. sidebar is { position, width?, startAfter?, doc } or null for none. The envelope is strict: an unknown top-level key is refused by name.

curl -sS -A 'my-app/1.0' -X PUT \
  -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  https://forumulate.com/api/orgs/$ORG/spaces/$SPACE/front-page \
  -d '{
    "doc": { "type": "doc", "content": [
      { "type": "spaceBand", "attrs": { "band": "intro" } },
      { "type": "spaceFeed", "attrs": {
          "heading": "Featured", "layout": "magazine", "limit": 5,
          "filter": { "featured": true } } },
      { "type": "spaceFeed", "attrs": {
          "heading": "Latest", "layout": "grid", "controls": true, "paginate": true } },
      { "type": "spaceBand", "attrs": { "band": "subscribe" } }
    ] },
    "sidebar": { "position": "right", "width": "narrow", "doc": { "type": "doc", "content": [
      { "type": "spaceFeed", "attrs": {
          "space": "9c71…", "heading": "From the community", "layout": "compact",
          "sort": "hot", "period": "week", "limit": 5,
          "moreLink": { "label": "All discussions" } } }
    ] } },
    "customization": { "ogTitle": "The Acme blog", "shareImageUrl": "/api/assets/…" }
  }'

moreLink with no href links to the target space's own address.

In markdown

Each live block is one self-closing directive with its settings spelled out:

:::band{name="intro"}:::

:::feed{layout="magazine" featured limit="5" heading="Featured"}:::

:::feed{layout="grid" cols="3" colsMd="2" colsSm="1" controls paginate heading="Latest"}:::

:::feed{space="9c71…" layout="compact" sort="hot" period="week" limit="5" more="All discussions"}:::

:::categories{layout="tiles" fields="color,count,topTopics" topTopics="3" heading="Browse"}:::

cols, colsMd and colsSm are the desktop, tablet and phone columns; fields and tags are comma lists; more and moreHref are the "see all" link (moreAt, moreStyle its position and style); empty is the empty-state text; the bare flags paginate, controls, dedupe, pinned and featured mean true. layoutSm, pattern, rest, offset, offsets, exclude, subtitle, headingStyle, spaceBefore, spaceAfter and perView/perViewMd/perViewSm are spelled as their keys (spaceBefore/spaceAfter take JSON for {sm, md, lg}) and subtitleInline is a flag; card, lead, tabs, interstitials, moreButton and the rest of a heading's look (headingLook) are JSON in one attribute. A band's options ride on its directive. A directive edited into something invalid is dropped on import rather than failing the document.

:::feed{layout="bento" pattern="1,2,3" dedupe card="{\"surface\":{\"radius\":18}}"}:::
:::band{name="tags" style="tabs" showSearch rss="icon"}:::
:::band{name="subscribe" layout="inline-row" frame="none"}:::

In the API

Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go clients are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP call_api tool. Where a CLI command or MCP tool exists too, the last column names it. Operation names link to the API reference.

Site Front Page

Operation

Request

What it does

Shortcuts

SiteFrontPage_get

GET /orgs/{orgId}/sites/{siteId}/front-page

The site front page

client.sites.frontPage.get()

SiteFrontPage_set

PUT /orgs/{orgId}/sites/{siteId}/front-page

Replace the site front page

client.sites.frontPage.set()

SiteFrontPage_remove

DELETE /orgs/{orgId}/sites/{siteId}/front-page

Reset the site front page

client.sites.frontPage.remove()

Space Front Page

Operation

Request

What it does

Shortcuts

SpaceFrontPage_get

GET /orgs/{orgId}/spaces/{spaceId}/front-page

The front page

client.spaces.frontPage.get()

SpaceFrontPage_set

PUT /orgs/{orgId}/spaces/{spaceId}/front-page

Replace the front page

client.spaces.frontPage.set()

SpaceFrontPage_remove

DELETE /orgs/{orgId}/spaces/{spaceId}/front-page

Reset the front page

client.spaces.frontPage.remove()

The Forumulate teamUpdated