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. You can use either JSX or non-JSX syntax to define the component — see the Writing React Components section for more details.

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.

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

With or without JSX

The following JSX examples assume you have a build step to transpile JSX to JavaScript. If you are not using JSX, see the non-JSX examples below. See Writing React Components for more details.

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

Showcase

Real-world examples of custom preview templates can be found in our showcase.