Skip to Content
GuidesUsing Foir Renderer

Using Foir Renderer

Foir Renderer is a React component library for rendering CMS content. It takes content from Foir’s API and displays it using your custom components.

Installation

npm install @eide/foir-renderer # Peer dependencies npm install react react-dom

Quick Start

1. Create a Component Registry

Map your entity types to React components:

// lib/foir-registry.ts import { createRegistry, DEFAULT_PRIMITIVES } from '@eide/foir-renderer'; import { PageComponent } from '@/components/foir/Page'; import { HeroBannerComponent } from '@/components/foir/HeroBanner'; export const registry = createRegistry({ components: { ...DEFAULT_PRIMITIVES, // Built-in renderers for text, images, etc. 'page': PageComponent, // Your page component 'hero-banner': HeroBannerComponent, }, });

2. Create Your Components

// components/foir/Page.tsx import type { EntityComponentProps } from '@eide/foir-renderer'; export function PageComponent({ zones, fields, breadcrumbs, }: EntityComponentProps) { const title = fields.find(f => f.key === 'title'); return ( <article> <nav> {breadcrumbs?.map(crumb => ( <a key={crumb.url} href={crumb.url}>{crumb.label}</a> ))} </nav> {title && <h1>{String(title.value)}</h1>} {zones.header} <main>{zones.main}</main> {zones.footer} </article> ); }

3. Fetch and Render Content

// app/[...slug]/page.tsx (Next.js) import { EntityRenderer } from '@eide/foir-renderer'; import { registry } from '@/lib/foir-registry'; async function getPageContent(path: string) { const res = await fetch(`${process.env.FOIR_API_URL}/graphql`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.FOIR_API_KEY!, }, body: JSON.stringify({ query: ` query ResolveRoute($path: String!) { resolveRoute(path: $path) { record { id modelKey naturalKey } variant { id variantKey } content { fields { key type value label layout } } breadcrumbs { label url } gridConfig { breakpoints cols rowHeight } } } `, variables: { path }, }), }); const { data } = await res.json(); return data.resolveRoute; } export default async function Page({ params }: { params: { slug?: string[] } }) { const path = '/' + (params.slug?.join('/') || ''); const route = await getPageContent(path); if (!route) return <div>Page not found</div>; return <EntityRenderer data={route} registry={registry} />; }

Component Types

Entity Components

For top-level content like pages:

interface EntityComponentProps { zones: Record<string, ReactNode>; // Pre-rendered content zones fields: ResolvedField[]; // All fields with values record: { id, modelKey, naturalKey }; variant: { id, variantKey }; breadcrumbs: Array<{ label, url }>; breakpoint: 'xs' | 'sm' | 'md' | 'lg'; } function PageComponent({ zones, fields, breadcrumbs }: EntityComponentProps) { return ( <main> {zones.header} {zones.main} {zones.footer} </main> ); }

Inline Components

For embedded content like hero banners, cards, etc.:

interface InlineComponentProps<T> { data: T; // Field values breakpoint: 'xs' | 'sm' | 'md' | 'lg'; children?: ReactNode; } interface HeroBannerData { heading: string; subheading?: string; image?: { url: string; alt?: string }; ctaText?: string; ctaUrl?: string; } function HeroBannerComponent({ data, breakpoint }: InlineComponentProps<HeroBannerData>) { return ( <section className={breakpoint === 'xs' ? 'hero--mobile' : 'hero'}> {data.image && <img src={data.image.url} alt={data.image.alt || ''} />} <h1>{data.heading}</h1> {data.subheading && <p>{data.subheading}</p>} {data.ctaText && data.ctaUrl && ( <a href={data.ctaUrl}>{data.ctaText}</a> )} </section> ); }

Built-in Primitives

Foir Renderer includes default renderers for common field types:

import { DEFAULT_PRIMITIVES } from '@eide/foir-renderer'; // Includes: // - TextRenderer // - RichTextRenderer // - ImageRenderer // - VideoRenderer const registry = createRegistry({ components: { ...DEFAULT_PRIMITIVES, // Your custom components 'page': PageComponent, }, });

Override any primitive with your own:

const registry = createRegistry({ components: { ...DEFAULT_PRIMITIVES, 'image': MyCustomImageRenderer, // Override the default 'page': PageComponent, }, });

Image Handling

import { ImageRenderer, getImageVariantUrl } from '@eide/foir-renderer'; // Use the built-in renderer <ImageRenderer value={imageField} size="large" /> // Or get URLs for custom rendering const url = getImageVariantUrl(imageField, 'medium'); // sizes: thumbnail, small, medium, large, xlarge, original

Responsive Breakpoints

Detect the current breakpoint:

import { useBreakpoint } from '@eide/foir-renderer'; function MyComponent({ gridConfig }) { const breakpoint = useBreakpoint(gridConfig); return ( <div> {breakpoint === 'xs' ? <MobileNav /> : <DesktopNav />} </div> ); }

Framework Examples

Next.js App Router

// app/[...slug]/page.tsx import { EntityRenderer } from '@eide/foir-renderer'; import { registry } from '@/lib/foir-registry'; import { getRouteData } from '@/lib/foir-client'; export default async function Page({ params }: { params: { slug?: string[] } }) { const path = '/' + (params.slug?.join('/') || ''); const route = await getRouteData(path); return <EntityRenderer data={route} registry={registry} />; }

Remix

// app/routes/($slug).tsx import { json, type LoaderFunctionArgs } from '@remix-run/node'; import { useLoaderData } from '@remix-run/react'; import { EntityRenderer } from '@eide/foir-renderer'; import { registry } from '~/lib/foir-registry'; export async function loader({ params }: LoaderFunctionArgs) { const route = await getRouteData('/' + (params.slug || '')); return json({ route }); } export default function Page() { const { route } = useLoaderData<typeof loader>(); return <EntityRenderer data={route} registry={registry} />; }

Troubleshooting

”Unknown component type”

Register all entity types used in your content:

const registry = createRegistry({ components: { 'page': PageComponent, 'hero-banner': HeroBannerComponent, // Don't forget inline types! }, fallback: ({ data }) => <pre>{JSON.stringify(data, null, 2)}</pre>, });

“Cannot read property of undefined”

Always handle missing data:

function HeroBannerComponent({ data }: InlineComponentProps) { return ( <section> {data?.heading && <h1>{data.heading}</h1>} {data?.image && <img src={data.image.url} alt="" />} </section> ); }

Empty zones

Make sure your page component renders zones:

function PageComponent({ zones }: EntityComponentProps) { return ( <main> {zones.header} {/* Render each zone */} {zones.main} {zones.footer} </main> ); }

Next Steps

Last updated on