Skip to content

The operator promo band for the browse feed (GitHub

GET
/featured/promo

The full-width strip the browse feed renders BETWEEN pages, or nothing.

THE COLLAPSE IS DECIDED HERE, NOT BY THE CLIENT. The response is {} — no band key at all — when the install has no band, when the band is disabled, when it has no headline, or when every one of its cards filtered away for this caller. Answering with an empty band and letting the page decide would put the rule in every client that ever renders one; answering with nothing puts it beside the filter that produced the emptiness. See PromoBand for why collapse (ADR 0030) rather than substitution (ADR 0079 §2, which is scoped to in-grid sized slots).

FEATURING NEVER WIDENS ACCESS, and the band is not an exception. Every card resolves through the same query the featured rail uses: the caller’s ADR 0063 predicate is spliced into the JOIN that resolves each subject, per-asset sensitivity still suppresses the thumbnail hints (ADR 0020), and the ADR 0090 mature conjunct drops a mature item for a reader who has not opted in. A card pointing at content the caller may not see produces no row, not a blank tile.

Anonymous-readable, and gated by the install’s public-mode setting exactly like GET /featured — the /featured prefix is one entry in auth.PublicSurfaceRoutes, so both move together. Whether an anonymous visitor sees a band at all is then the BAND’s own scope: public shows to everyone, org to signed-in readers only.

The band this caller may see, or an empty object when there is nothing to render.

object
band

An operator-authored full-width promo strip rendered BETWEEN pages of the browse feed (#1118): a headline, a blurb, an optional call-to-action, and an ordered row of curated cards.

THE CARDS ARE ORDINARY PLACEMENTS. They are featured_items rows carrying this band’s id, resolved through the same query, the same ADR 0063 predicate splices and the same ADR 0020 sensitivity and ADR 0090 mature gates as the featured rail — so a card pointing at content this caller may not see produces nothing at all, and a band is never a way to surface something.

THE COLLAPSE IS DECIDED SERVER-SIDE. GET /featured/promo answers with NO band object at all when the install has none, when the band is disabled, when it has no headline, or when every card filtered away for this reader. That is ADR 0030’s rule for a banner between content, which is the geometry a full-width band has; ADR 0079 §2’s “an unfilled slot becomes ordinary content” is scoped to IN-GRID sized slots, where collapsing would leave a hole in the middle of a wall. Between two walls there is no hole to leave.

object
id
required
string format: uuid
title
required

The strip’s headline. A band with an empty one does not render — a button and a blurb with no headline is an unfinished draft, not a banner.

string
blurb
required

The short line under the headline. May be empty.

string
cta_label
required

The call-to-action button’s text. Empty means the band has no button; cta_label and cta_url are written and validated as a PAIR, so one is never present without the other.

string
cta_url
required

Where the button goes. Restricted to an absolute http(s) URL or a site-relative path beginning with a single /.

THIS IS A SECURITY CONSTRAINT, not a formatting one. The value becomes an href on the browse page of every reader the band is shown to, and a javascript: URL there is stored XSS. A scheme-relative //host/x is refused too — a reader cannot tell it from a local link. Enforced by the handler (a 400) and by a CHECK constraint in migration 00053 (the backstop).

string
enabled
required

The operator’s switch. A disabled band is invisible to readers — it is filtered out by the query, not returned with a flag for a client to honour.

boolean
after_page
required

Where the band falls, counted in whole feed pages: 1 renders it after the first page.

The PAGE SIZE is the client’s (browse requests 36 posts at a time), so this is a position in the reader’s scroll rather than a row count — deliberately, because “after the first page” is something an operator can predict without knowing the API’s limit.

integer
>= 1
scope
required

THE WHOLE BAND’S AUDIENCE (ADR 0065’s placement scopes, narrowed to the two a band can hold). public includes anonymous visitors on an install that has opened its public surface; org is the signed-in audience.

It lives on the BAND and not on each card. A band is one authored unit with a headline and a button, and “half these cards are for logged-out visitors” is not a state an operator can hold in their head — nor one worth a denormalised copy of a visibility input on every card row. A card’s own scope column is left at its default and is never read.

string
Allowed values: public org
items
required

The cards, in curation order, filtered to what THIS caller may see. Never empty in a response: a band whose cards all filtered away is not returned at all.

Array<object>

GitHub #341 — one admin-curated featured entry pointing at an asset, a collection or a team, with an explicit ordering position and the resolved subject title. Assets and collections carry thumbnail hints so a tile can render a cover: assets from themselves, collections via ADR 0027’s hero-card fallback (#559). A team subject (#1084) resolves its NAME only and carries no hints — a team’s picture is admissible solely through the render-time hero re-check (#982), and resolving it here would be a second copy of that rule inside a query whose gates are deliberately weaker than the public rail’s.

object
id
required

The featured_items row id (used for remove/reorder).

string format: uuid
subject_kind
required

Must list every value FeaturedItemInput.subject_kind accepts. It is a separate schema and was the place this was nearly missed in #1084: the server would have gone on serialising team (Go emits the string it holds), while the generated client’s type narrowed it away and a strict response validator rejected the row — a placement the operator could see in the database and not in the UI.

string
Allowed values: asset collection team
subject_id
required
string format: uuid
position
required

0-based display order.

integer
title
required

Resolved subject title (asset title or collection name). Empty when the subject was deleted out from under the row.

string
subtitle
required

The wide card’s second line (#1110): the COLLECTION’s own description, empty when it has none — and empty for every other subject kind.

WITHHELD EXACTLY WHEN title IS, structurally rather than by a second rule. It is selected from the same c join that produces title for a collection subject, and that join carries the caller’s ADR 0063 collection predicate in its ON clause: a collection this caller may not see fails to join, contributes neither name nor description, and the placement row is then dropped entirely. There is no arrangement in which a description arrives without the name it belongs to.

ASSET SUBJECTS CARRY NO SUBTITLE, deliberately. ADR 0020 makes a non-public asset title-only on this surface — the rail already suppresses its thumbnail hints for exactly that reason — so sourcing this from assets.description would widen an embargo tile from “title” to “title and the author’s notes”. #1110 specifies the collection’s description; the asset half of that widening is not a rendering choice this field gets to make.

string
item_count

How many members of a COLLECTION subject THIS CALLER can see — the wide card’s subtitle fallback when subtitle is empty (#1110). Null for every non-collection subject.

Caller-scoped, not a raw COUNT(*). The membership is the same one the mosaic composes from — pinned, unexpired collection_posts rows, gated by the caller’s own post read predicate. A bare count would publish the SIZE of the withheld part of a public collection, which is the derived-copy leak class #902 closed for search text and #1066 closed for embeddings, arriving through a subtitle.

#1236 dropped the collection_resources half that used to be added to this. The tile describes what the page shows, and the page shows posts; a tile advertising more items than the collection opens to is the count disagreeing with the thing it counts.

integer
nullable
cover_asset_id

The asset whose col variant renders this tile (#559).

For an asset subject this is the subject itself. For a COLLECTION it is the hero-card fallback of ADR 0027 — the most-recent post’s cover asset — which is why the id is needed separately: subject_id is the collection, and the variant endpoint is keyed by asset.

Null when nothing is servable to this caller, in which case the tile is title-only. Subject to the same gating as asset_file_hash, so it never names an asset the caller cannot see.

SINCE #1207 A COLLECTION’S IS THE CURATOR’S CHOICE WHEN THERE IS ONE. The rail resolves it in preference order — featured_cover_asset_id, then cover_asset_id, then ADR 0027’s derived cover — with every rung carrying the caller’s own asset predicate, so a chosen cover this caller may not picture falls through to the next rung instead of leaking. Before that the rail read neither chosen column and always derived, which is #1200: the strip showed one picture and every other collection surface showed another.

string format: uuid
nullable
cover_focal_x

Where to centre this tile’s crop horizontally, as a fraction of the cover picture’s width (#1207) — the curator’s featured_cover_focal_x, ready to be written straight into CSS object-position.

Null means centre, which is the CSS default and what every tile did before this existed, so a client that ignores it renders exactly what it rendered before.

ONLY EVER SET WHEN THE TILE IS SHOWING A CHOSEN COVER. A derived cover is a picture the curator never saw in the editor, so positioning chosen for a different picture is not carried onto it; that arm returns null and the tile centres. Null for every non-collection subject.

number format: double
nullable
cover_focal_y

The vertical half of the tile’s focal point (#1207). See cover_focal_x.

number format: double
nullable
cover_zoom

How far this tile’s crop is tightened, as a multiplier on the fitting rectangle (#1212) — the curator’s featured_cover_zoom, ready to be applied alongside cover_focal_x/_y.

Null means fit, which is what every tile did before this existed. ONLY EVER SET WHEN THE TILE IS SHOWING A CHOSEN COVER, for the reason the focal point is: a derived cover is a picture the curator never framed, so a framing chosen for a different picture is not carried onto it.

number format: double
nullable
asset_file_hash

Thumbnail hint for the tile’s cover asset; null when there is nothing servable for this caller. Populated for collection subjects too since #559.

string
nullable
preview_available
required

True iff a servable col variant exists for the tile’s cover asset AND the caller passes the content plane (ADR 0064). The rail renders a thumbnail only when true, else a title-only tile with no byte request (#471). Computed from live variant existence rather than from a stored flag, which is what makes it trustworthy — the asset_has_image field that used to sit beside this one was the projection of a column nothing wrote, and was removed with it in #579. Populated for collection subjects too since #559.

boolean
ladder_available
required

True iff every variant in the operator’s CONFIGURED preview ladder exists for the tile’s cover asset AND the caller passes the content plane. Identical contract to Asset.ladder_available — see there for why this is computed from config rather than from the default rung list.

boolean
author

The subject’s owner, as the promo band’s card chip draws it (#1118) — the asset’s owner for an asset subject, the collection’s for a collection.

POPULATED ONLY BY GET /featured/promo, and the absence elsewhere is a statement rather than an oversight. The featured rail’s card prints a subtitle, not an author (#1110), and a value nothing consumes is a value nothing tests; the admin curation list identifies a row by its subject id, which is what an operator needs to prune it.

The name inside is resolved by users.ResolveDisplayName through users.LookupAuthors — the one home of the display-name ladder, with its authenticated rung and its anonymous arm. It is NOT re-derived per surface: #1023 exists because that ladder had been transcribed four times and three copies were wrong.

ABSENT — not redacted — for an author who took ADR 0024’s opt-out and for a hard-deleted account, exactly as on a post card. “No author object” means “no name to show”, never “look it up another way”.

object
ref
required
integer format: int64
username
required

The handle. Also the profile link target (/users/by-username/{username}).

string
display_name
required

Resolved display string ready to render — never empty.

RESOLUTION IS CALLER-DEPENDENT and matches UserPublic’s exactly, because it is literally the same expression (users.ResolveDisplayName):

authenticated: profile.display_name → user.fullname → username anonymous: profile.display_name → username

The anonymous rung skips fullname on purpose. Real name is authenticated-only (ADR 0070 §3), and a display-name fallback that reaches it would be that rule defeated by a COALESCE.

string
avatar_url

Null for users with no uploaded avatar; the card renders a generated initials disc in that case.

string
nullable
created_at
required
string format: date-time
created_at
string format: date-time
updated_at
string format: date-time