Skip to content

Latest commit

 

History

History
803 lines (623 loc) · 23.6 KB

File metadata and controls

803 lines (623 loc) · 23.6 KB

RichText Component Reference

The RichText component is a React component for rendering Optimizely CMS rich text content. It transforms structured JSON content from the CMS into React elements with full customization support.

Tip

Using the <RichText/> component is the recommended way to render rich text content. It's safer than dangerouslySetInnerHTML as it doesn't rely on HTML parsing, and allows you to customize how elements are rendered with your own React components.

Import

import { RichText } from '@optimizely/cms-sdk/react/richText';

Basic Usage

import { RichText } from '@optimizely/cms-sdk/react/richText';

function Article({ content }) {
  return (
    <div>
      <RichText content={content.body?.json} />
    </div>
  );
}

[!IMPORTANT] Integration API Limitations

When rich text content is created through Optimizely's Integration API (REST API) rather than the CMS editor interface, certain features may not function correctly:

  • Inline styles - Some inline CSS properties might not work as expected. See Attribute and CSS Property Support for details on supported CSS properties.
  • HTML validation is bypassed, which can result in malformed or invalid HTML that causes rendering issues.
  • Advanced formatting that relies on TinyMCE editor processing may be missing.
  • Custom attributes or props might not be mapped properly from raw HTML to React components.
  • Security sanitization performed by the editor may not occur.

For full feature compatibility and optimal rendering, create rich text content using the CMS's built-in TinyMCE editor interface.

Props Reference

content

Type: { type: 'richText', children: Node[] } | null | undefined

The rich text content from Optimizely CMS. This is typically accessed from a content type property with richText type.

<RichText content={article.body?.json} />

elements

Type: Record<string, React.ComponentType<ElementProps>>
Default: Built-in HTML element mappings

Custom React components for rendering specific element types. Allows you to override how different block and inline elements are rendered.

Available Element Types

All element types listed below are supported by default and can be customized with the elements prop:

  • Headings: heading-one, heading-two, heading-three, heading-four, heading-five, heading-six
  • Text blocks: paragraph, quote, div
  • Lists: bulleted-list, numbered-list, list-item
  • Text semantics (inline): span, mark, strong, em, u, s, i, b, small, sub, sup, ins, del, kbd, abbr, cite, dfn, q, data, bdo, bdi
  • Code-related: code, pre, var, samp
  • Links & Interactive: link, a, button, label
  • Tables: table, thead, tbody, tfoot, caption, tr, th, td

[!NOTE] > SVG elements are not supported by default. SVG requires specialized child elements (circle, path, rect, etc.) and attributes that would require extensive additional support.

Alternatives: Use custom element handlers, upload SVG as image assets, or create dedicated React components.

Example: Custom Elements

import { RichText, ElementProps } from '@optimizely/cms-sdk/react/richText';

// Custom heading with styling
const CustomHeading = ({ children, element }: ElementProps) => (
  <h1 className="text-4xl font-bold text-blue-600 mb-4">{children}</h1>
);

// Custom link with tracking
const CustomLink = ({ children, element }: ElementProps) => {
  const linkElement = element as LinkElement;

  return (
    <a
      href={linkElement.url}
      className="text-blue-500 hover:underline"
      onClick={() => trackLinkClick(linkElement.url)}
      target={linkElement.target}
      rel={linkElement.rel}
    >
      {children}
    </a>
  );
};

// Custom quote block
const CustomQuote = ({ children }: ElementProps) => (
  <blockquote className="border-l-4 border-gray-300 pl-4 py-2 italic text-gray-600">
    {children}
  </blockquote>
);

function Article({ content }) {
  return (
    <RichText
      content={content.body?.json}
      elements={{
        'heading-one': CustomHeading,
        link: CustomLink,
        quote: CustomQuote,
      }}
    />
  );
}

leafs

Type: Record<string, React.ComponentType<LeafProps>>
Default: Built-in text formatting mappings

Custom React components for rendering text formatting marks (bold, italic, etc.).

Available Leaf Types

  • bold - Bold text formatting
  • italic - Italic text formatting
  • underline - Underlined text
  • strikethrough - Strikethrough text
  • code - Inline code formatting

Example: Custom Leafs

import { RichText, LeafProps } from '@optimizely/cms-sdk/react/richText';

