FAQ columns
Renders FAQColumnsBlock from @bison-lab/ui. The eyebrow, title and description are the trio every marketing band opens with; they are top-level fields rather than a group so the heading is the first thing an editor sees.
Answers are plain text on purpose. A Lexical answer would pull @payloadcms/richtext-lexical into the ./react entry every consumer loads. Prose with links and lists belongs in a rich text block.
In the admin the questions are edited with the kit items list (ITEMS_FIELD): each closed row reads as its question with the opening of its answer beside it, and the last question cannot be deleted.
The call to action under the intro is hidden when its link is empty.
Questions from the FAQ Library
Section titled “Questions from the FAQ Library”createFaqColumnsBlock({ library }) puts the FAQ Library (createQuestions) behind the band, so an editor stops retyping the same answers. It adds Questions from ahead of the written questions, drawn with the same cards as the Resources card source:
- Choose questions (
picked): library questions in the editor’s order. A draft can be picked and stays off the page until it is published. - Match tags (
match): published questions carrying any ticked tag in any row. Matching is loose on purpose; leave every row on All for every published question.order(library order, A to Z, newest) andlimit(default 8, up to 12). - Write it here (
items): the fields above. Each question gains Save to FAQ Library, which files it as a draft library question on Save (never on autosave) and links the row to it.
A new band opens on Choose; a band saved before the library has no source and keeps its written questions.
import { createFaqColumnsBlock, type FaqLibrary } from "@bison-lab/payload-blocks";import { saveFaqsToLibrary } from "@bison-lab/payload-blocks/hooks";
export const FAQ_LIBRARY: FaqLibrary = { // The library's own Organize by, one chip row each; omit for none. facets: [ANATOMY_FACET, { name: "products", label: "Products", collection: "products" }, TOPICS_FACET],};
const pages = createPages({ blocks: [createFaqColumnsBlock({ library: FAQ_LIBRARY }) /* … */] });
export const Pages = { ...pages, hooks: { ...pages.hooks, beforeChange: [...(pages.hooks?.beforeChange ?? []), saveFaqsToLibrary(FAQ_LIBRARY)], },};The renderer cannot query, so the site fills Choose and Match tags bands where it reads the page, then builds the page’s FAQPage structured data from exactly the questions it shows (Google’s rule; a hidden band and a repeated question are left out):
import { faqPageJsonLd, resolveFaqLibrary } from "@bison-lab/payload-blocks";
page.layout = await resolveFaqLibrary(payload, page.layout, FAQ_LIBRARY);const faqJsonLd = faqPageJsonLd(page.layout, sectionControls);Library answers are Lexical; the band shows them as plain paragraphs (answerParagraphs). Run payload generate:types and payload migrate:create after switching to the factory.
import { FaqColumnsBlock } from "@bison-lab/payload-blocks";
// In the collection's `blocks` array:FaqColumnsBlockimport { FaqColumnsBlockRenderer } from "@bison-lab/payload-blocks/react";
// In the site's registry:faqColumns: FaqColumnsBlockRenderer,Fields
Section titled “Fields”| Field | Type | Notes |
|---|---|---|
eyebrow | text | Small label above the heading. Leave empty to hide the row. |
title * | text | |
description | textarea | |
items * | array | At least 1 row. Opens with 1 empty row; the admin field will not remove below that. |
items[].question * | text | |
items[].answer * | textarea | |
cta | group | Labelled “Call to action”. Shown under the intro. Leave the link empty to hide it. |
cta.title | text | Lead-in line, e.g. "Still have questions?". |
cta.linkText | text | |
cta.type | radio | Labelled “Link”. One of page, external. Default "page". |
cta.page | relationship | Relationship to pages. |
cta.href | text | Labelled “URL”. A full URL ("https://example.com"), "mailto:" or "tel:", or a site path ("/contact"). |
cta.newTab | checkbox | Labelled “Open in a new tab”. Default false. |
sticky | checkbox | Pin the intro column while the answers scroll. Default true. |
type | select | One of single, multiple. Default "single". |
collapsible | checkbox | Allow the open answer to be closed again. Single mode only. Default true. |
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.