---
title: "Troubleshooting Guide"
url: "https://glokki.com/docs/troubleshooting"
locale: "en"
updated: "2026-10-26T23:25:41.011Z"
---

# Troubleshooting Guide

Common issues when integrating glokki and how to fix them.

This guide covers the most frequent problems tenant developers encounter when setting up the SDK, configuring iframe embedding, and testing blocks in the visual editor. For initial setup, see the [Integration Guide](/docs/integration). For block-specific issues, see the [Block Development Guide](/docs/block-development).

---

## Table of Contents

1. [Iframe Blocked (CSP Misconfiguration)](#1-iframe-blocked-csp-misconfiguration)
2. [Handshake Failed (Origin Mismatch)](#2-handshake-failed-origin-mismatch)
3. [Editor Disconnected (Token Expired)](#3-editor-disconnected-token-expired)
4. ["Unknown Block" Placeholder](#4-unknown-block-placeholder)
5. [Iframe Not Loading (Network/CORS)](#5-iframe-not-loading-networkcors)
6. [Block Type Not Registered](#6-block-type-not-registered)
7. [Images Load From Storage Origin Instead of `/cdn/`](#7-images-load-from-storage-origin-instead-of-cdn)
8. [Debugging with DevTools](#8-debugging-with-devtools)

---

## 1. Iframe Blocked (CSP Misconfiguration)

### Symptom

The visual editor shows a blank iframe or a broken frame icon. The browser console displays:

```
Refused to frame 'https://your-app.com' because an ancestor violates the
following Content Security Policy directive: "frame-ancestors 'self'"
```

Or the older variant:

```
Refused to display 'https://your-app.com' in a frame because it set
'X-Frame-Options' to 'SAMEORIGIN'.
```

### Cause

The CMS loads your tenant app inside an iframe. By default, browsers block cross-origin framing. Your app must explicitly allow the CMS origin to embed it — but only when the page is loaded in editor mode (`?_editor=1`).

### Fix

Add middleware that sets `Content-Security-Policy: frame-ancestors` when the `_editor` query parameter is present. Here is a minimal version:

**Next.js middleware:**

```ts
// middleware.ts
import { type NextRequest, NextResponse } from "next/server"

const CMS_ORIGIN = new URL(process.env.CMS_BASE_URL || "http://localhost:3000")
  .origin

export function middleware(request: NextRequest) {
  const isEditorMode = request.nextUrl.searchParams.get("_editor") === "1"
  const response = NextResponse.next()

  if (isEditorMode) {
    // Allow the CMS to embed this page
    response.headers.set(
      "Content-Security-Policy",
      `frame-ancestors 'self' ${CMS_ORIGIN}`
    )
    // Remove X-Frame-Options — it takes precedence over CSP in older browsers
    response.headers.delete("X-Frame-Options")
  } else {
    // Block framing on public pages to prevent clickjacking
    response.headers.set("X-Frame-Options", "SAMEORIGIN")
  }

  return response
}
```

**If you already have a CSP:** The example above replaces the entire `Content-Security-Policy` header. If your app ships existing CSP directives (e.g., `script-src`, `img-src`), you must merge `frame-ancestors` into your existing policy rather than replacing it. Only add the `frame-ancestors` directive — do not drop your other directives on editor routes.

### Checklist

- `CMS_BASE_URL` matches the actual CMS origin (protocol + host + port)
- `X-Frame-Options` is deleted in editor mode (it overrides `frame-ancestors` in older browsers)
- The middleware runs on all routes that serve editor-embeddable pages
- If you already have a CSP, merge `frame-ancestors` into it rather than replacing the whole header
- If you use a reverse proxy (Nginx, Cloudflare), check that it is not adding its own `X-Frame-Options` header

---

## 2. Handshake Failed (Origin Mismatch)

### Symptom

The iframe loads your app, but the Component Library is empty and the page shows no blocks. No errors appear in the console. The editor appears "stuck" — the page renders but there is no editing UI.

### Cause

The postMessage bridge uses **strict origin matching** for security. Messages from origins that do not exactly match the expected origin are silently dropped — no errors are logged (this is by design to avoid leaking security information). Common mismatches:

| Expected                  | Actual                        | Problem                                                                                |
| ------------------------- | ----------------------------- | -------------------------------------------------------------------------------------- |
| `http://localhost:3001`   | `http://localhost:3000`       | Wrong port                                                                             |
| `https://app.example.com` | `http://app.example.com`      | HTTP vs HTTPS                                                                          |
| `https://app.example.com` | `https://www.app.example.com` | Subdomain mismatch                                                                     |
| `https://app.example.com` | `https://app.example.com:443` | Explicit port (browsers normalise this — usually not an issue, but some proxies don't) |

### Fix

1. **Check `frontendUrl` in CMS settings.** Go to **Dashboard → Settings → Visual Editor** and verify the **Frontend URL** matches your tenant app's origin exactly (protocol + host + port). For local development this is typically `http://localhost:3001`.

2. **Verify the CMS origin.** The `_editorOrigin` query parameter is set by the CMS when it builds the iframe URL (via `buildEditorIframeUrl()`). If the value is wrong, the issue is on the CMS side — check that you are accessing the CMS at its canonical URL (the origin the CMS derives from the incoming request headers). If you access the CMS at `http://127.0.0.1:3000` but the tenant app expects `http://localhost:3000`, the origins will not match.

3. **Verify both URLs in the browser address bar.** When the editor opens, the iframe URL will contain query parameters like:
   ```
   https://your-app.com/page?_editor=1&_editorOrigin=https%3A%2F%2Fcms.example.com&_editorToken=...
   ```
   Decode the `_editorOrigin` value and confirm it matches the CMS URL in your browser's main address bar.

### How to Debug Silent Origin Rejections

Since origin mismatches are silently dropped, use the browser DevTools to inspect `postMessage` traffic:

1. Open DevTools → Console
2. Add a temporary listener in the CMS window:
   ```js
   window.addEventListener("message", (e) =>
     console.log("msg from:", e.origin, e.data)
   )
   ```
3. Reload the editor. You should see `handshake:request` messages. Check the `e.origin` value — if it does not match what the CMS expects, the messages are being dropped.

---

## 3. Editor Disconnected (Token Expired)

### Symptom

The visual editor works initially, but after a long editing session the editor stops responding to changes. The CMS may show a "session expired" or "reconnect" prompt.

### Cause

Editor sessions use short-lived tokens (15-minute TTL) passed via the `_editorToken` query parameter. The CMS proactively sends `token:refresh` messages before the token expires, and the SDK updates the URL via `history.replaceState()` so that iframe reloads (including HMR) pick up the fresh token.

If the refresh mechanism fails — for example, if the postMessage bridge is broken (see [Origin Mismatch](#2-handshake-failed-origin-mismatch)) — the token will expire and the session will disconnect.

### Fix

1. **Ensure the handshake is working.** Token refresh relies on the same postMessage bridge as all other editor communication. If the handshake never completes, tokens cannot be refreshed. See [Handshake Failed](#2-handshake-failed-origin-mismatch).

2. **Don't strip query parameters.** If your app's routing strips or rewrites query parameters (`_editor`, `_editorOrigin`, `_editorToken`), the SDK cannot detect editor mode or receive token updates. Ensure these parameters pass through unchanged.

3. **Reload the editor.** If a session expires, close the editor tab and reopen the page in the visual editor. The CMS generates a fresh token on each editor open.

### Note

Under normal operation, token expiry should not happen — the CMS refreshes tokens automatically. If it happens repeatedly, it indicates a broken postMessage bridge rather than a token-specific problem.

---

## 4. "Unknown Block" Placeholder

### Symptom

Instead of your block's content, the editor shows a dashed-border placeholder with:

```
❓
your_block_type
Component not registered
```

### Cause

The block type in the page's component tree does not have a corresponding entry in the component registry. This happens when:

- The block was added in the CMS but the tenant app has not registered a component for that type
- The block type string has a typo (e.g., `"hero-banner"` vs `"heroBanner"`)
- The block was removed from the registry but pages still reference it
- The component `Map` used by `<CMSPageEditor>` is missing the entry (even if `createBlockRegistry()` has it)

### Fix

1. **Verify the block is in `createBlockRegistry()`.** Ensure your `defineBlock()` result is included in the array passed to `createBlockRegistry()`.

2. **Verify the component Map includes the block.** The `registry` Map passed to `<VisualEditorProvider>` must also contain the block type → component mapping:

   ```ts
   export const registry: Map<
     string,
     ComponentType<Record<string, unknown>>
   > = new Map(
     blockRegistry
       .getTypes()
       .map((type) => [
         type,
         blockRegistry.getComponent(type) as ComponentType<
           Record<string, unknown>
         >,
       ])
   )
   ```

3. **Check the type string.** Block types are case-sensitive. Ensure the `type` in `defineBlock()` matches what the CMS saved in the component tree. Use DevTools to inspect the `tree:update` message payload and compare `node.type` values.

4. **Duplicate type error.** If you see:
   ```
   createBlockRegistry: duplicate block type "hero". Each block type must be unique within a registry.
   ```
   Two `defineBlock()` calls use the same `type` value. Rename one of them.

---

## 5. Iframe Not Loading (Network/CORS)

### Symptom

The visual editor shows a loading spinner indefinitely, or the iframe area is completely blank. The browser console may show network errors.

### Cause

The CMS cannot reach your tenant app at the configured `frontendUrl`. This is a network-level problem, not a postMessage problem.

### Fix

1. **Verify your tenant app is running.** Open the `frontendUrl` directly in a new browser tab. If the page doesn't load, your app is down or unreachable.

2. **Check for port conflicts.** If both the CMS and tenant app run locally, ensure they use different ports (e.g., CMS on `:3000`, tenant app on `:3001`).

3. **Check firewall/VPN.** Corporate firewalls or VPNs can block localhost-to-localhost connections or cross-origin iframe loads.

4. **HTTPS in development.** If the CMS runs on HTTPS (even with a self-signed cert), the tenant app must also use HTTPS. Mixed content (HTTPS page embedding HTTP iframe) is blocked by browsers.

### Note on CORS

The visual editor uses `postMessage` for communication, not HTTP requests between origins. You do **not** need to configure CORS headers (`Access-Control-Allow-Origin`) for the editor to work. The only cross-origin header that matters is `Content-Security-Policy: frame-ancestors` (see [Iframe Blocked](#1-iframe-blocked-csp-misconfiguration)).

CORS headers are only relevant if your tenant app makes direct API calls to the CMS (e.g., fetching content from the Delivery API on the client side). Server-side API calls are not subject to CORS.

---

## 6. Block Type Not Registered

### Symptom

The Component Library in the CMS sidebar does not show your block. You can see other blocks, but your new one is missing.

### Cause

The block's definition was not included in the handshake. The Component Library is populated from block definitions sent during the `handshake:request` message — if a block is not in the registry at handshake time, it won't appear.

### Fix

1. **Add the block to `createBlockRegistry()`.** The block must be included in the array:

   ```ts
   import { YourNewBlock } from "./your-new-block"

   export const blockRegistry = createBlockRegistry([
     HeroBlock,
     YourNewBlock, // ← add here
   ])
   ```

2. **Pass `blockRegistry` to `<VisualEditorProvider>`.** The provider needs both the component Map (`registry`) and the typed block registry (`blockRegistry`) to send full block definitions in the v2 handshake:

   ```tsx
   <VisualEditorProvider
     registry={registry}
     blockRegistry={blockRegistry}
     fallbackTree={page.componentTree}
     pageSlug="/about"
   >
   ```

3. **Reload the editor.** The handshake happens once when the iframe loads. After changing the registry, you must reload the page in the visual editor (close and reopen, or use the browser refresh).

4. **Check for `defineBlock()` errors.** If the schema is not a `z.object()` at the root, `defineBlock()` may throw or produce invalid field descriptors. Check the browser console for errors.

### Missing Provider Error

If you see this error in the console:

```
useVisualEditor must be used within a <VisualEditorProvider>.
```

Your page component is not wrapped in the provider. Ensure `<VisualEditorProvider>` is an ancestor of `<CMSPageEditor>` and any component that calls `useVisualEditor()`.

---

## 7. Images Load From Storage Origin Instead of `/cdn/`

### Symptom

Published pages render `<img>` tags pointing at the S3/MinIO/storage host (e.g., `http://s3.example.com/bucket/tenants/<uuid>/foo.jpg`) instead of your tenant origin (`/cdn/foo.jpg`). The image may still load, but:

- The storage bucket name and tenant UUID prefix are exposed to visitors.
- Your CDN/edge cannot cache or invalidate the asset — every request hits storage directly.
- Responsive variants (`webpSrcset` / `avifSrcset`), focal-point-derived `objectPosition`, and asset dimensions are missing from the rendered HTML.

### Cause

The CMS rewrites every tenant-storage URL in a block's props to `/cdn/...` — whatever the prop is called and however deeply it is nested. When that doesn't happen, it is one of three things, in order of likelihood:

1. **The image is set in a per-viewport override.** Tablet/mobile overrides are not rewritten yet, so an image picked only there ships the absolute storage URL.
2. **The tenant app has no `/cdn/[...path]/route.ts`.** Without the proxy route, `/cdn/...` requests would 404, so the rewrite to relative paths would break the page.
3. **You're looking at the props panel right after picking an image.** The picker writes an absolute URL into the working tree; enrichment is read-time only. Save and reload the editor — the value collapses to `/cdn/...` on the next SSR pass.

### Fix

- For cause (1): set the image on the base (desktop) props and keep viewport overrides for non-image props. See [Query Parameters](/docs/asset-cdn-proxy#6-query-parameters) for what the rewritten URL carries.
- For cause (2): add the proxy route. See the full walkthrough in [Asset CDN Proxy](/docs/asset-cdn-proxy#3-quickstart).
- For cause (3): autosave + reload. No code change needed.

### Verification

After the fix, the published page's `<img src>` should be `/cdn/<filename>`, and a `curl -I` against that URL on your tenant origin should return `200 OK` with `Cache-Control` headers coming from the CMS.

---

## 8. Debugging with DevTools

### Inspect postMessage Traffic

The visual editor communicates via `window.postMessage`. All messages use the `__editorCms: true` namespace guard. To monitor traffic:

1. Open DevTools → Console in the **CMS window** (not the iframe)
2. Paste this snippet:
   ```js
   window.addEventListener("message", (e) => {
     if (e.data?.__editorCms) {
       console.log(`[editor] ${e.data.type}`, e.data.payload)
     }
   })
   ```
3. Reload the editor page. You should see the handshake sequence:
   ```
   [editor] handshake:request  { sdkVersion: "0.0.1", pageSlug: "/about", registeredBlockTypes: [...], registeredBlocks: [...] }
   [editor] ready              { timestamp: 1234567890 }
   ```
4. Interact with the editor — you'll see `block:clicked`, `block:props-update`, `tree:update`, etc.

### Verify Editor Mode Detection

The SDK detects editor mode from URL query parameters. Check that your iframe URL contains all three parameters:

| Parameter       | Example                   | Purpose                              |
| --------------- | ------------------------- | ------------------------------------ |
| `_editor`       | `1`                       | Signals editor mode is active        |
| `_editorOrigin` | `https://cms.example.com` | CMS origin for postMessage targeting |
| `_editorToken`  | `eyJ...`                  | Short-lived session token            |

If `_editor=1` is present but `_editorOrigin` is missing, the SDK will **not** enter editor mode (both are required).

### Check the Handshake Response

After `handshake:request`, the CMS should reply with `handshake:ack`. If you see the request but no ack:

- The CMS did not receive the message (origin mismatch — see [section 2](#2-handshake-failed-origin-mismatch))
- The CMS received it but the `frontendUrl` does not match the iframe's origin
- The CMS iframe handler has an error (check the CMS console)

### Common DevTools Filters

Filter console output to focus on editor messages:

- **Chrome:** Type `[editor]` or `__editorCms` in the Console filter box
- **Firefox:** Use the filter input above the console
- **Safari:** Use the filter field in the Console tab

---

## Quick Reference

| Problem                                  | Most Likely Cause        | First Step                                             |
| ---------------------------------------- | ------------------------ | ------------------------------------------------------ |
| Blank iframe                             | CSP blocking             | Check browser console for "Refused to frame"           |
| Editor loads but no blocks               | Origin mismatch          | Verify `frontendUrl` matches tenant app URL            |
| Editor stops responding                  | Token expiry             | Close and reopen the editor                            |
| "Unknown Block" placeholder              | Missing registry entry   | Add block to `createBlockRegistry()` and component Map |
| Block not in Component Library           | Not in handshake payload | Add to `createBlockRegistry()`, reload editor          |
| "useVisualEditor must be used within..." | Missing provider         | Wrap page in `<VisualEditorProvider>`                  |
| Iframe loads wrong page                  | Incorrect `frontendUrl`  | Update Frontend URL in CMS settings                    |

---

## Next Steps

- [Integration Guide](/docs/integration) — full setup from install to visual editing
- [Block Development Guide](/docs/block-development) — creating and testing custom blocks
- [Editor Preview Route](/docs/editor-preview-route) — the route the visual editor loads, including CSP setup
