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_URLmatches the actual CMS origin (protocol + host + port)X-Frame-Optionsis deleted in editor mode (it overridesframe-ancestorsin older browsers)- The middleware runs on all routes that serve editor-embeddable pages
- If you already have a CSP, merge
frame-ancestorsinto 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-Optionsheader
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
-
Check
frontendUrlin 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 typicallyhttp://localhost:3001. -
Verify the CMS origin. The
_editorOriginquery parameter is set by the CMS when it builds the iframe URL (viabuildEditorIframeUrl()). 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 athttp://127.0.0.1:3000but the tenant app expectshttp://localhost:3000, the origins will not match. -
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
_editorOriginvalue 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:
- Open DevTools → Console
- Add a temporary listener in the CMS window:
window.addEventListener("message", (e) => console.log("msg from:", e.origin, e.data) ) - Reload the editor. You should see
handshake:requestmessages. Check thee.originvalue — 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
-
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.
-
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. -
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
Mapused by<CMSPageEditor>is missing the entry (even ifcreateBlockRegistry()has it)
Fix
-
Verify the block is in
createBlockRegistry(). Ensure yourdefineBlock()result is included in the array passed tocreateBlockRegistry(). -
Verify the component Map includes the block. The
registryMap 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> >, ]) ) -
Check the type string. Block types are case-sensitive. Ensure the
typeindefineBlock()matches what the CMS saved in the component tree. Use DevTools to inspect thetree:updatemessage payload and comparenode.typevalues. -
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 sametypevalue. 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
-
Verify your tenant app is running. Open the
frontendUrldirectly in a new browser tab. If the page doesn't load, your app is down or unreachable. -
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). -
Check firewall/VPN. Corporate firewalls or VPNs can block localhost-to-localhost connections or cross-origin iframe loads.
-
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
-
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 ]) -
Pass
blockRegistryto<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" > -
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).
-
Check for
defineBlock()errors. If the schema is not az.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-derivedobjectPosition, 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:
- 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.
- 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. - 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:
- Open DevTools → Console in the CMS window (not the iframe)
- Paste this snippet:
window.addEventListener("message", (e) => { if (e.data?.__editorCms) { console.log(`[editor] ${e.data.type}`, e.data.payload) } }) - 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 } - 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)
- The CMS received it but the
frontendUrldoes 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__editorCmsin 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 — full setup from install to visual editing
- Block Development Guide — creating and testing custom blocks
- Editor Preview Route — the route the visual editor loads, including CSP setup