glokki
View .md

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. For block-specific issues, see the Block Development Guide.


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:

// 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:

ExpectedActualProblem
http://localhost:3001http://localhost:3000Wrong port
https://app.example.comhttp://app.example.comHTTP vs HTTPS
https://app.example.comhttps://www.app.example.comSubdomain mismatch
https://app.example.comhttps://app.example.com:443Explicit 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:
    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) — 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. 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:

    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).

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:

    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:

    <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 for what the rewritten URL carries.
  • For cause (2): add the proxy route. See the full walkthrough in Asset CDN Proxy.
  • 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:
    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:

ParameterExamplePurpose
_editor1Signals editor mode is active
_editorOriginhttps://cms.example.comCMS origin for postMessage targeting
_editorTokeneyJ...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)
  • 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

ProblemMost Likely CauseFirst Step
Blank iframeCSP blockingCheck browser console for "Refused to frame"
Editor loads but no blocksOrigin mismatchVerify frontendUrl matches tenant app URL
Editor stops respondingToken expiryClose and reopen the editor
"Unknown Block" placeholderMissing registry entryAdd block to createBlockRegistry() and component Map
Block not in Component LibraryNot in handshake payloadAdd to createBlockRegistry(), reload editor
"useVisualEditor must be used within..."Missing providerWrap page in <VisualEditorProvider>
Iframe loads wrong pageIncorrect frontendUrlUpdate Frontend URL in CMS settings

Next Steps