Quick reference for creating JSON Rich Text Editor plugins using the new simplified approach.
import ContentstackAppSDK, { PluginBuilder } from '@contentstack/app-sdk';
// Create a simple plugin
const boldPlugin = new PluginBuilder('bold-plugin')
.title('Bold')
.elementType('inline')
.on('exec', (rte) => {
rte.addMark('bold', true);
})
.build();
// Register the plugin
ContentstackAppSDK.registerRTEPlugins(boldPlugin);For text formatting (bold, italic, etc.)
const italicPlugin = new PluginBuilder('italic')
.title('Italic')
.elementType('inline')
.display(['toolbar', 'hoveringToolbar'])
.on('exec', (rte) => {
rte.addMark('italic', true);
})
.build();For block-level elements (headings, paragraphs, etc.)
const headingPlugin = new PluginBuilder('heading')
.title('Heading')
.elementType('block')
.render(({ children, attrs }) => (
<h2 style={{ color: attrs.color || 'black' }}>
{children}
</h2>
))
.on('exec', (rte) => {
rte.insertNode({
type: 'heading',
attrs: { level: 2 },
children: [{ text: 'New Heading' }]
});
})
.build();For self-closing elements (images, embeds, etc.)
const imagePlugin = new PluginBuilder('image')
.title('Image')
.elementType('void')
.render(({ attrs }) => (
<img
src={attrs.src}
alt={attrs.alt || 'Image'}
style={{ maxWidth: '100%' }}
/>
))
.on('exec', (rte) => {
const src = prompt('Enter image URL:');
if (src) {
rte.insertNode({
type: 'image',
attrs: { src },
children: [{ text: '' }]
});
}
})
.build();new PluginBuilder('plugin-id')
.title('Plugin Name') // Toolbar button text
.icon(<CustomIcon />) // Button icon (React element)
.elementType('block') // 'inline' | 'block' | 'void' .display(['toolbar']) // Show in main toolbar only
.display(['hoveringToolbar']) // Show in hover toolbar only
.display(['toolbar', 'hoveringToolbar']) // Show in both .on('exec', (rte) => {}) // Button click
.on('keydown', ({ event, rte }) => {}) // Key press
.on('paste', ({ rte, preventDefault }) => {}) // Paste event .render(ComponentFunction) // Custom render component
.shouldOverride((element) => boolean) // Override existing elements
.configure(async (sdk) => {}) // Dynamic configuration.on('exec', (rte) => {
// Insert text
rte.insertText('Hello World');
// Add formatting
rte.addMark('bold', true);
// Insert node
rte.insertNode({
type: 'custom-element',
attrs: { id: 'unique-id' },
children: [{ text: 'Content' }]
});
}).on('keydown', ({ event, rte }) => {
if (event.key === 'Enter' && event.ctrlKey) {
event.preventDefault();
// Custom enter behavior
rte.insertBreak();
}
})Create grouped plugins in a dropdown menu:
const mediaContainer = new PluginBuilder('media-dropdown')
.title('Media')
.icon(<MediaIcon />)
.addPlugins(
imagePlugin,
videoPlugin,
audioPlugin
)
.build();ContentstackAppSDK.registerRTEPlugins(myPlugin);ContentstackAppSDK.registerRTEPlugins(
boldPlugin,
italicPlugin,
headingPlugin,
imagePlugin
);// Register plugins first (captures RTE context)
await ContentstackAppSDK.registerRTEPlugins(myPlugin);
// Then initialize SDK (gets enhanced context)
const sdk = await ContentstackAppSDK.init();const youtubePlugin = new PluginBuilder('youtube')
.title('YouTube')
.elementType('void')
.render(({ attrs }) => (
<iframe
width="560"
height="315"
src={`https://www.youtube.com/embed/${attrs.videoId}`}
frameBorder="0"
allowFullScreen
/>
))
.on('exec', (rte) => {
const url = prompt('Enter YouTube URL:');
const videoId = extractVideoId(url);
if (videoId) {
rte.insertNode({
type: 'youtube',
attrs: { videoId },
children: [{ text: '' }]
});
}
})
.build();const smartQuotePlugin = new PluginBuilder('smart-quote')
.title('Smart Quotes')
.elementType('inline')
.on('keydown', ({ event, rte }) => {
if (event.key === '"') {
event.preventDefault();
const isStart = rte.selection.isAtStart();
rte.insertText(isStart ? '"' : '"');
}
})
.build();const configurablePlugin = new PluginBuilder('configurable')
.title('Dynamic Plugin')
.configure(async (sdk) => {
const config = await sdk.getConfig();
return {
title: config.customTitle || 'Default Title',
icon: config.customIcon || <DefaultIcon />
};
})
.on('exec', (rte) => {
// Plugin logic using dynamic config
})
.build();- Use semantic IDs:
'heading-h2'instead of'plugin1' - Provide clear titles: Users see these in the toolbar
- Handle edge cases: Check for selection, validate inputs
- Use TypeScript: Better development experience
- Test thoroughly: Different content structures, browser compatibility
const oldPlugin = new RTEPlugin('my-plugin', (rte) => ({
title: 'My Plugin',
icon: <Icon />,
display: ['toolbar'],
elementType: ['block'],
render: MyComponent
}));
oldPlugin.on('exec', handler);const newPlugin = new PluginBuilder('my-plugin')
.title('My Plugin')
.icon(<Icon />)
.display(['toolbar'])
.elementType('block')
.render(MyComponent)
.on('exec', handler)
.build();Happy plugin building! 🚀