// Custom bold with additional styling
const CustomBold = ({ children }: LeafProps) => (
  <strong className="font-extrabold text-gray-900">{children}</strong>
);

// Custom code with syntax highlighting theme
const CustomCode = ({ children }: LeafProps) => (
  <code className="bg-gray-100 px-1 py-0.5 rounded font-mono text-sm text-red-600">
    {children}
  </code>
);

// Custom highlight leaf (for custom mark)
const CustomHighlight = ({ children }: LeafProps) => (
  <mark className="bg-yellow-200 px-1 rounded">{children}</mark>
);

function Article({ content }) {
  return (
    <RichText
      content={content.body?.json}
      leafs={{
        bold: CustomBold,
        code: CustomCode,
        highlight: CustomHighlight, // Custom mark
      }}
    />
  );
}

decodeHtmlEntities

Type: boolean
Default: true

Controls whether HTML entities in text content should be decoded. When enabled, entities like &lt;, &gt;, &amp; are converted to their corresponding characters.

Example: Controlling HTML Entity Decoding

// Default behavior - entities are decoded
<RichText
  content={content.body?.json}
  decodeHtmlEntities={true} // &lt;div&gt; becomes <div>
/>

// Preserve HTML entities as-is
<RichText
  content={content.body?.json}
  decodeHtmlEntities={false} // &lt;div&gt; stays as &lt;div&gt;
/>

Use Cases for Disabling

// When displaying code examples where entities should remain encoded
const CodeExample = ({ content }) => (
  <div className="code-display">
    <RichText
      content={content}
      decodeHtmlEntities={false}
      elements={{
        paragraph: ({ children }) => <pre>{children}</pre>,
      }}
    />
  </div>
);

Complete Example

import React from 'react';
import {
  RichText,
  ElementProps,
  LeafProps,
} from '@optimizely/cms-sdk/react/richText';

// Custom element components
const CustomHeading = ({ children, element }: ElementProps) => (
  <h1 className="text-3xl font-bold mb-4 text-slate-800">{children}</h1>
);

const CustomParagraph = ({ children }: ElementProps) => (
  <p className="mb-4 text-slate-600 leading-relaxed">{children}</p>
);

const CustomLink = ({ children, element }: ElementProps) => {
  const link = element as { url: string; target?: string };
  return (
    <a
      href={link.url}
      target={link.target}
      className="text-blue-600 hover:text-blue-800 underline"
    >
      {children}
    </a>
  );
};

// Custom leaf components
const CustomBold = ({ children }: LeafProps) => (
  <strong className="font-semibold text-slate-900">{children}</strong>
);

const CustomItalic = ({ children }: LeafProps) => (
  <em className="italic text-slate-700">{children}</em>
);

const CustomCode = ({ children }: LeafProps) => (
  <code className="bg-slate-100 px-2 py-1 rounded text-sm font-mono text-slate-800">
    {children}
  </code>
);

export default function Article({ content }) {
  return (
    <article className="prose max-w-none">
      <RichText
        content={content.body?.json}
        elements={{
          'heading-one': CustomHeading,
          paragraph: CustomParagraph,
          link: CustomLink,
        }}
        leafs={{
          bold: CustomBold,
          italic: CustomItalic,
          code: CustomCode,
        }}
        decodeHtmlEntities={true}
      />
    </article>
  );
}

TypeScript Support

The component is fully typed with TypeScript. Import the prop types for better development experience:

import {
  RichText,
  RichTextProps,
  ElementProps,
  LeafProps,
  ElementMap,
  LeafMap,
} from '@optimizely/cms-sdk/react/richText';

// Type-safe element mapping
const elements: ElementMap = {
  'heading-one': CustomHeading,
  paragraph: CustomParagraph,
};

// Type-safe leaf mapping
const leafs: LeafMap = {
  bold: CustomBold,
  italic: CustomItalic,
};

Fallback Handling for Unknown Elements

The RichText component uses a simple and safe fallback strategy for unknown elements and text marks.

Default Behavior

All unknown elements and text marks render as <span> elements.

This ensures:

  • Valid HTML in any context (inline or block)
  • No hydration errors in React
  • Safe rendering without breaking layout
  • Can be styled via CSS if needed

