Skip to content

Custom Preview Templates

A custom preview template allows you to define how content entries are displayed in the CMS preview pane. By registering a custom preview template, you can create a more tailored and user-friendly editing experience for content editors.

Compatibility Note

Because there is little Netlify/Decap CMS documentation on this topic, Sveltia CMS may not be fully compatible with existing preview templates. Our implementation does not include any undocumented component props. Additionally, we haven’t verified that all of the examples below work with Sveltia CMS. If you encounter any issues, please report them to us.

Overview

To register a custom preview template, use the registerPreviewTemplate method on the CMS object:

js
CMS.registerPreviewTemplate(name, component);

Parameters

  • name (string, required): The name of the collection or collection file for which the preview template is being registered.
  • component (React component, required): A React class component that defines the preview template. This component receives the entry data as props and should render the preview accordingly.

Component Props

The component you register receives the following props during render:

  • entry (Immutable Map): Contains the entry data with the following structure:
    js
    {
      data: { ... },      // Default locale data
      i18n: {             // Non-default locale data (if i18n is enabled)
        [locale]: {
          data: { ... }
        }
      },
      slug,               // Entry slug
      path,               // Entry file path
      newRecord,          // Boolean indicating if it's a new entry
      collection,         // Collection name
      mediaFiles,         // Array of media files associated with the entry
    }
  • widgetFor (function): Returns a React element rendering a Svelte field preview for a given field key path. Useful for rendering individual field previews.
  • widgetsFor (function): Returns widget data for a given field name. For list fields, returns an array of objects; for object fields, returns a single object; for primitive fields, returns the raw value. Each object has:
    js
    {
      data: { ... },              // Raw field values
      widgets: { ... }            // React preview elements keyed by field name
    }
  • getAsset (function): Returns the asset item for a given path. Returns undefined if not found. Automatically resolves image paths to blob URLs for preview purposes.
  • getCollection (function): Async function that returns entries from a specified collection. Takes parameters:
    • collectionName (string): Name of the collection to query
    • slug (string, optional): Entry slug to fetch a specific entry; if omitted, returns all entries
  • fieldsMetaData (Immutable Map): Metadata for each field keyed by field name. Useful for accessing related entry data from relation fields.
  • document (Document): The preview pane iframe's Document object. Use this instead of the global document to manipulate the preview DOM.
  • window (Window): The preview pane iframe's Window object. Use this instead of the global window to access the preview window context.

Writing React Components Inline

Sveltia CMS exposes two constructs globally to allow you to create preview components inline without requiring a build step:

  • h — An alias for React.createElement(), used to create React elements in the non-JSX examples
  • createClass — Used to define React class components when not using JSX syntax

These are available on the window object when Sveltia CMS is loaded, allowing you to register preview templates directly in a <script> tag.

Future Plans

We plan to add support for Preact+HTM components in the future, which will allow you to write preview templates using a more lightweight syntax without JSX.

Working with Immutable Data

The entry and fieldsMetaData props are Immutable Map objects. Use their methods to safely access nested data:

  • entry.getIn(['data', 'fieldName']) — Access field values
  • entry.get('i18n') — Access internationalization data
  • .toJS() — Convert to a plain JavaScript object

For more information on working with Immutable data structures, see the Immutable.js documentation.

Examples

Basic Entry Preview

Display a simple blog post preview with a title and featured image:

js
const PostPreview = createClass({
  render: function () {
    const { entry, widgetFor, getAsset } = this.props;
    const image = entry.getIn(['data', 'image']);
    const imageAsset = image ? getAsset(image) : null;

    return h(
      'div',
      { style: { padding: '20px', fontFamily: 'sans-serif' } },
      h('h1', {}, entry.getIn(['data', 'title'])),
      imageAsset &&
        h('img', {
          src: imageAsset.url,
          alt: 'Featured',
          style: { maxWidth: '100%', height: 'auto' },
        }),
      h('div', { style: { marginTop: '20px' } }, widgetFor('body')),
    );
  },
});

CMS.registerPreviewTemplate('posts', PostPreview);
jsx
export default class PostPreview extends React.Component {
  render() {
    const { entry, widgetFor, getAsset } = this.props;
    const image = entry.getIn(['data', 'image']);
    const imageAsset = image ? getAsset(image) : null;

    return (
      <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
        <h1>{entry.getIn(['data', 'title'])}</h1>
        {imageAsset && (
          <img src={imageAsset.url} alt="Featured" style={{ maxWidth: '100%', height: 'auto' }} />
        )}
        <div style={{ marginTop: '20px' }}>{widgetFor('body')}</div>
      </div>
    );
  }
}

CMS.registerPreviewTemplate('posts', PostPreview);

List Fields

Preview a collection entry with a list of authors:

