Add an asset or collection to the featured list
POST /admin/featured
Appends a subject to the curation list, or inserts it at the
given position. Re-adding an already-featured subject returns
409. Requires system.admin.
Authorizations
Section titled “Authorizations ”Request Body required
Section titled “Request Body required ”GitHub #341 — payload to feature an asset, a collection or a team.
object
team was added by #1084. The admissible list is restated in
the handler’s validation and in the
featured_items_subject_kind_check database constraint
(migration 00048); all three move together, because a value
missing here is one the generated client will not send at
all.
Optional insertion position. Omit / null to append to the end of the list.
The placement’s AUDIENCE (#1104). scope decides who may
see the featuring; it does not decide which surface renders
it. A reader shows every audience its viewer qualifies for:
signed-in readers see org and public, anonymous readers
see public only.
Omit for org, the internal signed-in audience and the
audience every write produced before this property existed.
public includes anonymous visitors and is the deliberate
act; both are gated on system.admin, which is the
capability the whole endpoint already requires.
team is a valid value of the underlying column and is
deliberately not offered here: a team placement also needs a
team id, which this payload has no way to name, and no
reader consumes that audience yet.
Responses
Section titled “ Responses ”Added.
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
The featured_items row id (used for remove/reorder).
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.
0-based display order.
Resolved subject title (asset title or collection name). Empty when the subject was deleted out from under the row.
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.
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.
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.
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.
The vertical half of the tile’s focal point (#1207). See
cover_focal_x.
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.
Thumbnail hint for the tile’s cover asset; null when there is nothing servable for this caller. Populated for collection subjects too since #559.
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.
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.
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
The handle. Also the profile link target
(/users/by-username/{username}).
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.
Null for users with no uploaded avatar; the card renders a generated initials disc in that case.
Malformed request
object
Human-readable error summary
Example
the request could not be completedAuthentication required, missing, or invalid
object
Human-readable error summary
Example
the request could not be completedExample
{ "error": "authentication required: sign in and retry with a valid session or API token"}Authenticated but missing required capabilities
object
Human-readable error summary
Example
the request could not be completedSubject already featured.
object
Human-readable error summary
Example
the request could not be completed