---
title: Manage the Visual Editor
description: Storyblok’s Visual Editor pairs WYSIWYG editing with full freedom to customize your frontend. Learn how it works and how to connect your project.
url: https://www.storyblok.com/docs/concepts/visual-editor
---

# Manage the Visual Editor

Most CMSs require users to choose between integration and customization. Seamless integration with the CMS often means less freedom to customize your website, or vice versa.

Storyblok offers the best of both worlds: a WYSIWYG editing experience embedded directly in the Visual Editor, with complete freedom to customize both the website’s backend and frontend.

> [!TIP]
> Learn how to create and manage content in the [Visual Editor manual](/docs/manuals/visual-editor).

The Visual Editor supports various interactions:

-   Click on a block in the editor to scroll to the corresponding element in the preview area, or click on an element in the preview to open the block in the editor
-   Orient yourself with outlines that appear around block elements in the preview area
-   Click elements in the preview area and use context menus to navigate between blocks
-   Make changes in the editor and view the updated content in real-time

These features deliver an intuitive experience for everyone on your team—from sales reps to HR managers, developers, and marketers.

Provide this seamless editing using the following model:

## Implementation

1.  **Fetch draft content**
    
    To render a preview in the editor, fetch the draft version of the content. Learn more about [creating a preview deployment](https://www.storyblok.com/tp/create-preview-production-environments-and-deploy).
    
    ```javascript
    import { storyblokInit, apiPlugin } from "@storyblok/js";
    
    const { storyblokApi } = storyblokInit({
      accessToken: "YOUR_ACCESS_TOKEN",
      use: [apiPlugin],
    });
    
    // Fetch content
    const { data } = await storyblokApi.get("cdn/stories", {
      version: "draft",
    });
    ```
    
    The draft version of each story’s API response attaches a private `_editable` property to every block. This property enables live editing and instructs the Visual Editor how to handle the elements on the frontend:
    
    ```json
    "body": [
      {
        "_uid": "2db95a85-e979-da7f-895b-c3a41fbc5f8d",
        "component": "page",
        "_editable": "\u003C!--#storyblok ... --\u003E"
    // Shortened for brevitiy
      }
    ]
    ```
    
    The `_editable` property contains a string of encoded JSON. When extracted and parsed, the JSON has four properties:
    
    -   `name`: the block’s name
    -   `space`: the numeric space ID
    -   `uid`: the block’s unique ID
    -   `id`: the numeric story ID
    
    > [!TIP]
    > Learn the difference between [`uuid`, `id`, and `_uid`](/docs/concepts/content-modeling#identifiers-for-content-references).
    
    Add these properties to your HTML so the Visual Editor can access them.
    
2.  **Add HTML attributes**
    
    Each block has two native data attributes to extract the values of the `_editable` property:
    
    -   `data-blok-c` contains the stringified JSON
    -   `data-blok-uid` contains the ID and UID in the pattern `id-uid`
    
    Storyblok’s SDKs include utilities to format the data attributes:
    
    ```javascript
    const editableOptions = storyblokEditable(blok);
    
    const element = `
      <div
        class="storyblok__outline"
        data-blok-c="${editableOptions["data-blok-c"]}"
        data-blok-uid="${editableOptions["data-blok-uid"]}"
      <!-- Content <!-- Content <!-- Content <!-- Content -->
     </div>`;
    ```
    
    Finally, connect the frontend to the Visual Editor using the `StoryblokBridge`.
    
3.  **Enable the preview bridge**
    
    The [@storyblok/preview-bridge](/docs/libraries/js/preview-bridge) handles most of the heavy lifting required for live editing.
    
    Storyblok hosts the script on a dedicated CDN:
    
    ```bash
    https://app.storyblok.com/f/storyblok-v2-latest.js
    ```
    
    Either load it manually via the URL above, or use one of Storyblok’s frontend SDKs, such as [@storyblok/js](/docs/libraries/js/js-sdk):
    
    ```javascript
    import { storyblokInit, apiPlugin } from "@storyblok/js";
    import { storyblokInit, apiPlugin, useStoryblokBridge } from "@storyblok/js";
    
    const { storyblokApi } = storyblokInit({
      accessToken: "YOUR_ACCESS_TOKEN",
      use: [apiPlugin],
    });
    
    // Fetch content
    const { data } = await storyblokApi.get("cdn/stories", {
      version: "draft",
    });
    
    // Activate the StoryblokBridge
    useStoryblokBridge(data.story.id);
    ```
    
    Once activated, the bridge reloads the page each time the editor records a `save` or `publish` event.
    
    Customize and adjust the bridge’s behavior with a manual implementation of the `StoryblokBridge` class. Learn more about the supported events and method in [the package reference](/docs/libraries/js/preview-bridge).
    
    > [!NOTE]
    > If you write custom logic for bridge events, handle slug changes to avoid a `404` error when the page refreshes.
    
    The bridge is responsible for displaying context menus when the user clicks a block.
    
    -   When `save` and `publish` events are configured, the menu display navigation actions.
    -   When the `input` event is configured, the menu also displays editing actions.
    
    The website is now ready to communicate with Storyblok’s Visual Editor.
    

## Configure URLs

The Visual Editor loads your webpage in an iframe. To view it, open **Settings** → **Visual Editor** and define a **Preview URL**.

> [!NOTE]
> If the path to your webpage is different from the associated story’s slug, open **Config** in the Visual Editor, and define a **Real path**. This doesn’t affect the story’s endpoint or slug.

The preview environment can be a dedicated deployment that builds from the same codebase as the production deployment, and loads preview functionality based on variables.

To learn more, follow the [How to Create Preview & Production Environments and Deploy Your Website](https://www.storyblok.com/tp/create-preview-production-environments-and-deploy) tutorial.

### Preview slug

When the Visual Editor loads the page, it builds a URL from the base domain (`example.com`) and the story’s full slug (`folder/example-uid`).

It also appends several parameters to the URL:

-   `_storyblok`: the numeric story ID
-   `_storyblok_tk[space_id]`: the numeric space ID
-   `_storyblok_tk[timestamp]`: a UNIX timestamp
-   `_storyblok_tk[token]`: a validation token that combines `_storyblok_tk[space_id]`, `_storyblok_tk[timestamp]`, and the preview access token
-   `_storyblok_release`: the numeric release ID (requires the [Releases App](https://www.storyblok.com/apps/releases_only))
-   `_storyblok_lang`: the numeric language ID
-   `_storyblok_c` : the block’s name (content type)

The result would look like this:

```plaintext
https://example.com/folder/example-uid?_storyblok=580906535&_storyblok_c=page&_storyblok_version=&_storyblok_lang=default&_storyblok_release=0&_storyblok_rl=1732540047643&_storyblok_tk[space_id]=313862&_storyblok_tk[timestamp]=1732540047&_storyblok_tk[token]=9d25c03de1478da57e37a166a7c053ce0aff9234
```

## Security

Depending on your local or server configuration, the webpage embedded in the `iframe` might be blocked from the parent page (the Storyblok editor) or from the source page (your website).

If you encounter any problems when loading a webpage in the Visual Editor, add an SSL certificate or adjust the website’s [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP).

### SSL certificate

Storyblok’s security policy requires that you serve the preview over HTTPS (whether deployed or on localhost). To add an SSL certificate, use the tool that matches your web development framework:

-   For Vite-based frameworks, use [vite-plugin-mkcert](https://github.com/liuweiGL/vite-plugin-mkcert).
-   For Next.js, enable the [built-in HTTPS support](https://vercel.com/kb/guide/access-nextjs-localhost-https-certificate-self-signed).
-   For other frameworks, issue a local certificate with [mkcert](https://github.com/FiloSottile/mkcert), and serve the development server through an HTTPS proxy (such as [local-ssl-proxy](https://github.com/cameronhunter/local-ssl-proxy)).

Install `mkcert` with the package manager for your operating system:

-   macOS
    
    Use [Homebrew](https://brew.sh):
    
    ```bash
    brew install mkcert
    ```
    
-   Windows
    
    Use [Chocolatey](https://chocolatey.org/):
    
    ```powershell
    choco install mkcert
    ```

Next, create a local certificate for `localhost` and start the proxy. The following commands serve a development server running on port `3000` at `https://localhost:3010`:

```bash
mkcert -install
mkcert localhost
npm install --global local-ssl-proxy
local-ssl-proxy --source 3010 --target 3000 --cert localhost.pem --key localhost-key.pem
```

To use the certificate and proxy in your project, add the following to `package.json` and run `npm run proxy` alongside the development server:

package.json

```json
{
  "scripts": {
    "proxy": "mkcert -install && mkcert localhost && local-ssl-proxy --source 3010 --target 3000 --cert localhost.pem --key localhost-key.pem"
  }
}
```

Exclude the generated certificates from version control:

.gitignore

```plaintext
localhost.pem
localhost-key.pem
```

### Content Security Policy

The website’s CSP might block clients from loading pages inside an `<iframe>`. To allow embedding, add Storyblok to the `frame-ancestor` [directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-ancestors):

```plaintext
Content-Security-Policy: frame-ancestors https://app.storyblok.com
```

### Verify the preview token

A preview access token grants read access to draft content, so the frontend must confirm that the request originated from Storyblok before serving the draft.

The `_storyblok_tk` parameters described in the [Preview slug](#preview-slug) section above handle the check.

To regenerate the validation token, join the space ID, the preview access token, and the timestamp with colons, and then hash the result with SHA-1.

A request is valid when the hash matches `_storyblok_tk[token]` and the timestamp is less than one hour (3600 seconds) old:

```javascript
import crypto from "crypto";

const validationString = `${spaceId}:${previewToken}:${timestamp}`;
const validationToken = crypto.createHash("sha1").update(validationString).digest("hex");
const isValidPreviewRequest = validationToken === token && timestamp > Math.floor(Date.now() / 1000) - 3600;
```

> [!WARNING]
> Run the validation on the server and keep the preview access token out of client-side bundles.

To replace a preview access token without interrupting previews, accept both the current and the new token while the change rolls out:

1.  **Create another token**
    
    Select the space, open **Settings** → **Access Tokens**, and choose **\+ New Access Token**.
    
2.  **Accept both tokens**
    
    In your code, update the validation to match against either token, then deploy.
    
3.  **Delete the original token**
    
    In **Settings** → **Access Tokens**, select the original preview token, and delete it.
    
4.  **Remove the unused token**
    
    Delete the original token from your code on the next deploy.
    

## White labeling

Organization administrators can add a custom logo and custom domain in their organization settings, depending on their subscription plan. See the [pricing page](https://www.storyblok.com/pricing) for more details.

To override the Storyblok branding manually, a webpage can embed the Storyblok application. Storyblok serves a script that can run the entire web app at `https://app.storyblok.com/f/app-latest.js`. With this script, a developer can embed the app on any webpage and style the app interface as needed.

To embed the app on an empty page, create an index.html file:

index.html

```html
<!DOCTYPE html>
<html>
  <head>
    <title>Example Title</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="https://app.storyblok.com/f/app-latest.js" type="text/javascript"></script>
  </body>
</html>
```

Add CSS to override the Storyblok logo:

styles.css

```css
.logo-storyblok {
  display: none;
}

.logo-storyblok + *:before {
  display: block;
  height: 2em;
  margin-bottom: 1em;
  content: "";
  background-size: contain;
  background-repeat: no-repeat;
  background-image: url("https://raw.githubusercontent.com/mackenziechild/Acme-Logos/gh-pages/images/logo-1.svg");
}
```

## Pagination

-   [Previous: Traffic](/docs/concepts/traffic)
-   [Next: Webhooks](/docs/concepts/webhooks)