js
const AuthorsPreview = createClass({
  render: function () {
    const { widgetsFor } = this.props;
    const authors = widgetsFor('authors');

    return h(
      'div',
      { style: { padding: '20px' } },
      h('h2', {}, 'Authors'),
      Array.isArray(authors) &&
        authors.map(function (author, index) {
          return h(
            'div',
            { key: index, style: { marginBottom: '20px', borderBottom: '1px solid #eee' } },
            h('strong', {}, author.getIn(['data', 'name'])),
            h('p', {}, author.getIn(['data', 'description'])),
            author.getIn(['widgets', 'description']),
          );
        }),
    );
  },
});

CMS.registerPreviewTemplate('team', AuthorsPreview);
jsx
export default class AuthorsPreview extends React.Component {
  render() {
    const { widgetsFor } = this.props;
    const authors = widgetsFor('authors');

    return (
      <div style={{ padding: '20px' }}>
        <h2>Authors</h2>
        {Array.isArray(authors) &&
          authors.map((author, index) => (
            <div key={index} style={{ marginBottom: '20px', borderBottom: '1px solid #eee' }}>
              <strong>{author.getIn(['data', 'name'])}</strong>
              <p>{author.getIn(['data', 'description'])}</p>
              {author.getIn(['widgets', 'description'])}
            </div>
          ))}
      </div>
    );
  }
}

CMS.registerPreviewTemplate('team', AuthorsPreview);

Object Fields

Preview settings stored as an object structure:

js
const SiteSettingsPreview = createClass({
  render: function () {
    const { entry, widgetsFor } = this.props;
    const settings = widgetsFor('site_config');

    return h(
      'div',
      { style: { padding: '20px', backgroundColor: '#f5f5f5', borderRadius: '4px' } },
      h('h2', {}, entry.getIn(['data', 'title'])),
      h(
        'dl',
        {},
        h('dt', {}, 'Posts per page:'),
        h('dd', {}, settings.getIn(['data', 'posts_per_page'])),

        h('dt', {}, 'Site tagline:'),
        h('dd', {}, settings.getIn(['data', 'tagline'])),

        h('dt', {}, 'Enable comments:'),
        h('dd', {}, settings.getIn(['data', 'enable_comments']) ? 'Yes' : 'No'),
      ),
    );
  },
});

CMS.registerPreviewTemplate('settings', SiteSettingsPreview);
jsx
export default class SiteSettingsPreview extends React.Component {
  render() {
    const { entry, widgetsFor } = this.props;
    const settings = widgetsFor('site_config');

    return (
      <div style={{ padding: '20px', backgroundColor: '#f5f5f5', borderRadius: '4px' }}>
        <h2>{entry.getIn(['data', 'title'])}</h2>
        <dl>
          <dt>Posts per page:</dt>
          <dd>{settings.getIn(['data', 'posts_per_page'])}</dd>

          <dt>Site tagline:</dt>
          <dd>{settings.getIn(['data', 'tagline'])}</dd>

          <dt>Enable comments:</dt>
          <dd>{settings.getIn(['data', 'enable_comments']) ? 'Yes' : 'No'}</dd>
        </dl>
      </div>
    );
  }
}

CMS.registerPreviewTemplate('settings', SiteSettingsPreview);

Accessing Metadata & Relations

Display entry data with related entries fetched via fieldsMetaData:

js
const ArticlePreview = createClass({
  render: function () {
    const { entry, fieldsMetaData, widgetFor } = this.props;
    const authorSlug = entry.getIn(['data', 'author']);
    const authorData = fieldsMetaData.getIn(['author', 'authors', authorSlug]).toJS();

    return h(
      'article',
      { style: { padding: '20px', maxWidth: '600px' } },
      h('h1', {}, entry.getIn(['data', 'title'])),

      authorData &&
        h(
          'div',
          { style: { marginBottom: '20px', fontStyle: 'italic', color: '#666' } },
          'By ',
          h('strong', {}, authorData.name),
        ),

      h('div', { style: { marginTop: '20px' } }, widgetFor('content')),

      h(
        'footer',
        { style: { marginTop: '40px', paddingTop: '20px', borderTop: '1px solid #eee' } },
        h('small', {}, `Published: ${entry.getIn(['data', 'date'])}`),
      ),
    );
  },
});

