Skip to content

Tool plugins extend Storyblok’s Visual Editor by adding custom tool windows as standalone applications embedded in iframes. They communicate with Storyblok via cross-window messaging and the Management API.

Content editors open tools directly from the Visual Editor. For example, the Autosave plugin gives editors access to actions relevant to the story they currently edit. Common use cases for tools include:

  • Analyzing content
  • Transforming content
  • Performing actions on content

Storyblok's plugin directory includes several other tools, including Export Translatable Fields and Import Translatable Fields.

Tool plugins are similar to space plugins but differ in key ways. Namely, tool plugins embed directly in the Visual Editor and can access the story while editing.

Your plugin is currently a standalone app embedded in Storyblok. To connect it to the Visual Editor, use the postMessage() method.

This allows the plugin to respond to changes in the story. The following snippets include some practical examples.

Tools appear stacked vertically in the Visual Editor, with iframes set at a specific height in pixels. When the height of your tool changes (for example, on initial load), notify the Visual Editor to adjust the iframe height:

window.parent.postMessage({
action: 'tool-changed',
tool: 'my-tool-plugin-name',
event: 'heightChange',
height: 500
}, '*');

In this example, height is set to 500 pixels. Use minimal vertical space to avoid blocking tools below, because the stacking order is managed automatically.

To auto-adjust height, use ResizeObserver and update the iframe's height when the document height changes. In the CSS, set the html element’s height to auto:

html {
height: auto;
overflow: hidden;
}

Update the component's code:

const handleResize = () => {
const height = document.body.clientHeight;
window.parent.postMessage({
action: 'tool-changed',
tool: 'my-tool-plugin-name',
event: 'heightChange',
height,
}, "*");
}
const observer = new ResizeObserver(handleResize);
observer.observe(document.body);

And use the disconnect() method to stop observing the component:

observer.disconnect();

To read the story in the Visual Editor, create an event listener:

const handleMessage = (e) => {
console.log(e.data);
};
window.addEventListener('message', handleMessage, false);

Remove the listener when the component unmounts:

window.removeEventListener('message', handleMessage, false);

And send a message to request context:

window.parent.postMessage({
action: 'tool-changed',
tool: 'my-tool-plugin-name',
event: 'getContext'
}, "*");

The JSON response from the Visual Editor contains the full story context:

{
"action": "get-context",
"story": {
"name": "hello444",
"uuid": "45bbd2c4-b418-4737-8f4f-d893ec1f7f10",
"content": { ... },
...
},
"language": ""
}

To reflect content updates in the Visual Editor preview, create a handleReloadStory function that reloads the preview. Call it after the story update, for example, via a button click.

const handleReloadStory = () => {
window.parent.postMessage({
action: 'content-changed',
tool: 'my-tool-plugin-name',
event: 'reloadPreview',
}, "*");
}

Server-side code uses http.IncomingMessage to interact with the Management API, and obtain the accessToken, region, and spaceId :

const { query } = req;
const sessionStore = sessionCookieStore(authHandlerParams)({ req, res });
const { accessToken, region, spaceId } = await sessionStore.get(query);

The @storyblok/app-extension-auth library automatically appends storyId and spaceId as query parameters, enabling the session store to access accessToken and region.

To update the current story via the Management API, send requests that reference these values:

new StoryblokClient({
oauthToken: `Bearer ${accessToken}`,
region,
})
.get(`spaces/${spaceId.toString(10)}/stories`, {
sort_by: 'updated_at:desc'
});

Was this page helpful?

What went wrong?

This site uses reCAPTCHA and Google's Privacy Policy (opens in a new window) . Terms of Service (opens in a new window) apply.