← Back to all how-tos

Integrate external data

Generate pages from a visual template and external data

Create one visually editable template page in React Bricks and generate many dynamic pages by passing args into getExternalData.

Estimated time: 12 minRaw Markdown

In this how-to, you'll learn a very powerful external-data pattern for React Bricks.

Instead of creating one React Bricks page for every external item, you create just one visually editable template page, and then reuse it to render many dynamic pages.

This is very useful when your real data already lives in an external system, such as:

  • an ecommerce platform with products
  • a booking or travel API
  • a PIM or ERP
  • a legacy backend

So instead of syncing thousands of pages into React Bricks, you keep the data in the external source and use React Bricks only for the visual template and editorial content.

This is a perfect use case for args in getExternalData.

Docs reference: Get data from external APIs

What we'll build

We'll create a Pokemon example:

  • a small API wrapper for the Pokemon API
  • a template page type with getExternalData(page, args)
  • 4 custom bricks that render a Pokemon details page
  • one visually editable pokemon-template page in React Bricks
  • one dynamic frontend route at /pokemon/[slug]

The important idea is this:

  • in React Bricks, we only create one entity page: pokemon-template
  • on the frontend, we can render /pokemon/pikachu, /pokemon/dragonite, and many more
  • each route passes a different name through getExternalDataArgs
  • the same React Bricks template is reused, but the external data changes

Here, "template" means a visually editable page reused for many dynamic routes. It is different from the template: slot API covered in Create a page template.

Why this pattern is useful

This approach is especially useful when you don't want to:

  • create one page per product or entity in React Bricks
  • keep those pages in sync with an external catalog
  • duplicate data that already exists elsewhere

Instead, editors work on one reusable page composition, and developers decide which external entity to render by passing args.

In an ecommerce project, this means:

  • product data still comes from the commerce backend
  • React Bricks controls the layout and editorial content around that data
  • product pages can be generated dynamically without managing one CMS page per product

For real-world commerce examples, see:

1. Create a Pokemon API wrapper

First, create a helper file such as react-bricks/pokemon.ts.

This file fetches a Pokemon from the public API and transforms the response into a simpler shape for our bricks.

type Pokemon = {
  id: number
  name: string
  types: { type: { name: string } }[]
  height: number
  weight: number
  imageUrl: string
  abilities: { ability: { name: string } }[]
  moves: { move: { name: string } }[]
  stats: { base_stat: number; stat: { name: string } }[]
}
 
async function fetchPokemon(name: string): Promise<Pokemon> {
  const response = await fetch(
    `https://pokeapi.co/api/v2/pokemon/${name || 'pikachu'}`
  )
 
  if (!response.ok) {
    throw new Error(response.statusText)
  }
 
  const pokemon = (await response.json()) as Pokemon
 
  return pokemon
}
 
type SimplifiedPokemon = {
  id: number
  name: string
  types: string[]
  height: number
  weight: number
  imageUrl: string
  abilities: string[]
  moves: string[]
  stats: { name: string; value: number }[]
}
 
export const getPokemon = async (name: string) => {
  const pokemon = await fetchPokemon(name)
 
  const myPokemon: SimplifiedPokemon = {
    id: pokemon.id,
    name: pokemon.name,
    types: pokemon.types.map((type) => type.type.name),
    height: pokemon.height / 10,
    weight: pokemon.weight / 10,
    imageUrl: `https://img.pokemondb.net/artwork/large/${pokemon.name}.jpg`,
    abilities: pokemon.abilities.map((ability) => ability.ability.name),
    moves: pokemon.moves.map((move) => move.move.name),
    stats: pokemon.stats.map((stat) => ({
      name: stat.stat.name,
      value: stat.base_stat,
    })),
  }
 
  return myPokemon
}

This gives us one clean function:

getPokemon(name)

which returns all the data needed by our bricks.

2. Create a template page type

Now create a page type that fetches external data with getExternalData.

The important part is that the function uses args.name.

Docs reference: Page Types

const pageTypes: types.IPageType[] = [
  // ...
  {
    name: 'template',
    pluralName: 'templates',
    defaultLocked: false,
    defaultStatus: types.PageStatus.Published,
    getDefaultContent: () => [],
    getExternalData: (page, args) => {
      if (!args) {
        return getPokemon('pikachu')
      }
      return getPokemon(args.name)
    },
    isEntity: true,
  },
  // ...
]

