{zones.header}
{zones.main}
{zones.footer}
React component library for rendering Foir CMS content with type-safe component mapping
npm install @eide/foir-rendererReact component library for rendering Foir CMS content with type-safe component mapping.
- EntityRenderer - Renders resolved route data with automatic zone handling
- Component Registry - Type-safe mapping of entity models to React components
- Zone Rendering - Automatic layout and positioning for content zones
- Field Rendering - Built-in primitives for text, images, video, and rich text
- Responsive Grid - Breakpoint-aware layouts from your design system
- TypeScript First - Full type safety with exported types for all props
``bash
npm install @eide/foir-renderer
Quick Start
`tsx
import { EntityRenderer, createRegistry } from '@eide/foir-renderer';// 1. Create components for your entity models
function PageComponent({ zones, fields, record }) {
return (
{record.naturalKey}
{zones.header}
{zones.main}
{zones.footer}
);
}
function HeroBanner({ value }) {
return (
{value.heading}
{value.subheading}
);
}
// 2. Create a registry mapping models to components
const registry = createRegistry({
components: {
'page': PageComponent,
'hero-banner': HeroBanner,
'product-card': ProductCard,
},
});
// 3. Render your content
function App({ routeData }) {
return (
data={routeData}
registry={registry}
/>
);
}
`Core Components
$3
The main entry point for rendering Foir CMS content. Takes resolved route data and renders it using your component registry.
`tsx
import { EntityRenderer } from '@eide/foir-renderer'; data={resolvedRoute} // From resolveRoute query
registry={registry} // Your component registry
gridConfig={customGrid} // Optional: override grid settings
breakpoint="desktop" // Optional: override breakpoint detection
className="my-page" // Optional: wrapper class
/>
`Props:
| Prop | Type | Description |
|------|------|-------------|
|
data | ResolvedRoute | Resolved route data from your GraphQL query |
| registry | Registry | Component registry created with createRegistry |
| gridConfig | GridConfig | Optional grid override (defaults to design system config) |
| breakpoint | Breakpoint | Optional breakpoint override (auto-detected if not set) |
| className | string | Optional CSS class for the wrapper element |$3
Renders a content zone with its blocks positioned according to layout settings.
`tsx
import { ZoneRenderer } from '@eide/foir-renderer'; zone={zoneValue}
registry={registry}
breakpoint="desktop"
/>
`$3
Renders individual fields with automatic type detection and component resolution.
`tsx
import { FieldRenderer } from '@eide/foir-renderer'; field={field}
registry={registry}
/>
`Component Registry
Create a type-safe registry that maps your entity model keys to React components.
`tsx
import { createRegistry } from '@eide/foir-renderer';const registry = createRegistry({
// Map model keys to components
components: {
// Entity components (for pages, records)
'page': PageComponent,
'blog-post': BlogPostComponent,
// Inline components (for blocks, nested content)
'hero-banner': HeroBanner,
'product-card': ProductCard,
'testimonial': Testimonial,
// Primitive overrides (optional)
'richtext': CustomRichText,
'image': CustomImage,
},
// Layout configuration (optional)
layout: {
defaultMode: 'auto',
},
// Wrapper components (optional)
wrappers: {
zone: ZoneWrapper,
field: FieldWrapper,
entity: EntityWrapper,
},
// Fallback component (optional)
fallback: FallbackComponent,
});
`Component Props
$3
Components registered for top-level entities receive these props:
`tsx
interface EntityComponentProps {
zones: Record; // Pre-rendered zone content
fields: ResolvedField[]; // All fields from content
record: ResolvedEntityRecord; // Entity record data
variant: ResolvedEntityVariant; // Active variant
template?: ResolvedTemplateRef; // Template reference
layoutMode?: 'flat' | 'tree'; // Editor mode
extraContent?: Record; // Additional resolved content
breadcrumbs?: Breadcrumb[]; // Navigation breadcrumbs
breakpoint: Breakpoint; // Current breakpoint
gridConfig: GridConfig; // Grid configuration
_raw: ResolvedRoute; // Full resolved route
}
`Example:
`tsx
function PageComponent({
zones,
fields,
record,
breadcrumbs,
breakpoint,
}: EntityComponentProps) {
return (
{zones.header}
{zones.main}
{zones.footer}
);
}
`$3
Components for blocks and nested content receive:
`tsx
interface InlineComponentProps {
value: Record; // Field values
fields: ResolvedField[]; // Field definitions
modelKey: string; // Entity model key
breakpoint: Breakpoint; // Current breakpoint
gridConfig: GridConfig; // Grid configuration
position?: LayoutPosition; // Position in zone
}
`Example:
`tsx
function HeroBanner({ value, breakpoint }: InlineComponentProps) {
return (
hero ${breakpoint}}>
{value.heading}
{value.subheading && {value.subheading}
}
{value.ctaText && (
{value.ctaText}
)}
);
}
`Built-in Primitives
The library includes default renderers for primitive field types:
`tsx
import {
TextRenderer,
RichTextRenderer,
ImageRenderer,
VideoRenderer,
DEFAULT_PRIMITIVES,
} from '@eide/foir-renderer';// Use defaults
const registry = createRegistry({
components: {
...DEFAULT_PRIMITIVES,
// Your custom components...
},
});
// Or use individually
`$3
Renders images with responsive variants and focal point support.
`tsx
import { ImageRenderer, getImageVariantUrl } from '@eide/foir-renderer';// As a component
value={imageField}
size="large"
className="hero-image"
/>
// Get URL directly
const url = getImageVariantUrl(imageField, 'medium');
`Available sizes:
thumbnail, small, medium, large, xlarge, originalHooks
$3
Detect current breakpoint based on grid configuration.
`tsx
import { useBreakpoint } from '@eide/foir-renderer';function MyComponent({ gridConfig }) {
const breakpoint = useBreakpoint(gridConfig);
return (
layout-${breakpoint}}>
{breakpoint === 'mobile' ? : }
);
}
`$3
Access renderer context from nested components.
`tsx
import { useRendererContext } from '@eide/foir-renderer';function NestedComponent() {
const { registry, breakpoint, gridConfig } = useRendererContext();
// Access context values
}
`Layouts
Built-in layout components for common patterns:
`tsx
import { GridLayout, ListLayout } from '@eide/foir-renderer';// CSS Grid-based layout
items={blocks}
columns={3}
gap="1rem"
/>
// Vertical list layout
items={blocks}
gap="2rem"
/>
`Type Guards
Utility functions for type checking resolved content:
`tsx
import {
isResolvedRichText,
isResolvedFile,
isZoneValue,
isZoneField,
isResolvedInlineEntity,
} from '@eide/foir-renderer';// Check field types
if (isZoneField(field)) {
// Handle zone field
}
if (isResolvedRichText(value)) {
// Handle rich text content
}
`TypeScript
All types are exported for use in your components:
`tsx
import type {
// Route & Content
ResolvedRoute,
ResolvedContent,
ResolvedField,
ResolvedEntityRecord,
ResolvedEntityVariant, // Values
ZoneValue,
ResolvedRichText,
ResolvedFile,
ResolvedInlineEntity,
// Props
EntityComponentProps,
InlineComponentProps,
PrimitiveComponentProps,
// Registry
Registry,
RegistryConfig,
// Layout
Breakpoint,
GridConfig,
LayoutPosition,
} from '@eide/foir-renderer';
`Framework Examples
$3
`tsx
// app/routes/($locale)._index.tsx
import { json, type LoaderFunctionArgs } from '@shopify/remix-oxygen';
import { useLoaderData } from '@remix-run/react';
import { EntityRenderer } from '@eide/foir-renderer';
import { registry } from '~/lib/foir-registry';export async function loader({ context }: LoaderFunctionArgs) {
const route = await context.foir.resolveRoute({ path: '/' });
return json({ route });
}
export default function Homepage() {
const { route } = useLoaderData();
return ;
}
`$3
`tsx
// app/[...slug]/page.tsx
import { EntityRenderer } from '@eide/foir-renderer';
import { foirClient } from '@/lib/foir';
import { registry } from '@/lib/registry';export default async function Page({
params,
}: {
params: { slug?: string[] };
}) {
const path = params.slug?.join('/') || '/';
const route = await foirClient.resolveRoute({ path });
return ;
}
``- React 18.0.0 or higher (React 19 supported)
- TypeScript 5.0+ recommended
For complete documentation, visit docs.foir.dev
Proprietary - All rights reserved.