CMS.registerPreviewTemplate('posts', ArticlePreview);
jsx
export default class ArticlePreview extends React.Component {
  render() {
    const { entry, fieldsMetaData, widgetFor } = this.props;
    const authorSlug = entry.getIn(['data', 'author']);
    const authorData = fieldsMetaData.getIn(['author', 'authors', authorSlug]).toJS();

    return (
      <article style={{ padding: '20px', maxWidth: '600px' }}>
        <h1>{entry.getIn(['data', 'title'])}</h1>

        {authorData && (
          <div style={{ marginBottom: '20px', fontStyle: 'italic', color: '#666' }}>
            By <strong>{authorData.name}</strong>
          </div>
        )}

        <div style={{ marginTop: '20px' }}>{widgetFor('content')}</div>

        <footer style={{ marginTop: '40px', paddingTop: '20px', borderTop: '1px solid #eee' }}>
          <small>Published: {entry.getIn(['data', 'date'])}</small>
        </footer>
      </article>
    );
  }
}

CMS.registerPreviewTemplate('posts', ArticlePreview);

Using getAsset

Display multiple images from a gallery field with proper asset resolution:

js
const GalleryPreview = createClass({
  render: function () {
    const { entry, getAsset } = this.props;
    const images = entry.getIn(['data', 'gallery']) ?? [];

    return h(
      'div',
      { style: { padding: '20px' } },
      h('h2', {}, 'Image Gallery'),
      h(
        'div',
        { style: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '10px' } },
        images.map(function (imagePath, index) {
          const asset = getAsset(imagePath);
          return asset
            ? h('img', {
                key: index,
                src: asset.url,
                alt: `Gallery image ${index + 1}`,
                style: { width: '100%', height: 'auto', borderRadius: '4px' },
              })
            : null;
        }),
      ),
    );
  },
});

CMS.registerPreviewTemplate('portfolio', GalleryPreview);
jsx
export default class GalleryPreview extends React.Component {
  render() {
    const { entry, getAsset } = this.props;
    const images = entry.getIn(['data', 'gallery']) ?? [];

    return (
      <div style={{ padding: '20px' }}>
        <h2>Image Gallery</h2>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '10px' }}>
          {images.map((imagePath, index) => {
            const asset = getAsset(imagePath);
            return asset ? (
              <img
                key={index}
                src={asset.url}
                alt={`Gallery image ${index + 1}`}
                style={{ width: '100%', height: 'auto', borderRadius: '4px' }}
              />
            ) : null;
          })}
        </div>
      </div>
    );
  }
}

CMS.registerPreviewTemplate('portfolio', GalleryPreview);

Using getCollection

Display related entries from another collection:

js
const ProductPreview = createClass({
  getInitialState: function () {
    return { relatedProducts: [] };
  },

  componentDidMount: function () {
    const { getCollection } = this.props;
    const relatedSlugs = this.props.entry.getIn(['data', 'related_products']) ?? [];

    getCollection('products').then((products) => {
      const related = products.filter(function (product) {
        const slug = product.get('slug');
        return relatedSlugs.includes(slug);
      });
      this.setState({ relatedProducts: related });
    });
  },

  render: function () {
    const { entry } = this.props;
    const { relatedProducts } = this.state;

    return h(
      'div',
      { style: { padding: '20px' } },
      h('h1', {}, entry.getIn(['data', 'title'])),
      h('p', {}, entry.getIn(['data', 'description'])),

      relatedProducts.length > 0 &&
        h(
          'div',
          { style: { marginTop: '30px', borderTop: '1px solid #ddd', paddingTop: '20px' } },
          h('h3', {}, 'Related Products'),
          h(
            'ul',
            {},
            relatedProducts.map(function (product, index) {
              return h('li', { key: index }, product.getIn(['data', 'title']));
            }),
          ),
        ),
    );
  },
});

CMS.registerPreviewTemplate('products', ProductPreview);
jsx
export default class ProductPreview extends React.Component {
  constructor(props) {
    super(props);
    this.state = { relatedProducts: [] };
  }

  componentDidMount() {
    const { getCollection } = this.props;
    const relatedSlugs = this.props.entry.getIn(['data', 'related_products']) ?? [];

    // Fetch all products and filter for related ones
    getCollection('products').then((products) => {
      const related = products.filter((product) => {
        const slug = product.get('slug');
        return relatedSlugs.includes(slug);
      });
      this.setState({ relatedProducts: related });
    });
  }

  render() {
    const { entry } = this.props;
    const { relatedProducts } = this.state;

    return (
      <div style={{ padding: '20px' }}>
        <h1>{entry.getIn(['data', 'title'])}</h1>
        <p>{entry.getIn(['data', 'description'])}</p>

        {relatedProducts.length > 0 && (
          <div style={{ marginTop: '30px', borderTop: '1px solid #ddd', paddingTop: '20px' }}>
            <h3>Related Products</h3>
            <ul>
              {relatedProducts.map((product, index) => (
                <li key={index}>{product.getIn(['data', 'title'])}</li>
              ))}
            </ul>
          </div>
        )}
      </div>
    );
  }
}

CMS.registerPreviewTemplate('products', ProductPreview);