This means:

  • if no args are passed, the template uses pikachu
  • if args.name is passed, it loads that Pokemon instead

So the same React Bricks page can render different external entities depending on the runtime args.

The page type signature is:

getExternalData?: (page: Page, args?: any) => Promise<Props>

As in the previous guides:

  • page is the current page object
  • args is optional extra runtime data passed from fetchPage

In this pattern, args is usually the bridge between your dynamic route params and your external API call.

We also set:

isEntity: true

so this page type appears under the Entities tab in the editor. That makes sense because we are using it as a reusable template entity rather than a normal content page.

3. Create the Pokemon bricks

Now let's create the 4 bricks used by the visual template.

All of them receive the shared external data from the page type using mapExternalDataToProps.

Docs reference: Use data fetched at Page level

PokemonHeader/PokemonHeader.tsx

import Image from 'next/image'
import React from 'react'
import { types } from 'react-bricks/rsc'
 
import imgPreview from './pokemon_header.png'
 
interface PokemonHeaderProps {
  bgColor: types.IColor & { className: string }
  id: number
  name: string
  height: number
  weight: number
  imageUrl: string
  types: string[]
}
 
const PokemonHeader: types.Brick<PokemonHeaderProps> = ({
  bgColor,
  id,
  name,
  height,
  weight,
  imageUrl,
  types,
}) => {
  if (!id || !name || !height || !weight || !imageUrl) {
    return null
  }
  return (
    <section className={`py-8 ${bgColor?.className}`}>
      <div className="max-w-3xl mx-auto flex gap-x-12">
        <div
          className={`bg-white p-5 rounded-3xl outline-slate-500/5 ${
            bgColor?.color !== 'white' ? 'outline' : ''
          }`}
        >
          <Image
            alt=""
            src={imageUrl}
            width={500}
            height={500}
            className="w-72"
          />
        </div>
        <div className="flex-1">
          <div className="mt-12 text-lg text-pink-600 uppercase tracking-widest font-bold mb-2">
            #{id}
          </div>
          <h1 className="text-5xl text-gray-950 font-extrabold mb-6 capitalize">
            {name}
          </h1>
 
          <ul className="flex items-center flex-wrap gap-x-2">
            {types?.map((type) => (
              <li
                className="py-0.5 px-2 font-semibold rounded-lg bg-blue-200 text-blue-950 capitalize"
                key={type}
              >
                {type}
              </li>
            ))}
          </ul>
        </div>
      </div>
    </section>
  )
}
 
PokemonHeader.schema = {
  name: 'pokemon-header',
  label: 'Pokemon Header',
  previewImageUrl: imgPreview.src,
  getDefaultProps: () => ({
    bgColor: {
      color: 'white',
      className: 'bg-white',
    },
  }),
  mapExternalDataToProps: (externalData) => {
    return externalData
  },
  sideEditProps: [
    {
      name: 'bgColor',
      label: 'Background Color',
      type: types.SideEditPropType.Select,
      selectOptions: {
        display: types.OptionsDisplay.Color,
        options: [
          {
            value: {
              color: 'white',
              className: 'bg-white',
            },
            label: 'White',
          },
          {
            value: {
              color: '#f9fafb',
              className: 'bg-gray-50',
            },
            label: 'Gray',
          },
          {
            value: {
              color: '#f0f9ff',
              className: 'bg-blue-50',
            },
            label: 'Blue',
          },
          {
            value: {
              color: '#f7fee7',
              className: 'bg-lime-50',
            },
            label: 'Lime',
          },
        ],
      },
    },
  ],
}
 
export default PokemonHeader

Pokemon Header brick

PokemonDetails/PokemonDetails.tsx

import React from 'react'
import { types, Text } from 'react-bricks/rsc'
 
import imgPreview from './pokemon_details.png'
 
interface PokemonDetailsProps {
  heightTitle: types.TextValue
  weightTitle: types.TextValue
  abilitiesTitle: types.TextValue
  bgColor: types.IColor & { className: string }
  height: number
  weight: number
  abilities: string[]
}
 
