Disclaimer: This website requires Please enable JavaScript in your browser settings for the best experience.

Dev guideRecipesAPI ReferenceChangelog
Dev guideRecipesUser GuidesNuGetDev CommunityOptimizely AcademySubmit a ticketLog In
Dev guide

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>
  );
}