--- title: Generate pages from a visual template and external data category: Integrate external data order: 3 status: published summary: Create one visually editable template page in React Bricks and generate many dynamic pages by passing args into getExternalData. estimatedTime: 12 min keywords: external data getexternaldata args getexternaldataargs fetchpage template ecommerce dynamic pages --- 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](https://docs.reactbricks.com/common-tasks/get-data-from-external-apis/#fetch-external-data-in-pages) ## 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](/how-tos/page-types/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: - [Next.js Commerce with React Bricks](https://nextjs-commerce.reactbricks.com/) - [Composable E-commerce with React Bricks](/solutions/headless-commerce-cms) ## 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. ```tsx 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 { 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: ```tsx 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](https://docs.reactbricks.com/page-types/#properties-definition) ```tsx {9-14} 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: ```tsx getExternalData?: (page: Page, args?: any) => Promise ``` 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: ```tsx 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](https://docs.reactbricks.com/bricks/schema/connect-external-apis/#use-data-fetched-at-page-level) ### `PokemonHeader/PokemonHeader.tsx` ```tsx {79-81} 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 = ({ bgColor, id, name, height, weight, imageUrl, types, }) => { if (!id || !name || !height || !weight || !imageUrl) { return null } return (
#{id}

{name}

    {types?.map((type) => (
  • {type}
  • ))}
) } 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](/images/how-tos/integrate-external-data/pokemon/pokemon-header.png) ### `PokemonDetails/PokemonDetails.tsx` ```tsx {108-110} 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 = ({ heightTitle, weightTitle, abilitiesTitle, bgColor, height, weight, abilities, }) => { return (
(

{children}

)} />
{height} m
(

{children}

)} />
    {abilities?.map((ability) => (
  • {ability}
  • ))}
(

{children}

)} /> {weight} Kg
) } 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](/images/how-tos/integrate-external-data/pokemon/pokemon-details.png) ### `PokemonMoves/PokemonMoves.tsx` ```tsx {62-64} 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 = ({ title, badgeColor, maxNumber, moves, }) => { return (
(

{children}

)} />
    {moves?.slice(0, maxNumber).map((move) => (
  • {move}
  • ))}
) } 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](/images/how-tos/integrate-external-data/pokemon/pokemon-moves.png) ### `PokemonStats/PokemonStats.tsx` ```tsx {90-92} 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 = { 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` ```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 (
(

{children}

)} />
} />
) } ``` ![Pokemon Stats brick](/images/how-tos/integrate-external-data/pokemon/pokemon-stats.png) 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](/images/how-tos/integrate-external-data/pokemon/editing-pokemon-template.png) 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: ```tsx slug: 'pokemon-template' ``` but it passes a different runtime argument with: ```tsx getExternalDataArgs: { name: slug } ``` That argument is then received by the page type `getExternalData(page, args)`. ```tsx {43} 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 { 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 && } {pageOk && !errorPage && !errorNoKeys && ( )} {errorNoKeys && } {errorPage && } {pageOk && config && ( )} ) } ``` This line is the key: ```tsx 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](/images/how-tos/integrate-external-data/pokemon/frontend-pokemon-example.png) 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