const PokemonDetails: types.Brick<PokemonDetailsProps> = ({
  heightTitle,
  weightTitle,
  abilitiesTitle,
  bgColor,
  height,
  weight,
  abilities,
}) => {
  return (
    <section className={`py-8 ${bgColor?.className}`}>
      <div className="max-w-3xl mx-auto grid grid-cols-2 gap-6">
        <div
          className={`rounded-2xl p-6 border-blue-100 ${
            bgColor?.color === 'white' ? 'bg-blue-50' : 'bg-white border'
          }`}
        >
          <Text
            propName="heightTitle"
            value={heightTitle}
            placeholder="Title for Height..."
            renderBlock={({ children }) => (
              <h3 className="text-sm text-blue-900/70 uppercase tracking-widest font-bold mb-2">
                {children}
              </h3>
            )}
          />
          <div>
            <span className="text-3xl font-semibold">{height}</span> m
          </div>
        </div>
        <div
          className={`rounded-2xl p-6 border-blue-100 row-span-2 ${
            bgColor?.color === 'white' ? 'bg-blue-50' : 'bg-white border'
          }`}
        >
          <Text
            propName="abilitiesTitle"
            value={abilitiesTitle}
            placeholder="Type a title for types..."
            renderBlock={({ children }) => (
              <h3 className="text-sm text-blue-900/70 uppercase tracking-widest font-bold mb-2">
                {children}
              </h3>
            )}
          />
          <ul className="flex flex-col gap-y-2">
            {abilities?.map((ability) => (
              <li
                className="mt-2 py-2 px-4 text-lg font-semibold rounded-xl p-6 bg-blue-200 text-blue-950 capitalize"
                key={ability}
              >
                {ability}
              </li>
            ))}
          </ul>
        </div>
        <div
          className={`rounded-2xl p-6 border-blue-100 ${
            bgColor?.color === 'white' ? 'bg-blue-50' : 'bg-white border'
          }`}
        >
          <Text
            propName="weightTitle"
            value={weightTitle}
            placeholder="Title for Height..."
            renderBlock={({ children }) => (
              <h3 className="text-sm text-blue-900/70 uppercase tracking-widest font-bold mb-2">
                {children}
              </h3>
            )}
          />
          <span className="text-3xl font-semibold">{weight}</span> Kg
        </div>
      </div>
    </section>
  )
}
 
PokemonDetails.schema = {
  name: 'pokemon-details',
  label: 'Pokemon Details',
  previewImageUrl: imgPreview.src,
  getDefaultProps: () => ({
    heightTitle: 'Height',
    weightTitle: 'Weight',
    abilitiesTitle: 'Abilities',
    bgColor: {
      color: 'white',
      className: 'bg-white',
    },
  }),
  mapExternalDataToProps: (externalData) => {
    return externalData
  },
  sideEditProps: [
    {
      name: 'bgColor',
      label: 'Background Color',
      type: types.SideEditPropType.Select,
      selectOptions: {
        display: types.OptionsDisplay.Color,
        options: [
          {
            value: {
              color: 'white',
              className: 'bg-white',
            },
            label: 'White',
          },
          {
            value: {
              color: '#f0f9ff',
              className: 'bg-blue-50',
            },
            label: 'Blue',
          },
        ],
      },
    },
  ],
}
 
export default PokemonDetails

Pokemon Details brick

PokemonMoves/PokemonMoves.tsx

import React from 'react'
import { types, Text } from 'react-bricks/rsc'
 
import imgPreview from './pokemon_moves.png'
 
interface PokemonMovesProps {
  title: types.TextValue
  badgeColor: types.IColor & { className: string }
  maxNumber: number
  moves: string[]
}
 
const PokemonMoves: types.Brick<PokemonMovesProps> = ({
  title,
  badgeColor,
  maxNumber,
  moves,
}) => {
  return (
    <section className="py-8">
      <div className="max-w-3xl mx-auto">
        <Text
          propName="title"
          value={title}
          placeholder="Type a title..."
          renderBlock={({ children }) => (
            <h3 className="text-sm text-blue-900/70 uppercase tracking-widest font-bold mb-2">
              {children}
            </h3>
          )}
        />
 
        <div>
          <ul className="flex items-center flex-wrap gap-x-2">
            {moves?.slice(0, maxNumber).map((move) => (
              <li
                className={`mt-2 py-0.5 px-2 font-semibold rounded-lg ${badgeColor?.className}`}
                key={move}
              >
                {move}
              </li>
            ))}
          </ul>
        </div>
      </div>
    </section>
  )
}
 
