Block with image and metadata
How to render reusable content blocks with metadata using GraphQL fragments in Optimizely Graph.
Use block fragments to define reusable content structures (such as images, text, videos, and rich content) with metadata support. These fragments help you build flexible and consistent content layouts across different page types.
Block fragment definitions
Use these fragments to define the structure and metadata for each block type. You can reuse them across multiple queries to simplify your GraphQL code.
# This fragment defines the structure of an image block.
# It includes image details and metadata for display and type identification.
fragment ImageBlockData on ImageBlock {
Name
Url
Description
Width
Height
Caption
Copyright
_metadata {
displayName
types
}
}
# This fragment defines a text block.
# It includes text content, alignment, background color, and metadata.
fragment TextBlockData on TextBlock {
Name
Text
TextAlign
BackgroundColor
_metadata {
displayName
types
}
}
# This fragment defines a content block.
# It includes title, description, body text, and call-to-action elements.
fragment ContentBlockData on ContentBlock {
Name
Title
Description
MainBody
CallToActionText
CallToActionUrl
_metadata {
displayName
types
}
}
# This fragment defines a video block.
# It includes video URL, thumbnail, description, duration, and metadata.
fragment VideoBlockData on VideoBlock {
Name
VideoUrl
ThumbnailUrl
Description
Duration
_metadata {
displayName
types
}
}
``Complete page with blocks query
Use this query to retrieve a full page and its associated content blocks. It supports multiple page types and includes metadata, teaser text, and structured content areas.
The following query fetches a page by its relative path, returning metadata and content blocks for ArticlePage, StandardPage, and LandingPage types:
query GetPageWithBlocks($path: String!) {
Content(
where: { RelativePath: { eq: $path } }
) {
item {
Name
RelativePath
_metadata {
displayName
types
lastModified
published
}
... on ArticlePage {
TeaserText
MainBody
StartPublish
PageImage {
Url
Description
}
# Main content area with blocks
MainContentArea {
... on ImageBlock {
...ImageBlockData
}
... on TextBlock {
...TextBlockData
}
... on ContentBlock {
...ContentBlockData
}
... on VideoBlock {
...VideoBlockData
}
}
# Related content area
RelatedContentArea {
... on ImageBlock {
...ImageBlockData
}
... on ContentBlock {
...ContentBlockData
}
}
}
... on StandardPage {
TeaserText
MainBody
MetaTitle
MetaDescription
MainContentArea {
... on ImageBlock {
...ImageBlockData
}
... on TextBlock {
...TextBlockData
}
... on ContentBlock {
...ContentBlockData
}
... on VideoBlock {
...VideoBlockData
}
}
}
... on LandingPage {
HeroImage {
Url
}
HeroContentArea {
... on ImageBlock {
...ImageBlockData
}
... on TextBlock {
...TextBlockData
}
... on ContentBlock {
...ContentBlockData
}
}
MainContentArea {
... on ImageBlock {
...ImageBlockData
}
... on TextBlock {
...TextBlockData
}
... on ContentBlock {
...ContentBlockData
}
... on VideoBlock {
...VideoBlockData
}
}
}
}
}
}React block renderer components
Use React components to dynamically render each block type. Each component manages its own layout and styling based on the block’s metadata.
import { gql, useQuery } from '@apollo/client';
import React from 'react';
// Block type definitions
interface BaseBlock {
Name: string;
_metadata: {
displayName: string;
types: string[];
};
}
interface ImageBlock extends BaseBlock {
Url: string;
Description?: string;
Width?: number;
Height?: number;
Caption?: string;
Copyright?: string;
}
interface TextBlock extends BaseBlock {
Text: string;
TextAlign?: string;
BackgroundColor?: string;
}
interface ContentBlock extends BaseBlock {
Title?: string;
Description?: string;
MainBody?: string;
CallToActionText?: string;
CallToActionUrl?: string;
}
interface VideoBlock extends BaseBlock {
VideoUrl: string;
ThumbnailUrl?: string;
Description?: string;
Duration?: number;
}
// Block renderer components
const ImageBlockRenderer: React.FC<{ block: ImageBlock }> = ({ block }) => (
<div className="image-block" data-block-name={block.Name}>
<figure className="image-container">
<img
src={block.Url}
alt={block.Name}
width={block.Width}
height={block.Height}
className="block-image"
/>
{block.Caption && (
<figcaption className="image-caption">
{block.Caption}
{block.Copyright && (
<span className="copyright"> © {block.Copyright}</span>
)}
</figcaption>
)}
</figure>
{block.Description && (
<div className="image-description">
{block.Description}
</div>
)}
</div>
);
const TextBlockRenderer: React.FC<{ block: TextBlock }> = ({ block }) => (
<div
className="text-block"
data-block-name={block.Name}
style={{
textAlign: block.TextAlign as any,
backgroundColor: block.BackgroundColor
}}
>
<div
className="text-content"
dangerouslySetInnerHTML={{ __html: block.Text }}
/>
</div>
);
const ContentBlockRenderer: React.FC<{ block: ContentBlock }> = ({ block }) => (
<div className="content-block" data-block-name={block.Name}>
{block.Title && (
<h2 className="content-title">{block.Title}</h2>
)}
{block.Description && (
<div className="content-description">{block.Description}</div>
)}
{block.MainBody && (
<div
className="content-body"
dangerouslySetInnerHTML={{ __html: block.MainBody }}
/>
)}
{block.CallToActionText && block.CallToActionUrl && (
<div className="content-cta">
<a href={block.CallToActionUrl} className="cta-button">
{block.CallToActionText}
</a>
</div>
)}
</div>
);
const VideoBlockRenderer: React.FC<{ block: VideoBlock }> = ({ block }) => (
<div className="video-block" data-block-name={block.Name}>
<div className="video-container">
<video controls poster={block.ThumbnailUrl}>
<source src={block.VideoUrl} />
Your browser does not support the video tag.
</video>
</div>
{block.Description && (
<div className="video-description">{block.Description}</div>
)}
{block.Duration && (
<div className="video-duration">
Duration: {Math.floor(block.Duration / 60)}:{(block.Duration % 60).toString().padStart(2, '0')}
</div>
)}
</div>
);
// Block renderer dispatcher
const BlockRenderer: React.FC<{ block: any }> = ({ block }) => {
const blockType = block._metadata?.types?.[0] || 'Unknown';
switch (blockType) {
case 'ImageBlock':
return <ImageBlockRenderer block={block as ImageBlock} />;
case 'TextBlock':
return <TextBlockRenderer block={block as TextBlock} />;
case 'ContentBlock':
return <ContentBlockRenderer block={block as ContentBlock} />;
case 'VideoBlock':
return <VideoBlockRenderer block={block as VideoBlock} />;
default:
return (
<div className="unknown-block">
<p>Unknown block type: {blockType}</p>
<pre>{JSON.stringify(block, null, 2)}</pre>
</div>
);
}
};
// Main page component
const GET_PAGE_WITH_BLOCKS = gql`
fragment ImageBlockData on ImageBlock {
Name
Url
Description
Width
Height
Caption
Copyright
_metadata {
displayName
types
}
}
fragment TextBlockData on TextBlock {
Name
Text
TextAlign
BackgroundColor
_metadata {
displayName
types
}
}
fragment ContentBlockData on ContentBlock {
Name
Title
Description
MainBody
CallToActionText
CallToActionUrl
_metadata {
displayName
types
}
}
fragment VideoBlockData on VideoBlock {
Name
VideoUrl
ThumbnailUrl
Description
Duration
_metadata {
displayName
types
}
}
query GetPageWithBlocks($path: String!) {
Content(
where: { RelativePath: { eq: $path } }
) {
item {
Name
RelativePath
_metadata {
displayName
types
lastModified
}
... on ArticlePage {
TeaserText
MainBody
StartPublish
MainContentArea {
... on ImageBlock {
...ImageBlockData
}
... on TextBlock {
...TextBlockData
}
... on ContentBlock {
...ContentBlockData
}
... on VideoBlock {
...VideoBlockData
}
}
}
... on StandardPage {
TeaserText
MainBody
MainContentArea {
... on ImageBlock {
...ImageBlockData
}
... on TextBlock {
...TextBlockData
}
... on ContentBlock {
...ContentBlockData
}
... on VideoBlock {
...VideoBlockData
}
}
}
}
}
}
`;
export function PageWithBlocks({ path }: { path: string }) {
const { loading, error, data } = useQuery(GET_PAGE_WITH_BLOCKS, {
variables: { path },
errorPolicy: 'partial'
});
if (loading) return (
<div className="loading-state">
<div className="spinner"></div>
<p>Loading page content...</p>
</div>
);
if (error) return (
<div className="error-state">
<h3>Failed to load page</h3>
<p>{error.message}</p>
</div>
);
const page = data?.Content?.item;
if (!page) {
return <div className="not-found">Page not found</div>;
}
return (
<article className="page-with-blocks">
<header className="page-header">
<h1>{page.Name}</h1>
{page.TeaserText && (
<div className="page-teaser">{page.TeaserText}</div>
)}
<div className="page-meta">
<span>Type: {page._metadata.types.join(', ')}</span>
{page.StartPublish && (
<time dateTime={page.StartPublish}>
Published: {new Date(page.StartPublish).toLocaleDateString()}
</time>
)}
</div>
</header>
{page.MainBody && (
<div
className="page-main-body"
dangerouslySetInnerHTML={{ __html: page.MainBody }}
/>
)}
{page.MainContentArea && page.MainContentArea.length > 0 && (
<div className="content-area">
<h2>Content Blocks</h2>
<div className="blocks-container">
{page.MainContentArea.map((block: any, index: number) => (
<div key={`${block.Name}-${index}`} className="block-wrapper">
<BlockRenderer block={block} />
</div>
))}
</div>
</div>
)}
</article>
);
}Updated 16 days ago