How It Works

  • Known Elements: Use their default HTML tags (e.g., heading-one<h1>, paragraph<p>)
  • Unknown Elements: Automatically become <span> tags
  • Known Text Marks: Use their default tags (e.g., bold<strong>, italic<em>)
  • Unknown Text Marks: Automatically become <span> tags

Custom Element Handling

If you encounter unknown HTML tags or elements not supported by the SDK, you can override them using the elements or leafs props to render custom React components.

Note

Unknown elements and text marks are typically introduced when rich text content is created through the Integration API (REST API) rather than the CMS editor. When using the Integration API, developers can insert custom HTML elements or formatting that may not be recognized by the SDK's default element mappings. Additionally, some element attributes may not be fully supported when content is created via the Integration API, as the TinyMCE editor processing that normalizes and validates these attributes is bypassed.

import {
  RichText,
  ElementProps,
  LeafProps,
} from '@optimizely/cms-sdk/react/richText';

// Custom component for unknown block elements
const UnknownElement = ({ children, element }: ElementProps) => (
  <div className="unknown-element" data-type={element.type}>
    {children}
  </div>
);

// Custom component for unknown text marks
const UnknownLeaf = ({ children, leaf }: LeafProps) => (
  <span className="unknown-leaf" data-marks={Object.keys(leaf).join(',')}>
    {children}
  </span>
);

// Custom component for a specific custom element
const CustomElement = ({ children, element }: ElementProps) => (
  <div className="custom-element">{children}</div>
);

function Article({ content }) {
  return (
    <RichText
      content={content.body?.json}
      elements={{
        'unknown-element': UnknownElement, // Handle specific unknown element type
        'custom-element': CustomElement, // Your custom CMS element
      }}
      leafs={{
        'unknown-leaf': UnknownLeaf, // Handle specific unknown text mark
        highlight: ({ children }) => (
          <mark className="highlight">{children}</mark>
        ),
      }}
    />
  );
}

Example: Handling Custom CMS Elements

// Custom video element not supported by default
const VideoElement = ({ children, element }: ElementProps) => {
  const videoData = element as { videoUrl?: string; autoplay?: boolean };

  return (
    <div className="video-container">
      <video src={videoData.videoUrl} autoPlay={videoData.autoplay} controls>
        {children}
      </video>
    </div>
  );
};

// Custom callout box element
const CalloutElement = ({ children, element }: ElementProps) => {
  const calloutData = element as { variant?: string };

  return (
    <div className={`callout callout-${calloutData.variant || 'info'}`}>
      {children}
    </div>
  );
};

function Article({ content }) {
  return (
    <RichText
      content={content.body?.json}
      elements={{
        'video-embed': VideoElement,
        'callout-box': CalloutElement,
      }}
    />
  );
}

Attribute and CSS Property Support

Under the hood, the RichText component performs sophisticated attribute and CSS property processing to ensure React compatibility. Here's how it handles the technical challenges of converting CMS content to proper React elements.

🔧 Technical Deep Dive: The component normalizes HTML attributes to camelCase React props and moves CSS properties to React's style object, preventing warnings and ensuring proper rendering.

HTML Attributes (Converted to React Props)

The component automatically normalizes HTML attributes to React-compatible prop names. This conversion happens through a predefined mapping table that handles common naming conflicts between HTML and React.

⚠️ React Compatibility: React requires camelCase prop names for DOM attributes, but HTML uses kebab-case. Our mapping system handles this conversion automatically to prevent console warnings.

Core Required Mappings

Reserved Keywords (Must be mapped):

  • classclassName
  • forhtmlFor

Table Attributes:

  • colspancolSpan
  • rowspanrowSpan
  • cellpaddingcellPadding
  • cellspacingcellSpacing

Form & Input Attributes

  • tabindex, tab-indextabIndex
  • readonlyreadOnly
  • maxlengthmaxLength
  • minlengthminLength
  • autocompleteautoComplete
  • autofocusautoFocus
  • autoplayautoPlay
  • contenteditable, content-editablecontentEditable
  • spellcheckspellCheck
  • novalidatenoValidate

Media Attributes

  • crossorigincrossOrigin
  • usemapuseMap
  • allowfullscreenallowFullScreen
  • frameborderframeBorder
  • playsinlineplaysInline
  • srcsetsrcSet
  • srcdocsrcDoc
  • srclangsrcLang