PokemonMoves.schema = {
  name: 'pokemon-moves',
  label: 'Pokemon Moves',
  previewImageUrl: imgPreview.src,
  getDefaultProps: () => ({
    title: 'Moves',
    badgeColor: {
      color: 'blue',
      className: 'bg-blue-200 text-blue-950',
    },
    maxNumber: 200,
  }),
  mapExternalDataToProps: (externalData) => {
    return externalData
  },
  sideEditProps: [
    {
      name: 'badgeColor',
      label: 'Badges Color',
      type: types.SideEditPropType.Select,
      selectOptions: {
        display: types.OptionsDisplay.Color,
        options: [
          {
            value: {
              color: '#bfdbfe',
              className: 'bg-blue-200 text-blue-950',
            },
            label: 'Blue',
          },
          {
            value: {
              color: '#a5f3fc',
              className: 'bg-cyan-400/50 text-cyan-950',
            },
            label: 'Cyan',
          },
          {
            value: {
              color: '#fde68a',
              className: 'bg-amber-200 text-amber-950',
            },
            label: 'Amber',
          },
          {
            value: {
              color: '#f5d0fe',
              className: 'bg-fuchsia-200 text-fuchsia-950',
            },
            label: 'Fuchsia',
          },
        ],
      },
    },
    {
      name: 'maxNumber',
      label: 'Max number of moves',
      type: types.SideEditPropType.Range,
      rangeOptions: {
        min: 20,
        max: 200,
        step: 20,
      },
    },
  ],
}
 
export default PokemonMoves

Pokemon Moves brick

PokemonStats/PokemonStats.tsx

import { types, wrapClientComponent } from 'react-bricks/rsc'
import { RegisterComponent } from 'react-bricks/rsc/client'
 
import imgPreview from './pokemon_stats.png'
 
import {
  PokemonStatsClient,
  PokemonStatsClientProps,
} from './PokemonStatsClient'
 
const schema: types.IBlockType<PokemonStatsClientProps> = {
  name: 'pokemon-stats',
  label: 'Pokemon Stats',
  previewImageUrl: imgPreview.src,
  getDefaultProps: () => ({
    title: 'Stats',
    bgColor: {
      color: 'white',
      className: 'bg-white',
    },
    barColor: {
      color: '#0d9488',
      className: 'fill-teal-600',
    },
  }),
  sideEditProps: [
    {
      name: 'bgColor',
      label: 'Background Color',
      type: types.SideEditPropType.Select,
      selectOptions: {
        display: types.OptionsDisplay.Color,
        options: [
          {
            value: {
              color: 'white',
              className: 'bg-white',
            },
            label: 'White',
          },
          {
            value: {
              color: '#f0f9ff',
              className: 'bg-blue-50',
            },
            label: 'Blue',
          },
          {
            value: {
              color: '#000',
              className: 'bg-gray-950 dark',
            },
            label: 'Dark',
          },
        ],
      },
    },
    {
      name: 'barColor',
      label: 'Bar Color',
      type: types.SideEditPropType.Select,
      selectOptions: {
        display: types.OptionsDisplay.Color,
        options: [
          {
            value: {
              color: '#0d9488',
              className: 'fill-teal-700/80',
            },
            label: 'Teal',
          },
          {
            value: {
              color: '#0284c7',
              className: 'fill-sky-700/70',
            },
            label: 'Sky',
          },
          {
            value: {
              color: '#2563eb',
              className: 'fill-blue-700/70',
            },
            label: 'Blue',
          },
        ],
      },
    },
  ],
  mapExternalDataToProps: (externalData) => {
    return externalData
  },
}
 
export default wrapClientComponent({
  ClientComponent: PokemonStatsClient,
  RegisterComponent,
  schema,
})

PokemonStats/PokemonStatsClient.tsx

'use client'
 
import { useReactBricksContext, usePageValues } from 'react-bricks/rsc/client'
 
import { Bar, BarChart, LabelList, XAxis, YAxis } from 'recharts'
 
