Compose a record-backed catalog grid
Build a catalog as a grid container with a repeated card inside it. The card’s binding.repeat_over selects model records, binding.item_var gives each iteration a stable item name, and child content interpolates fields such as {{ item.name }}.
Prerequisite and zero-state
Section titled “Prerequisite and zero-state”- Create or look up the catalog model first and replace
<model-id>with its real per-tenant UUID. Publish the model schema and configure the public read policy required by the page. - Under a
publicAccessToken, if the server render did not supplyblock.runtime.records, asource: "model"repeat_overresolves to an empty list with no error. Include a visible empty state and verify that the published page server-renders records; a successful editor preview is not sufficient. - A per-record image must be a child
imageblock interpolatingcontent.src.record-listanddata-tablenever render animage_urlvalue as an<img>. - External per-record image origins require
trusted_image_origins. It is default-closed, andset_trusted_image_originsreplaces the complete allowlist rather than appending an origin.
The following tree has an outer grid and repeats one card container for each matching record. fields explicitly allows the interpolated values into each public runtime record. filter is part of the source and can be empty, as shown; replace it with the model’s supported field filter when narrowing the catalog.
{ "blocks": [ { "id": "catalog-section", "type": "container", "display": {"layout": "flex", "direction": "column", "gap": 24, "full_width": true}, "style": {"class": "mx-auto max-w-6xl px-6 py-12"}, "blocks": [ {"id": "catalog-heading", "type": "heading", "content": {"text": "Catalog", "level": "h1"}}, {"id": "catalog-empty-state", "type": "text", "content": {"text": "No catalog items are available yet."}, "style": {"class": "text-muted-foreground"}}, { "id": "catalog-grid", "type": "container", "display": {"layout": "grid", "columns": 3, "gap": 24, "full_width": true}, "blocks": [ { "id": "catalog-card", "type": "container", "display": {"layout": "flex", "direction": "column", "gap": 12}, "style": {"class": "rounded-lg border p-4"}, "binding": {"item_var": "item", "repeat_over": {"source": "model", "model_id": "<model-id>", "fields": ["name", "description", "image_url"], "page_size": 24, "sort": "name", "order": "asc", "filter": {}}}, "blocks": [ {"id": "catalog-image", "type": "image", "content": {"src": "{{ item.image_url }}", "alt": "{{ item.name }}"}, "display": {"fit": "cover"}, "style": {"class": "h-48 w-full rounded-md"}}, {"id": "catalog-name", "type": "heading", "content": {"text": "{{ item.name }}", "level": "h2"}}, {"id": "catalog-description", "type": "text", "content": {"text": "{{ item.description }}"}} ] } ] } ] } ]}Data block column formats
Section titled “Data block column formats”Use the repeated card when the catalog needs images or custom composition. If a compact textual list is enough, record-list.content.columns is a comma-separated string split on ,, for example "name,description"; it is not a JSON array. data-table normalises columns, so do not transfer the record-list format assumption to it.
The existing repeat_over catalog example in docs/page-builder-blocks.md demonstrates interpolation and an image child, but leaves out prerequisites, the public-token empty state, and the record-list/data-table columns-format distinction supplied here.
