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:
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. Returnsundefinedif 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 queryslug(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 globaldocumentto manipulate the preview DOM.window(Window): The preview pane iframe's Window object. Use this instead of the globalwindowto 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 forReact.createElement(), used to create React elements in the non-JSX examplescreateClass— 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 valuesentry.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:
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);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:
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);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:
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);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:
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);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:
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);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:
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);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);