import {
  ChartConfig,
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
} from '@/components/ui/chart'
 
import { types, Text } from 'react-bricks/rsc'
 
const chartConfig = {} satisfies ChartConfig
 
export interface PokemonStatsClientProps {
  title: types.TextValue
  bgColor: types.IColor & { className: string }
  barColor: types.IColor & { className: string }
  stats: { name: string; value: number }[]
}
 
export function PokemonStatsClient({
  title,
  bgColor,
  barColor,
  stats,
}: PokemonStatsClientProps) {
  const { isRtlLanguage } = useReactBricksContext()
  const [page] = usePageValues()
  const isRtl = isRtlLanguage({ language: page.language })
 
  return (
    <section className={`py-8 ${bgColor?.className}`}>
      <div className="max-w-3xl mx-auto">
        <div className="rounded-2xl border border-blue-100 dark:border-blue-200/10 bg-white dark:bg-white/5">
          <div className="rounded-t-2xl px-6 py-3 bg-blue-500/5 dark:bg-blue-200/10">
            <Text
              propName="title"
              value={title}
              placeholder="Type a title..."
              renderBlock={({ children }) => (
                <h3 className="text-sm text-blue-900/70 dark:text-white/90 uppercase tracking-widest font-bold">
                  {children}
                </h3>
              )}
            />
          </div>
          <div className="px-6 py-6">
            <ChartContainer config={chartConfig} className="w-full aspect-[3]">
              <BarChart
                accessibilityLayer
                data={stats}
                layout="vertical"
                margin={{}}
              >
                <XAxis type="number" dataKey="value" hide reversed={isRtl} />
                <YAxis
                  dataKey="name"
                  type="category"
                  tickLine={false}
                  tickMargin={10}
                  axisLine={false}
                  className="text-base capitalize"
                  hide
                />
                <ChartTooltip
                  cursor={false}
                  content={<ChartTooltipContent hideLabel />}
                />
                <Bar dataKey="value" className={barColor?.className} radius={5}>
                  <LabelList
                    dataKey="name"
                    position="insideLeft"
                    offset={8}
                    className="fill-white capitalize"
                    fontSize={16}
                    fontWeight={600}
                  />
                  <LabelList
                    dataKey="value"
                    position="right"
                    offset={8}
                    className="fill-gray-700 dark:fill-white"
                    fontSize={14}
                  />
                </Bar>
              </BarChart>
            </ChartContainer>
          </div>
        </div>
      </div>
    </section>
  )
}

Pokemon Stats brick

These bricks show an important idea:

  • the visual structure and styling live in the brick
  • the actual Pokemon data is injected from the page-level external data
  • editors can still customize visual and textual parts such as titles, colors, and limits

4. Create the visual template page

Now go into the visual editor and create a page called pokemon-template under the template page type.

Then compose the page using these bricks, for example in this order:

  1. PokemonHeader
  2. PokemonDetails
  3. PokemonMoves
  4. PokemonStats

At this point, editors can fine-tune:

  • colors
  • titles
  • spacing and visual composition
  • how many moves to show

while the actual Pokemon data still comes from the API.

Here is what the template looks like in the editor:

Editing the Pokemon template

Notice the key benefit: editors are visually composing a reusable page pattern, not filling in one page per Pokemon.

5. Create the dynamic frontend page

Now create the dynamic route at app/[lang]/pokemon/[slug]/page.tsx.

This route always fetches the same React Bricks page:

slug: 'pokemon-template'

but it passes a different runtime argument with:

getExternalDataArgs: {
  name: slug
}

That argument is then received by the page type getExternalData(page, args).

import type { Metadata } from 'next'
import {
  JsonLd,
  PageViewer,
  cleanPage,
  fetchPage,
  getBricks,
  types,
} from 'react-bricks/rsc'
import { ClickToEdit } from 'react-bricks/rsc/client'
 
import ErrorNoKeys from '@/components/errorNoKeys'
import ErrorNoPage from '@/components/errorNoPage'
import config from '@/react-bricks/config'
import { getPokemon } from '@/react-bricks/pokemon'
 