Meta & Form Attributes

  • accept-charsetacceptCharset
  • http-equivhttpEquiv
  • charsetcharSet
  • datetimedateTime
  • hreflanghrefLang
  • accesskeyaccessKey
  • autocapitalizeautoCapitalize
  • referrerpolicyreferrerPolicy
  • formactionformAction
  • formenctypeformEnctype
  • formmethodformMethod
  • formnovalidateformNoValidate
  • formtargetformTarget
  • enctypeencType

Attributes That Work As-Is

🚀 Performance Optimization: Many HTML attributes work directly in React without conversion, so they're not included in our mapping table for better performance.

These attributes work directly in React:

  • Basic: id, name, value, type, href, src, alt, title
  • Form: disabled, checked, selected, multiple, required, placeholder, pattern, min, max, step
  • Interactive: draggable, hidden, lang, dir, role
  • Media: width, height, preload, loop, muted, controls
  • Security: nonce, sandbox, download

Special Attribute Handling

ARIA Attributes (Preserved as-is): All ARIA attributes remain in kebab-case as React requires:

  • aria-label, aria-labelledby, aria-describedby
  • aria-hidden, aria-expanded, aria-* (all ARIA attributes)

Data Attributes (Preserved as-is):

  • data-* (all data attributes are preserved in kebab-case)

CSS Properties (Moved to Style Object)

CSS properties are automatically detected using a curated set of known CSS property names and moved to React's style object with proper camelCase conversion.

🎨 Style Processing: React requires CSS properties to be in a style object with camelCase keys. Properties like background-color become style.backgroundColor. This system prevents invalid DOM property warnings.

Layout & Sizing

  • min-widthstyle.minWidth
  • max-widthstyle.maxWidth
  • min-heightstyle.minHeight
  • max-heightstyle.maxHeight

Spacing

  • margin, margin-top, margin-right, margin-bottom, margin-left
  • padding, padding-top, padding-right, padding-bottom, padding-left

Typography

  • font, font-family, font-size, font-weight, font-style, font-variant
  • line-height, letter-spacing, word-spacing
  • text-align, text-decoration, text-transform, text-indent, text-shadow
  • vertical-align

Colors & Backgrounds

  • color
  • background, background-color, background-image, background-repeat
  • background-position, background-size, background-attachment
  • background-clip, background-origin

Borders

  • border-width, border-style, border-color, border-radius
  • border-top, border-right, border-bottom, border-left
  • border-*-width, border-*-style, border-*-color (all directional variants)
  • border-*-radius (all corner variants)

Positioning

  • position, top, right, bottom, left, z-index
  • float, clear

Display & Visibility

  • display, visibility, opacity
  • overflow, overflow-x, overflow-y
  • clip, clip-path

Flexbox

  • flex, flex-direction, flex-wrap, flex-flow
  • justify-content, align-items, align-content, align-self
  • flex-grow, flex-shrink, flex-basis

Grid Layout

  • grid, grid-template, grid-template-rows, grid-template-columns
  • grid-template-areas, grid-area, grid-row, grid-column
  • grid-gap, gap, row-gap, column-gap

Text & Content

  • white-space, word-wrap, word-break, overflow-wrap
  • hyphens, text-overflow, direction, unicode-bidi, writing-mode

Visual Effects

  • box-shadow, text-shadow
  • filter, backdrop-filter
  • transform, transform-origin
  • perspective, perspective-origin

Animation & Transitions

  • transition, transition-property, transition-duration
  • transition-timing-function, transition-delay
  • animation, animation-name, animation-duration
  • animation-timing-function, animation-delay
  • animation-iteration-count, animation-direction
  • animation-fill-mode, animation-play-state

Interaction

  • cursor, pointer-events, user-select, resize

Modern CSS

  • aspect-ratio, object-fit, object-position
  • scroll-behavior, overscroll-behavior
  • scroll-snap-type, scroll-snap-align
  • scroll-margin, scroll-padding

Special Handling

Dual-Purpose Properties

Some properties exist in both HTML and CSS domains. The component uses context-aware logic to determine the correct handling:

  • width, height: Treated as HTML attributes for tables and images, CSS properties for other elements
  • border: Treated as HTML attribute for tables, CSS property for other elements

Table-Specific Attributes

Table elements receive special handling for HTML attributes:

// These remain as HTML attributes for tables
<table border="1" cellpadding="5" cellspacing="0" width="100%">
  <tr>
    <td colspan="2" rowspan="1">
      Content
    </td>
  </tr>
