Resources
Renders ResourcesBlock from @bison-lab/ui: one featured card beside a list. The featured card is required; the list opens empty. Each card needs a heading and a date. The date is text, written as it should read (“March 2026”), and a card missing either is not rendered. A card with no author (one fed from a collection) shows its date alone. A row whose featured card is incomplete renders nothing. In the admin the listed resources are edited with the kit items list (ITEMS_FIELD): each closed row reads as its heading with its image beside it and its date, and the list has no minimum. An open row is the card’s own fields, tags and link picker included. The featured card is one resource, not a list, and stays a group.
Links. A card’s link is a destination without a label of its own: the card’s heading is the link text. Pick a published page, or paste a URL for a PDF or another site. A page link that no longer resolves (the page was unpublished) leaves the card in place, unlinked.
Tags come from taxonomies. createResourcesBlock({ facets }) takes the collections a card is tagged on, in badge order. Each facet is { name, label, collection } and becomes facetField(facet) from @bison-lab/payload-core: a stock relationship, hasMany, on the featured card and on every listed card. The same helper builds the FAQ Library’s organizeBy fields, so a FAQ entry works here as it stands. Editors pick existing terms. A user with create access on the facet’s collection also sees Payload’s Add new, which opens the stock drawer; everyone else can only pick. There is no Categories collection: pass the taxonomies the site already has (Spinal passes Topics and Anatomy). Name a facet after its taxonomy; a name a card already uses (header, date, link, categories, …) throws when the config loads.
Badges. A badge is the populated term’s name (or label, as an anatomy term may carry). Read the page at a depth that populates the facet fields: a bare id renders no badge, and does not throw. Badges render in the library’s default tone. A site that wants a color per term registers resourcesBlockRenderer({ categoryStyles }) (from @bison-lab/payload-blocks/react, called in a 'use client' module) in place of ResourcesBlockRenderer, keyed by the term’s slug; a term without a slug takes the default tone. Colors stay in code.
Text categories (deprecated). ResourcesBlock is the earlier config: tags typed as free text on each card (categories), which drift (“Research”, “research”, “Studies”). It still works for a site with no taxonomy, and the renderer still reads categories on any row, keyed in categoryStyles by label, so a site can switch to createResourcesBlock before its migration has moved the old tags. After the switch, run payload generate:types and payload migrate:create.
Cards from a collection. Pass feed and the editor stops retyping articles. createResourcesBlock({ facets, feed }) adds a Card source with three choices: Latest (the newest published rows of feed.collection), Choose (rows picked by hand, in order) and Write cards (the fields above). New rows start on Latest; a row saved before the feed has no source and keeps its written cards. Latest filters by whatever the site passes, so a site without an anatomy taxonomy simply leaves it out:
createResourcesBlock({ facets: [TOPICS_FACET, ANATOMY_FACET], feed: { collection: "news", label: "Latest news", noun: { singular: "article", plural: "articles" }, facets: [TOPICS_FACET, ANATOMY_FACET], // one chip row each; omit for a date-only feed selects: [{ name: "kind", label: "Type", options: NEWS_KINDS }], date: "publishedAt", // turns on Published within and Order viewAll: { label: "View all news", href: "/news" }, maxCards: 12, },});Each filter is drawn as chips (FEED_FILTER_FIELD) led by an All … chip, which is on while nothing is ticked: an empty filter is no filter. Inside one filter any ticked term matches; every narrowed filter must match. Under the filters, FEED_MATCHES_FIELD lists the published rows the settings match as they are typed, names every filter in one line, and warns when nothing matches (the band then renders nothing on the page). Only published rows feed a card, in draft mode too. A feed facet queries the collection field of the same name; give it field when the collection names its relationship differently ({ ...ANATOMY_FACET, field: "anatomyTerms" }). A filter named after a feed setting (within, order, limit, viewAll, matches) throws when the config loads. Pass drafts: false for a collection without drafts. Run payload generate:types, payload migrate:create and payload generate:importmap after adding a feed.
The renderers cannot query, so the site fills the cards where it reads the page, on the server, before RenderBlocks:
import { resolveResourcesFeeds } from "@bison-lab/payload-blocks";
page.layout = await resolveResourcesFeeds(payload, page.layout, { feed: NEWS_FEED, toCard: (doc) => ({ header: doc.title, subtext: doc.excerpt, date: formatDate(doc.publishedAt), image: doc.cover, topics: doc.topics, link: { type: "external", href: newsPath(doc.slug) }, }),});toCard is the site’s: only it knows the collection’s fields and where a row lives. It returns a card in the written card’s shape, facet arrays included, so the badges and the renderer are the same for all three sources. The first card is the large one; the view-all link renders beside the heading through the site’s linkComponent when the feed has one and the row leaves it on.
Card style. cardVariant switches every card between the image above the text and the text over the image. Over an image, the renderer always lays a dark scrim under the white text, so a light photo stays legible; editors have no control for it.
Block library. resources is not in the package catalogue of Block library. A site that uses the library lists it as an extra, createBlockLibrary({ blocks: [{ slug: "resources", label: "Resources", category: "content", sample: blockSamples.resources }] }), so it gets a card and an on/off switch.
import { createResourcesBlock } from "@bison-lab/payload-blocks";
// In the collection's `blocks` array:createResourcesBlock({ facets: [ { name: "topics", label: "Topics", collection: "topics" }, { name: "anatomy", label: "Anatomy", collection: "anatomy_terms" }, ],})import { ResourcesBlockRenderer } from "@bison-lab/payload-blocks/react";
// In the site's registry:resources: ResourcesBlockRenderer,Fields
Section titled “Fields”| Field | Type | Notes |
|---|---|---|
eyebrow | text | Small label above the heading. Leave empty to hide the row. |
title * | text | |
description | textarea | |
featured | group | Labelled “Featured resource”. |
featured.header * | text | Labelled “Heading”. |
featured.subtext | textarea | Labelled “Summary”. |
featured.image | upload | Optional. Cards without an image show a muted panel. From the media collection. |
featured.date * | text | As it should read on the card: "March 2026", "Updated 4 Feb 2026". |
featured.author | group | |
featured.author.name * | text | |
featured.author.avatar | upload | Optional. Initials from the name are used when empty. From the media collection. |
featured.topics | relationship | Labelled “Topics”. Relationship to topics. |
featured.anatomy | relationship | Labelled “Industry”. Relationship to anatomy_terms. |
featured.link | group | Optional. Makes the whole card a link: a page, a document, or any URL. |
featured.link.type | radio | Labelled “Link”. One of page, external. Default "page". |
featured.link.page | relationship | Relationship to pages. |
featured.link.href | text | Labelled “URL”. A full URL ("https://example.com"), "mailto:" or "tel:", or a site path ("/contact"). |
featured.link.newTab | checkbox | Labelled “Open in a new tab”. Default false. |
resources | array | Listed beside the featured resource. |
resources[].header * | text | Labelled “Heading”. |
resources[].subtext | textarea | Labelled “Summary”. |
resources[].image | upload | Optional. Cards without an image show a muted panel. From the media collection. |
resources[].date * | text | As it should read on the card: "March 2026", "Updated 4 Feb 2026". |
resources[].author | group | |
resources[].author.name * | text | |
resources[].author.avatar | upload | Optional. Initials from the name are used when empty. From the media collection. |
resources[].topics | relationship | Labelled “Topics”. Relationship to topics. |
resources[].anatomy | relationship | Labelled “Industry”. Relationship to anatomy_terms. |
resources[].link | group | Optional. Makes the whole card a link: a page, a document, or any URL. |
resources[].link.type | radio | Labelled “Link”. One of page, external. Default "page". |
resources[].link.page | relationship | Relationship to pages. |
resources[].link.href | text | Labelled “URL”. A full URL ("https://example.com"), "mailto:" or "tel:", or a site path ("/contact"). |
resources[].link.newTab | checkbox | Labelled “Open in a new tab”. Default false. |
cardVariant | select | Labelled “Card style”. One of default, overlay. Default "default". |
settings | group | Labelled “Block Settings”. |
settings.background | select | Labelled “Background”. The surface this section sits on. Page is the plain page background. One of page, card, muted, color, gradient. Default "page". |
settings.backgroundColor | text | Labelled “Color”. |
settings.backgroundColorDark | text | Labelled “Dark mode color”. |
settings.gradientTo | text | Labelled “Gradient end”. |
settings.gradientToDark | text | Labelled “Dark mode gradient end”. |
settings.gradientDirection | text | Labelled “Gradient direction”. |
settings.spacing | select | Labelled “Spacing”. The air above and below this section. Default is the block's own padding; Compact and Spacious replace it. This is the band, not the site-wide spacing in Theme → Appearance. One of compact, default, spacious. Default "default". |
settings.anchor | text | Labelled “Anchor”. In-page links can target this section. Leave blank for none. |
settings.hidden | checkbox | Labelled “Hidden”. The section stays in the editor and is left off the page. Default false. |
* required.