pro
Rendering Layouts
Implement layout rendering in your frontend application with examples for popular frameworks.
- Overview
- React Implementation
- Vue 3 Implementation
- Next.js with Image Optimization
- Server Component Pattern
- PHP/Twig Implementation
- PHP Class
- Twig Template
- Astro Implementation
- Error Handling
- Development Mode Placeholder
- TypeScript Types
- Performance Tips
- Memoize Components
- Lazy Load Heavy Components
- Use Intersection Observer for Images
Overview
The layout API returns a tree structure of components that you need to render on your frontend. Each component has a component type, data object, and optionally children or columns for nested content.
React Implementation
// components/LayoutRenderer.jsx
import { marked } from 'marked';
const componentMap = {
heading: ({ data }) => {
const Tag = `h${data.level || 1}`;
return <Tag>{data.text}</Tag>;
},
richtext: ({ data }) => (
<div dangerouslySetInnerHTML={{ __html: data.html }} />
),
markdown: ({ data }) => (
<div dangerouslySetInnerHTML={{ __html: marked(data.markdown) }} />
),
image: ({ data }) => (
<figure>
<img src={data.image?.path} alt={data.caption || ''} />
{data.caption && <figcaption>{data.caption}</figcaption>}
</figure>
),
button: ({ data }) => (
<a href={data.url} target={data.target} className="button">
{data.caption}
</a>
),
section: ({ data, children }) => (
<section className={data.class}>
<LayoutRenderer components={children} />
</section>
),
grid: ({ data, columns }) => (
<div className={`grid ${data.class || ''}`}>
{columns?.map((col, i) => (
<div key={i} className="grid-column">
<LayoutRenderer components={col.components} />
</div>
))}
</div>
),
spacer: ({ data }) => (
<div style={{ height: `${(data.size || 1) * 1}rem` }} />
),
// Custom components
hero: ({ data }) => (
<section
className="hero"
style={{ backgroundImage: `url(${data.image?.path})` }}
>
<h1>{data.headline}</h1>
<p>{data.subheadline}</p>
{data.cta_url && (
<a href={data.cta_url} className="cta">{data.cta_text}</a>
)}
</section>
),
};
export function LayoutRenderer({ components = [] }) {
return (
<>
{components.map((item, index) => {
const Component = componentMap[item.component];
if (!Component) {
console.warn(`Unknown component: ${item.component}`);
return null;
}
return (
<Component
key={item.id || index}
data={item.data}
children={item.children}
columns={item.columns}
/>
);
})}
</>
);
}
// Usage in a page
export default function Page({ pageData }) {
return (
<main>
<LayoutRenderer components={pageData.layout} />
</main>
);
}
Vue 3 Implementation
<!-- components/LayoutRenderer.vue -->
<script setup>
import { h } from 'vue';
import { marked } from 'marked';
const props = defineProps({
components: { type: Array, default: () => [] }
});
const componentMap = {
heading: (item) => h(`h${item.data.level || 1}`, item.data.text),
richtext: (item) => h('div', { innerHTML: item.data.html }),
markdown: (item) => h('div', { innerHTML: marked(item.data.markdown) }),
image: (item) => h('figure', [
h('img', { src: item.data.image?.path, alt: item.data.caption }),
item.data.caption && h('figcaption', item.data.caption)
]),
section: (item) => h('section', { class: item.data.class }, [
h(LayoutRenderer, { components: item.children })
]),
grid: (item) => h('div', { class: `grid ${item.data.class || ''}` },
item.columns?.map(col =>
h('div', { class: 'grid-column' }, [
h(LayoutRenderer, { components: col.components })
])
)
),
hero: (item) => h('section', {
class: 'hero',
style: { backgroundImage: `url(${item.data.image?.path})` }
}, [
h('h1', item.data.headline),
h('p', item.data.subheadline),
item.data.cta_url && h('a', { href: item.data.cta_url }, item.data.cta_text)
])
};
const LayoutRenderer = {
name: 'LayoutRenderer',
props: { components: Array },
setup(props) {
return () => props.components?.map((item, i) => {
const renderer = componentMap[item.component];
return renderer ? renderer(item) : null;
});
}
};
</script>
<template>
<component
v-for="(item, index) in components"
:key="item.id || index"
:is="componentMap[item.component]?.(item)"
/>
</template>
Next.js with Image Optimization
// components/LayoutRenderer.jsx
import Image from 'next/image';
const COCKPIT_URL = process.env.NEXT_PUBLIC_COCKPIT_URL;
const componentMap = {
image: ({ data }) => (
<figure>
<Image
src={`${COCKPIT_URL}${data.image?.path}`}
alt={data.caption || ''}
width={800}
height={600}
style={{ objectFit: 'cover' }}
/>
{data.caption && <figcaption>{data.caption}</figcaption>}
</figure>
),
// Use Cockpit's image API for thumbnails
thumbnail: ({ data }) => (
<Image
src={`${COCKPIT_URL}/api/assets/image/${data.image?._id}?w=400&h=300&m=thumbnail`}
alt={data.caption || ''}
width={400}
height={300}
/>
),
// ... other components
};
Server Component Pattern
// app/[slug]/page.jsx
async function getPage(slug) {
const res = await fetch(
`${process.env.COCKPIT_URL}/api/content/item/pages?filter[slug]=${slug}`,
{ headers: { 'api-key': process.env.COCKPIT_API_KEY } }
);
return res.json();
}
export default async function Page({ params }) {
const page = await getPage(params.slug);
return (
<main>
<LayoutRenderer components={page.layout} />
</main>
);
}
PHP/Twig Implementation
PHP Class
// LayoutRenderer.php
class LayoutRenderer {
public function render(array $components): string {
$html = '';
foreach ($components as $item) {
$html .= $this->renderComponent($item);
}
return $html;
}
protected function renderComponent(array $item): string {
$method = 'render' . ucfirst($item['component']);
if (method_exists($this, $method)) {
return $this->$method($item);
}
return '';
}
protected function renderHeading(array $item): string {
$level = $item['data']['level'] ?? 1;
$text = htmlspecialchars($item['data']['text'] ?? '');
return "<h{$level}>{$text}</h{$level}>";
}
protected function renderRichtext(array $item): string {
return '<div class="richtext">' . ($item['data']['html'] ?? '') . '</div>';
}
protected function renderSection(array $item): string {
$class = htmlspecialchars($item['data']['class'] ?? '');
$children = $this->render($item['children'] ?? []);
return "<section class=\"{$class}\">{$children}</section>";
}
protected function renderGrid(array $item): string {
$class = htmlspecialchars($item['data']['class'] ?? '');
$columns = '';
foreach ($item['columns'] ?? [] as $col) {
$colContent = $this->render($col['components'] ?? []);
$columns .= "<div class=\"grid-column\">{$colContent}</div>";
}
return "<div class=\"grid {$class}\">{$columns}</div>";
}
}
Twig Template
{# layout.twig #}
{% macro render_component(item) %}
{% import _self as layout %}
{% if item.component == 'heading' %}
<h{{ item.data.level|default(1) }}>{{ item.data.text }}</h{{ item.data.level|default(1) }}>
{% elseif item.component == 'richtext' %}
<div class="richtext">{{ item.data.html|raw }}</div>
{% elseif item.component == 'image' %}
<figure>
<img src="{{ item.data.image.path }}" alt="{{ item.data.caption }}">
{% if item.data.caption %}<figcaption>{{ item.data.caption }}</figcaption>{% endif %}
</figure>
{% elseif item.component == 'section' %}
<section class="{{ item.data.class }}">
{% for child in item.children %}
{{ layout.render_component(child) }}
{% endfor %}
</section>
{% elseif item.component == 'grid' %}
<div class="grid {{ item.data.class }}">
{% for column in item.columns %}
<div class="grid-column">
{% for comp in column.components %}
{{ layout.render_component(comp) }}
{% endfor %}
</div>
{% endfor %}
</div>
{% elseif item.component == 'hero' %}
<section class="hero" style="background-image: url({{ item.data.image.path }})">
<h1>{{ item.data.headline }}</h1>
<p>{{ item.data.subheadline }}</p>
{% if item.data.cta_url %}
<a href="{{ item.data.cta_url }}" class="cta">{{ item.data.cta_text }}</a>
{% endif %}
</section>
{% endif %}
{% endmacro %}
{% import _self as layout %}
{% for item in page.layout %}
{{ layout.render_component(item) }}
{% endfor %}
Astro Implementation
---
// components/LayoutRenderer.astro
import { marked } from 'marked';
const { components = [] } = Astro.props;
function renderComponent(item) {
switch (item.component) {
case 'heading':
return `<h${item.data.level || 1}>${item.data.text}</h${item.data.level || 1}>`;
case 'richtext':
return `<div class="richtext">${item.data.html}</div>`;
case 'markdown':
return `<div>${marked(item.data.markdown)}</div>`;
default:
return '';
}
}
---
{components.map(item => (
<Fragment set:html={renderComponent(item)} />
))}
Error Handling
Always handle unknown components gracefully:
const Component = componentMap[item.component];
if (!Component) {
console.warn(`Unknown component: ${item.component}`);
return null; // or render a placeholder
}
Development Mode Placeholder
const UnknownComponent = ({ component }) => {
if (process.env.NODE_ENV === 'development') {
return (
<div className="unknown-component">
Unknown component: {component}
</div>
);
}
return null;
};
TypeScript Types
interface LayoutComponent {
component: string;
label?: string;
id?: string;
data: Record<string, any>;
children?: LayoutComponent[];
columns?: { components: LayoutComponent[] }[];
}
interface LayoutRendererProps {
components: LayoutComponent[];
}
type ComponentRenderer = (props: {
data: Record<string, any>;
children?: LayoutComponent[];
columns?: { components: LayoutComponent[] }[];
}) => JSX.Element | null;
type ComponentMap = Record<string, ComponentRenderer>;
Performance Tips
Memoize Components
import { memo } from 'react';
const MemoizedLayoutRenderer = memo(function LayoutRenderer({ components }) {
// ... implementation
});
Lazy Load Heavy Components
import { lazy, Suspense } from 'react';
const VideoPlayer = lazy(() => import('./VideoPlayer'));
const componentMap = {
video: ({ data }) => (
<Suspense fallback={<div>Loading video...</div>}>
<VideoPlayer src={data.src} />
</Suspense>
),
};
Use Intersection Observer for Images
import { useInView } from 'react-intersection-observer';
const LazyImage = ({ data }) => {
const { ref, inView } = useInView({ triggerOnce: true });
return (
<figure ref={ref}>
{inView && <img src={data.image?.path} alt={data.caption || ''} />}
</figure>
);
};