</table>

Style Object Conversion

The component performs real-time CSS property conversion using kebab-to-camelCase transformation:

// Input: Slate.js node with CSS properties as attributes
{
  type: 'div',
  'font-size': '16px',
  'background-color': 'blue',
  'margin-top': '10px',
  children: [{ text: 'Styled content' }]
}

// Output: React element with style object
<div style={{
  fontSize: '16px',
  backgroundColor: 'blue',
  marginTop: '10px'
}}>
  Styled content
</div>

Unsupported CSS Properties

While our CSS property detection covers most real-world use cases, some advanced CSS features are intentionally excluded to maintain performance and simplicity:

Print-Specific Properties

  • page-break-before, page-break-after, page-break-inside
  • orphans, widows

Advanced Layout Features

  • Multi-column layout properties (columns, column-count, etc.)
  • CSS Masking properties (mask, mask-image, etc.)
  • Ruby text properties (ruby-align, ruby-position, etc.)

Logical Properties

  • margin-inline-start, margin-block-end, etc.
  • border-inline-start, border-block-end, etc.

CSS Custom Properties

  • CSS variables (--custom-property) are not automatically detected

Extending Support

If you encounter unsupported properties in your CMS content, you can handle them by providing custom element components:

🔧 Extensibility: The component is designed to be extended rather than modified. Use custom element components for application-specific handling of unsupported properties.

// Custom handling for unsupported properties
const CustomDiv = ({ children, element, attributes }: ElementProps) => {
  const customStyle = {
    // Handle unsupported CSS properties manually
    '--custom-property': attributes['--custom-property'],
    columnCount: attributes['column-count'],
  };

  return <div style={customStyle}>{children}</div>;
};

<RichText
  content={content}
  elements={{
    div: CustomDiv,
  }}
/>;

Technical Implementation Example

Here's how the attribute processing works in practice with a real Slate.js node structure:

// Rich text content in Slate.js format with mixed attributes
const richTextContent = {
  type: 'richText',
  children: [
    {
      type: 'div',
      class: 'content-block',
      'data-testid': 'rich-content',
      'aria-label': 'Article content',
      width: '100%', // HTML attribute for some elements
      'font-size': '16px', // CSS property → style.fontSize
      'background-color': 'lightblue', // CSS property → style.backgroundColor
      'margin-top': '20px', // CSS property → style.marginTop
      border: '1px solid #ccc', // CSS property → style.border
      children: [
        { text: 'This content has mixed HTML attributes and CSS properties' },
      ],
    },
  ],
};

// Rendered as:
<div
  className="content-block"
  data-testid="rich-content"
  aria-label="Article content"
  width="100%"
  style={{
    fontSize: '16px',
    backgroundColor: 'lightblue',
    marginTop: '20px',
    border: '1px solid #ccc',
  }}
>
  This content has mixed HTML attributes and CSS properties
</div>;

This attribute and CSS property processing ensures that rich text content from Optimizely CMS renders correctly in React applications while maintaining proper HTML semantics and React compatibility.

Best Practices

  1. Performance: Only override elements/leafs you need to customize - the default implementations are optimized
  2. Accessibility: Maintain semantic HTML structure in custom components - screen readers depend on proper markup
  3. Type Safety: Use TypeScript interfaces for better development experience and catch errors at compile time
  4. Unknown Elements: The default <span> fallback handles unknown elements safely - no configuration needed

Common Use Cases

Blog Content

<RichText
  content={post.content?.json}
  elements={{
    'heading-one': ({ children }) => (
      <h1 className="article-title">{children}</h1>
    ),
    paragraph: ({ children }) => (
      <p className="article-paragraph">{children}</p>
    ),
  }}
/>

Documentation

<RichText
  content={doc.body?.json}
  elements={{
    code: ({ children }) => <CodeBlock>{children}</CodeBlock>,
    pre: ({ children }) => <PreformattedText>{children}</PreformattedText>,
  }}
  decodeHtmlEntities={false} // Preserve code entities
/>

Marketing Content

<RichText
  content={page.content?.json}
  elements={{
    link: ({ children, element }) => (
      <TrackableLink url={element.url} eventName="content-click">
        {children}
      </TrackableLink>
    ),
  }}
/>