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-domQuick 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, originalResponsive 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
- Using UniformGen - Generate types for your components
- Creating API Keys - Set up API authentication
- Core Concepts - Understand entities and variants
Last updated on