const getData = async (
  slug: any,
  locale: string
): Promise<{
  page: types.Page | null
  errorNoKeys: boolean
  errorPage: boolean
}> => {
  let errorNoKeys = false
  let errorPage = false
 
  if (!config.apiKey) {
    errorNoKeys = true
 
    return {
      page: null,
      errorNoKeys,
      errorPage,
    }
  }
 
  const page = await fetchPage({
    slug: 'pokemon-template',
    language: locale,
    config,
    fetchOptions: { next: { revalidate: 3 } },
    getExternalDataArgs: { name: slug },
  }).catch(() => {
    errorPage = true
    return null
  })
 
  return {
    page,
    errorNoKeys,
    errorPage,
  }
}
 
export async function generateStaticParams({
  params,
}: {
  params: { lang: string }
}) {
  if (!config.apiKey) {
    return []
  }
 
  const pokemons = await fetch('https://pokeapi.co/api/v2/pokemon?limit=100')
    .then((response) => response.json())
    .then((data) => data.results)
 
  type PokemonFromList = {
    name: string
  }
 
  const pages = pokemons.map((pokemon: PokemonFromList) => ({
    slug: pokemon.name,
  }))
 
  return pages
}
 
export async function generateMetadata({
  params,
}: {
  params: { lang: string; slug?: string }
}): Promise<Metadata> {
  const pokemon = await getPokemon(params.slug || 'pikachu')
 
  return {
    title: pokemon.name,
    description: `Here you can find info about the Pokemon ${pokemon.name}`,
  }
}
 
export default async function Page({
  params,
}: {
  params: { lang: string; slug?: string }
}) {
  const { page, errorNoKeys, errorPage } = await getData(
    params.slug,
    params.lang
  )
 
  const bricks = getBricks()
  const pageOk = page ? cleanPage(page, config.pageTypes || [], bricks) : null
 
  return (
    <>
      {page?.meta && <JsonLd page={page}></JsonLd>}
      {pageOk && !errorPage && !errorNoKeys && (
        <PageViewer page={pageOk} main />
      )}
      {errorNoKeys && <ErrorNoKeys />}
      {errorPage && <ErrorNoPage />}
      {pageOk && config && (
        <ClickToEdit
          pageId={pageOk?.id}
          language={params.lang}
          editorPath={config.editorPath || '/admin/editor'}
          clickToEditSide={config.clickToEditSide}
        />
      )}
    </>
  )
}

This line is the key:

getExternalDataArgs: {
  name: slug
}

It means:

  • the route param is read from [slug]
  • fetchPage passes it into the page type as args
  • getExternalData(page, args) uses that arg to fetch the correct Pokemon

So /pokemon/pikachu and /pokemon/dragonite both use the same React Bricks template page, but each one gets different external data.

6. See the final result

Here is an example rendered page for Dragonite:

Pokemon frontend example

This page was not manually created in React Bricks as a separate page.

Instead:

  • the layout came from the visually edited pokemon-template
  • the data came from the external API
  • the route parameter selected which Pokemon to render

That is exactly the same pattern you can use in ecommerce for products, categories, or product variants.

Why getExternalDataArgs is the missing piece

The previous guides showed how external data can depend on:

  • brick props
  • page data

This guide adds a third dimension:

  • runtime route parameters

That is why args is so powerful.

With getExternalDataArgs, you can keep one reusable visual template in React Bricks, and still render many pages whose data depends on the current route.

From Pokemon to ecommerce

The Pokemon example is just a fun stand-in for a product catalog.

In a real ecommerce site, you could:

  • create one product-template page in React Bricks
  • visually compose product-specific sections with reusable bricks
  • read the current product slug from the route
  • pass that slug as getExternalDataArgs
  • fetch the product from Shopify, Commerce Layer, BigCommerce, or another commerce API

So the editors work on one product template, while the frontend generates a full product page for every product in the catalog.

This avoids:

  • duplicated page creation
  • fragile synchronization between CMS pages and commerce data
  • maintaining thousands of nearly identical content pages

Recap

With this pattern:

  • React Bricks owns the visual template
  • the external system owns the entity data
  • the frontend route decides which entity to render
  • getExternalDataArgs passes that decision into getExternalData

That gives you the best of both worlds:

  • visual editing for marketers
  • dynamic, scalable pages for developers
  • no need to create one CMS page per external entity