# How to Embed Structured Content Records in PageBuilder

Source: https://dev.arcxp.com/custom-content/how-to-guides/how-to-embed-structured-content-records-in-pagebuilder/

Once you’ve defined a schema and editors have started adding records, the next question is always the same. How do you actually get those records on a page? Arc Themes ships content source blocks that handle the data-fetching side, so you don’t need to write your own. This guide walks through adding those blocks and building the front-end feature components that render what they return. Arc Themes doesn’t ship a display block for structured content, so that part is still on you.

## What you’ll build

Two content sources (provided by Arc Themes) and two PageBuilder feature blocks you’ll write yourself:

- A **list** view of structured content records (event listing, recipe index, staff directory)
- A **single record** in full detail (event detail page, recipe card, profile page)

Events are used as the running example, but nothing here is event-specific. Swap the field names and the same pattern works for any schema you’ve defined in Arc XP.

### Prerequisites

- You have at least one structured content type schema defined in Arc XP Data Manager or via the Draft API.
- Records exist in that schema and are published and circulated to your site.
- You have a PageBuilder bundle set up locally with the Arc Themes starter. See [How to Run PageBuilder Engine Locally](https://docs.arcxp.com) for initial setup.
- Your bundle uses Arc Themes 4.2.0 or above. The content source blocks in this guide ship with Themes and aren’t available in earlier versions.
- Your bundle has a `.env` file with:

```
CONTENT_BASE=https://api.sandbox.{org}.arcpublishing.com
ARC_ACCESS_TOKEN=<your-access-token>
```

## Step 1: Add the content source blocks

Arc Themes blocks aren’t npm dependencies, so there’s nothing to add to `package.json`. Add both blocks to the `blocks` array in your bundle’s `blocks.json`, and make sure `themesReleaseVersion` is 4.2.0 or above:

```js
// blocks.json
{
  "themesReleaseVersion": "4.2.0",
  "blocks": [
    "@wpmedia/schema-feed-content-source-block",
    "@wpmedia/schema-item-content-source-block"
  ]
}
```

Restart `npx fusion start`. Each block registers a content source under a name derived from its source file: `schema-feed` for the list block, `schema-item` for the single-record block. Those are the names you’ll select in PageBuilder and reference in your feature blocks.

### schema-feed: list of records

| Parameter   | Required | Description                                                                                                                             |
| ----------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Schema Name | Yes      | Exact schema name from Data Manager (e.g., events)                                                                                      |
| Sort By     | No       | first\_publish\_date (default) or publish\_date. Sort order is always descending.                                                       |
| Query       | No       | An ArcSL filter expression against schema\_content fields (e.g., schema\_content.location:"City Park"). Not a free-text keyword search. |
| Size        | No       | Records per page, default 25                                                                                                            |
| Page        | No       | Page number, default 1                                                                                                                  |

Calls `GET /content/v5/search/schemas/` and returns an array of record objects in `data`:

```json
{
  "data": [
    {
      "document_id": "abc123",
      "schema_name": "events",
      "schema_content": {
        "title": "Summer Block Party",
        "dt_start": "2026-07-04T18:00:00Z",
        "location": "City Park",
        ...
      }
    }
  ]
}
```

Field values live under `item.schema_content`, not at the top level.

### schema-item: single record

| Parameter   | Required | Description                             |
| ----------- | -------- | --------------------------------------- |
| Document ID | Yes      | The document\_id of the record to fetch |
| Schema Name | Yes      | Exact schema name from Data Manager     |

Calls `GET /content/v5/search/schemas/by-id/` and returns a single object in `data`, not an array:

```json
{
  "data": {
    "document_id": "abc123",
    "schema_name": "events",
    "schema_content": {
      "title": "Summer Block Party",
      "dt_start": "2026-07-04T18:00:00Z",
      "location": "City Park",
      ...
    }
  }
}
```

Same nesting as the feed source: fields live under `data.schema_content`.

## Step 2: Build a feature block to render a list

Content sources are just the plumbing. Now for the part that actually shows up on the page. Feature components live in `components/features/`, and each one needs a file named `default.jsx` inside a folder named after your block.

Create `components/features/event-list/default.jsx`:

```js
import React from "react";
import PropTypes from "@arc-fusion/prop-types";
import { useContent } from "fusion:content";
import { useFusionContext } from "fusion:context";

const EventList = ({ customFields }) => {
  const { contentConfig, displayAmount } = customFields;
  const { arcSite } = useFusionContext();

  // useContent fetches data from the content source the editor selected.
  // Pass the source name and the query params (contentConfigValues) from
  // the contentConfig custom field.
  const content = useContent(
    contentConfig
      ? {
          source: contentConfig.contentService,
          query: {
            ...contentConfig.contentConfigValues,
            // Pass the schema name your records belong to.
            schemaName: "events",
          },
        }
      : {},
  );

  if (!contentConfig || !content?.data) return null;

  // The feed source returns an array. Slice it to the requested display amount.
  const records = content.data.slice(0, displayAmount || content.data.length);

  return (
    <ul className="event-list">
      {records.map((item) => {
        // Each item in the array has schema_content with your field values.
        const { title, dt_start, location, description } =
          item.schema_content || {};
        const documentId = item.document_id;

        return (
          <li key={documentId}>
            {title && <h2>{title}</h2>}
            {dt_start && <p>{new Date(dt_start).toLocaleDateString()}</p>}
            {location && <p>{location}</p>}
            {description && <p>{description}</p>}
          </li>
        );
      })}
    </ul>
  );
};

EventList.propTypes = {
  customFields: PropTypes.shape({
    // contentConfig renders the content source picker in PageBuilder.
    contentConfig: PropTypes.contentConfig().tag({
      group: "Configure Content",
      label: "Content Source",
    }),
    displayAmount: PropTypes.number.tag({
      label: "Number of records to display",
      defaultValue: 10,
      group: "Configure Content",
    }),
  }),
};

EventList.label = "Event List – My Block";

export default EventList;
```

**Key patterns**

`useContent({ source, query })` fetches data from the named content source at render time. The `source` value has to match the registered content source name, `schema-feed` here.

`contentConfig` is doing the heavy lifting for editors. It’s a special prop type that renders the content source picker in PageBuilder’s settings panel, so editors pick a source and fill in its params without ever seeing code. `contentConfig.contentService` gives you the selected source name, and `contentConfig.contentConfigValues` gives you whatever the editor typed into the params (`schemaName`, `size`, `query`, and so on).

## Step 3: Build a feature block to render a single record

A detail block has two ways to get its data. It can inherit `globalContent` if the template’s page-level content source is already set to `schema-item`, or it can configure its own content source independently. The component below supports both, controlled by a toggle.

Create `components/features/event-details/default.jsx`:

```js
import React from "react";
import PropTypes from "@arc-fusion/prop-types";
import { useContent } from "fusion:content";
import { useFusionContext } from "fusion:context";

const EventDetails = ({ customFields }) => {
  const { inheritGlobalContent, contentConfig } = customFields;
  const { globalContent } = useFusionContext();

  // If inheritGlobalContent is true, use the page's global content
  // (set at the template level). Otherwise fetch via block-level contentConfig.
  const customContent = useContent(
    contentConfig
      ? {
          source: contentConfig.contentService,
          query: contentConfig.contentConfigValues,
        }
      : {},
  );

  const showGlobalContent = inheritGlobalContent ?? !contentConfig;
  const content = showGlobalContent ? globalContent : customContent;

  // The item source returns data.schema_content directly.
  if (!content?.data?.schema_content) return null;

  const { title, dt_start, dt_end, location, description, organizer, url } =
    content.data.schema_content;

  return (
    <article className="event-details">
      {title && <h1>{title}</h1>}
      {dt_start && <p>Start: {new Date(dt_start).toLocaleString()}</p>}
      {dt_end && <p>End: {new Date(dt_end).toLocaleString()}</p>}
      {location && <p>Location: {location}</p>}
      {organizer && <p>Organizer: {organizer}</p>}
      {description && <p>{description}</p>}
      {url && <a href={url}>Event Link</a>}
    </article>
  );
};

EventDetails.propTypes = {
  customFields: PropTypes.shape({
    inheritGlobalContent: PropTypes.bool.tag({
      name: "Inherit from global content",
      defaultValue: false,
      description:
        "Use the page's global content instead of configuring a content source on this block.",
      group: "Configure Content",
    }),
    contentConfig: PropTypes.contentConfig().tag({
      group: "Configure Content",
      label: "Event Content Source",
    }),
  }),
};

EventDetails.label = "Event Details – My Block";

export default EventDetails;
```

`globalContent` **vs.** `useContent`

Use `globalContent` when the page-level content source is already set to `schema-item`. The block just inherits whatever record the page already fetched, so no extra network call. Use `useContent` via `contentConfig` when the block needs its own record independent of the page, or when you want editors to be able to configure the record per block instead of per page.

## Step 4: Registering the pieces

Your feature blocks need no separate registration step. Fusion picks up every `components/features/*/default.jsx` file automatically when you run `npx fusion start`.

The content source blocks are different. They’re Arc Themes blocks, not files you wrote, so they need the `blocks.json` entries from Step 1\. If a content source doesn’t show up in PageBuilder’s picker after adding it, confirm both blocks are in the `blocks` array, confirm `themesReleaseVersion` is 4.2.0 or above, and restart `npx fusion start`. The engine caches source definitions on startup.

## Step 5: Configure the block in PageBuilder

1. Start the local Fusion engine:

```
npx fusion start
```

1. Navigate to `http://localhost/pagebuilder/pages`.
2. Open or create a page template.
3. Drag your block (e.g., “Event List”) from the block picker into the layout.
4. In the block’s settings panel, find **Configure Content** → **Content Source**. Select `schema-feed` from the dropdown.
5. Fill in the content source parameters: **Schema Name** (the exact name from Data Manager, e.g., `events`), **Size** (records to fetch), **Sort By** (`first_publish_date` or `publish_date`, always descending), and **Query** (an ArcSL filter expression, not a keyword search; leave blank if you don’t need one).
6. Save and preview the page.

For a detail block using `schema-item`, set **Document ID** to an existing record’s ID from Data Manager instead.

## End-to-end pipeline

It’s easy to lose track of where data actually comes from once you’re three files deep in a block. Here’s the whole path, schema to published page:

```
Data Manager / Draft API
     │
     ▼
Schema definition + Records published and circulated in Arc XP
     │
     ▼
Content Source (@wpmedia/schema-feed-content-source-block / schema-item-content-source-block)
  └─ Calls /content/v5/search/schemas/ (or /by-id/)
  └─ Returns records with schema_content fields
     │
     ▼
Feature Block (event-list / event-details, you build these)
  └─ useContent() fetches from content source
  └─ Reads item.schema_content field values
  └─ Renders HTML
     │
     ▼
PageBuilder Template
  └─ Editor picks content source and sets params
  └─ Block renders on published page
```

## Troubleshooting

**Query filter not working**Only indexed fields can be used in the query parameter. Check your schema’s field definitions in Data Manager to confirm the field has `search_settings.retrieval` configured. See the [Developer Guide](https://dev.arcxp.com/custom-content/core-concepts/developer-guide/) for API search vs. editorial search.

**Custom block renders nothing**Nine times out of ten this is a schema name mismatch, so check `schemaName` against Data Manager first, case and all. After that, verify records actually exist for that schema and site, and confirm `CONTENT_BASE` and `ARC_ACCESS_TOKEN` are set in `.env`. If those all check out, open dev tools on the preview page and look at the network tab for the content source fetch. A 401 means your token expired or was never valid. A 404 means no published record matched on that site: the schema name or record ID is wrong, the record isn’t circulated to the site, the query matches nothing, or the page is past the last result.

**Content source doesn’t appear in PageBuilder’s picker**Confirm both blocks are listed in the `blocks` array in `blocks.json` and that `themesReleaseVersion` is 4.2.0 or above, then restart `npx fusion start`. The engine caches source definitions on startup, so a block you just added won’t show up until you restart. If you’re on a Themes version below 4.2.0, the content sources don’t exist yet, so upgrade first.

**`content.data` is undefined in the block**Usually means the fetch function returned an empty string, which happens when a required param like `schemaName` never made it through. Check the params are actually filled in on the PageBuilder settings panel. Also double check you’re reading the right shape: the feed endpoint hands back an array, so `content.data` is an array, while the by-ID endpoint hands back an object, so it’s `content.data.schema_content` you want, not `content.data` directly.
