Space front pages
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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| none — a docs space opens its first page. The Landing preset generates |
|
|
| as |
site |
|
"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 |
|---|---|
| One column of full cards — a blog's single column. The default |
| Cover-led cells, three per row on a wide screen |
| One opener drawn large — your featured post if the feed holds one, else the newest — then a grid |
| A divided list, one line each — a community's stat rows |
| Title and date, nothing else — a sidebar's "Latest" |
| Square tiles with the picture behind the title |
| A bento: the opener spans two by two, every fifth card runs wide |
| One item per row, the picture beside the text — editorial rows, or a screen-tall hero |
| Rows of N from |
| A scroll-snap row with arrows and a "1 of 4" count; |
| A grid on a line: each item a dot on one rule across the row ( |
| A ruled table, one row per item, its columns the feed's |
| One disclosure per item, the title its summary; the only one on a page is the page's |
| ROADMAP only: the board's status lanes as columns, read across |
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 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| — | 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.
pinnedandfeaturedtaketrue(only marked items) orfalse(only items that are not marked) —"pinned": falseunder a pinned hero keeps the hero out of the feed below it. Absent means both.Several
tagscombine bytagMatch:"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(solvedorunanswered— questions only),kind(questionordiscussion) andbountied(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 |
|---|---|
| The thread's faces, overlapping: the opener, the latest replier, the most frequent |
| The last poster's face and name, and how long ago ("Ana · 3h") |
| When the thread last moved (its last post), relative |
| Every reaction on the thread, summed |
| "Asked in {category}" on a question, "Posted in {category}" otherwise |
| The last two replies, one line each |
| The |
| On |
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 |
|---|---|
| The parts, in order: |
|
|
| Slots drawn in the picture's column, above it: any of |
|
|
|
|
| The gutter between the words and a picture beside them |
| The words above the title: |
|
|
|
|
|
|
|
|
| Slots that join one meta row, where the first of them falls: any of |
|
|
|
|
|
|
| The subtitle's colour |
| Lines of standfirst before it is clamped, 0 (never) – 6; |
|
|
|
|
|
|
|
|
| The card as a box: |
| The meta rows' type: |
|
|
| Also |
| The standfirst's ink (unset: muted); |
| The space between the card's parts (title, standfirst, meta…), a length or one per breakpoint; unset 6px. Not |
| 0.8–2 (unset 1.25), and |
|
|
|
|
|
|
| Each also one per breakpoint, |
| One rule between the rows of a grid, across the gutter, none after the last row |
| "3 days ago" under 30 days, the |
| The numeral: |
| A phone's order of the parts; a part it leaves out is hidden on a phone, and the picture takes |
| Also |
| Per-tag variants: |
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 |
|---|---|
| The title's ink (a run-in title's too). Unset: the surface's |
| The title's widest line, a length ( |
| The card's text colour; the title takes it and the muted parts (meta, standfirst, what rides with the picture) take it softened |
|
|
|
|
| The author role's type size (unset 0.85em) |
| Between a name and its role on one line, ≤8 chars (unset |
| Between the joined meta parts, ≤8 chars (unset |
| After a run-in title, before the standfirst, ≤4 chars ( |
| The tag pills' ink, and an outline pill's border unless |
| The pill box's border colour and corner |
| 1–12: at most this many tags, the rest behind a "•••" chip (the post head's) |
| EXTRA room after named parts, on top of |
| The room BEFORE named parts, in place of |
| 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 |
| Also take |
| 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 |
|---|---|
| The opener of a |
| Everything after the lead: |
| A |
| A |
| The arrangement on a phone, when it differs: |
| A staggered grid: pixels each desktop column is pushed down, e.g. |
| Words, or |
|
|
| Tag tabs over this feed that filter it in place ( |
| The room above this feed (round 5) — the section's |
| The room under this feed before what follows it — stacked feeds in one section (a length, or |
| Notes between the rows: |
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 |
|---|---|
| The column gap between a feed's cards — and the row gap too, unless |
| The gap between rows, a length or |
|
|
| Also |
| The opener's own min height, a length or |
| The opener's words column at most this wide |
| The head's weight (100–900) and line height (0.8–3). Unset, the head follows the theme's |
|
|
| The room under the head, a length or |
|
|
| Now in twentieths: |
|
|
| The note floats over the row after it instead of taking a row: |
|
|
| Up to six tabs that SORT or FILTER rather than pick a tag: |
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="{…}".
Round-5 keys: lead, tabs, filter, carousel, timeline (2026-10-11)
Key | What it does |
|---|---|
| A card patch for the opener alone, merged over the feed's card (and any |
| The room under the feed's tabs, a length or |
| Up to 10 tags (slug or label): items carrying any of them are left out. Lanes inherit it |
| Also |
| A grid whose items are dots on one line across the row (on a phone the line runs down the start). Columns default |
| Now honoured on a |
| Each also |
Round-6 keys (2026-10-11)
Key | What it does |
|---|---|
| The |
|
|
| The line as a gradient from |
| The arrows' corner — |
|
|
|
|
|
|
| The phone dropdown face's exact height (also on the |
Round-7 keys (2026-10-11)
Key | What it does |
|---|---|
| A length: pads the whole |
| A length: the gap between the kicker's rule and its words. Unset: 12px ( |
| A length or |
| 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 |
| A length or |
| px (80–1200): the newsletter heading block's measure — the heading and subheading wrap at it. Unset: the column's |
| The |
| Also |
| The card's subtitle (dek) size, a length or |
| A length: the "Read more" link's type size. Unset: 14px |
| Now also |
heading / list | A length or |
list | A list's own type size, as a heading's or paragraph's |
Round-3 forum keys (2026-10-12)
On the card (spaceFeed.card, and a when variant's card):
Key | What it does |
|---|---|
|
|
|
|
|
|
On the feed:
Key | What it does |
|---|---|
|
|
{ "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 |
|---|---|
|
|
|
|
| Now also |
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 |
|---|---|---|---|
|
|
| On a docs front page a feed stays in the same 768px column as the blocks around it. |
| any | the space's list | That feed's forum rows wear these list keys, for example |
| boolean | follows | The glyph before the numbers in the |
| boolean | off | An 18px face before each reply preview. A preview with no picture shows the author's initials |
card field / slot | — | — | 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 |
| — | — | "@username" in place of the display name, for rows that have a username |
|
| off |
|
|
| none | The rows layout draws each category's latest topics, up to 6 per category. It works with any |
{ "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 2h ago" now sits on the card's meta row, in the date's muted 13px type (and |
|
| 24px on a | The room between whatever stands above the feed (a lede paragraph) and the feed's "View all" row or first card. A |
|
|
| Which part of a cropped picture stays in its box ( |
|
|
|
|
| 12–28 (px), or a type level ( |
| A tile's (and |
| 400–800 | 600 | The name's weight |
|
| bare muted titles | Each top topic under a tile or a |
| — | — | The display name. With |
|
|
|
|
| 10–48 (px) | 12px | The rank number's size, for a board whose numbers are the design (markdown |
heading |
|
| On a community's composed front page, the top-level heading marked |
heading |
| none | A link beside the |
heading |
|
|
|
{ "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.*,widthandlayout.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 |
|---|---|
| A compact link tree — 32px rows, a 10px colour square, the count on the right, the category being viewed marked. |
| 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 |
| Each category beside its latest topics (Discourse's "Categories + latest"). |
|
|
| Also one per screen: |
| No "Categories →" link. |
| 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:
| Who is on it, and in what order |
|---|---|
| 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 |
| The same over the last 30 days |
| All time — the hall of fame — ranked by likes received, then solutions, then posts |
| 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 |
|---|---|
| The member's picture (fetched through us), or an initials disc |
| The display name |
| "@handle" in the name's place; with |
| The trust level's label: New, Basic, Member, Regular or Leader |
| Likes received in the period |
| Answers accepted in the period |
| Posts in the period |
| Points — filled only on |
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 |
|---|---|
| A blog's tag pills or category tabs. |
|
|
|
|
|
|
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 |
|---|---|
| The community's name as the page's |
| Offered, never generated: a greeting and a large search. |
|
|
| The blog's tag band options; the tabs link to the community's tag pages, and with no |
|
|
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— stylesegmented,searchStyle: "icon"(compact on a desktop, a glyph that opens into the field on a phone) andrss: "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}— withcheckbox: truethe box is required before the form sends; the double opt-in stores nothing more;buttonStylesolid(today),linkorinside(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.searchtakes none.
Round-5 band keys (2026-10-11)
tags:phoneInk(the filled phone dropdown's ink; unset, black or white byphoneFill's lightness — it was the page ink, unreadable on a dark fill),heightandpadding(each tab's height — exact, padding included — and its padding),rss: "icon-round"andsearchStyle: "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) andbuttonStyle: "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 |
|---|---|
| The space's name as the page's |
| The space's search box. Hidden when the front door's |
| One card per collection that has pages, in the space's collection order. A collection with a cover ( |
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
controlson 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 ownheading,moreLink,fields,filter,limit,emptyTextanddedupe— the URL's choices go over itsfilterandsort. Give it acard(or any composed-card key) orcolumnsand 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: trueleaves 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 withdedupe, never shows the hero twice.offset(0–50) skips that many items first.filter.excludeleaves outpinned,featured, or items by id.filter.pinned: false/featured: falsekeep 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 |
|---|---|---|
|
| The space read. Unset: the space this front page belongs to |
| up to 8 space ids | More spaces read with |
|
|
|
| object | Which items: |
|
| Each type answers its own subset; a merged feed ( |
|
| The window |
| 1–50 | How many items. Unset: 6 |
| 0–50 | Skip this many first |
| 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 |
|
| Pages instead of a fixed |
| 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:
fieldsnames 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).cardis 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 nocarddraws the type's own card, exceptsplit,bentoandcarousel, which always draw the composed one.card.imageplaces 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/3or3/4—aspect(1:1,4:3,3:2,16:9,21:9,2:1,original),fit(cover,contain),focus(start,center,end) andsource(cover, orauthorAvatar).card.whenis 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 withfilter.when, an events feed's half of the calendar.
The opener — lead and rest, for magazine, bento and cards-mixed:
Key | Takes |
|---|---|
|
|
| 1–5: the desktop columns a |
|
|
|
|
|
|
| The opener's own type and box, each beating the card's for it |
| A card patch for the opener alone, merged over |
|
|
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 (onbentoandcards-mixed, always the first item). The feed fetcheslimititems in itssortorder, so a featured post older than the newestlimitis not the opener — and not on the page at all.featured: the opener is the newest featured item of the feed's filter, whateverlimitis. 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 showslimititems in all. With nothing featured, it isauto. Earlier feeds' items (dedupe),filter.excludeandfilter.excludeTagshold for the opener too, and a feed whose filter already leaves featured items out (filter.featured: false,exclude: ["featured"]) keepsauto. 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) orfull. 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) orlg.nonelets 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.nullclears the key.paddingBottom: the space under the page, above the footer — the same values aspaddingTop. Unset is 3rem, as it always was (it is never decided for you)."none"lets a last full-bleed band meet the footer.nullclears 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 |
|---|---|
| CSS for this front page only, after the space theme; sanitised and scoped like |
| A partial theme layered over the space's — colours, palettes, fonts, scheme, widths, motion |
| The link-preview image for the front page |
| The link-preview title and description |
| The X/Twitter card type |
| 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.featuredshows only featured posts, and a feed withlead.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 |
|---|---|
| A feed's (or a category rail's) wrapper |
| One card in it |
| On the wrapper: the layout it wears |
| On a feed's wrapper: the type of the space it reads |
| On a card: the item is featured |
| On the wrapper: it is a category rail, not a feed |
| A composed card; |
| On a composed card: where its picture sits |
| On a composed card: it is drawn as a box |
| On a composed card: it is the lead, and how |
| The tag pills / category tabs band |
| 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 |
|---|---|
|
|
| Store one. Replaces the whole stored front page |
| Reset to the generated default |
| The same for a site |
| |
|
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 |
|---|---|---|---|
| The site front page |
| |
| Replace the site front page |
| |
| Reset the site front page |
|
Space Front Page
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| The front page |
| |
| Replace the front page |
| |
| Reset the front page |
|