Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ with Contentstack Support.

| Symptom | What it actually is |
|---|---|
| The Visual Experience or builder-mode option is absent from navigation | The plan or org flag was never enabled for that stack. |
| The Visual Experience or builder-mode option is absent from navigation | First check the one gate the user can see: Live Preview enabled on the stack under Settings → Visual Experience. If it is on, the plan or org flag was never enabled for that stack. See `visual-experience-navigation-missing-for-the-stack` in faq-integration.md. |
| Custom Preview URLs or Open in New Tab are unavailable | Both are plan-gated. |

## Not Contentstack at all

| Symptom | What it actually is |
|---|---|
| A previously working `localhost` preview stops working, console shows a local network access error | Chrome's Local Network Access policy now blocks the app origin from framing localhost. Only the user can grant the permission, in the browser. |
| Preview pane blank, and the site sits behind platform password protection or third-party SSO | Those screens set `X-Frame-Options: DENY` on themselves. Contentstack cannot inject headers into a browser-originated iframe request. Workarounds, cheapest first: enable **Open in New Tab**, which renders the site outside the iframe so frame headers stop applying; otherwise disable protection on the preview deployment, or front the **preview deployment only** with a server-side proxy that replaces the header with `Content-Security-Policy: frame-ancestors 'self' https://app.contentstack.com` using the exact app host for the stack's region (for example `https://eu-app.contentstack.com`), not a `https://*.contentstack.com` wildcard (never just strip it: that makes the protected site frameable by any origin). A bypass token in the Base URL does not work: see `base-url-query-string-corrupts-preview-url` in faq-integration.md. |
| Preview pane blank, and the site sits behind platform password protection or third-party SSO | Those screens set `X-Frame-Options: DENY` on themselves. Contentstack cannot inject headers into a browser-originated iframe request. Workarounds, cheapest first: for Live Preview, enable **Open in New Tab**, which renders the site outside the iframe so frame headers stop applying (Visual Editor does not support it, so this does not help there); otherwise disable protection on the preview deployment, or front the **preview deployment only** with a server-side proxy that replaces the header with `Content-Security-Policy: frame-ancestors 'self' https://app.contentstack.com` using the exact app host for the stack's region (for example `https://eu-app.contentstack.com`), not a `https://*.contentstack.com` wildcard (never just strip it: that makes the protected site frameable by any origin). A bypass token in the Base URL does not work: see `base-url-query-string-corrupts-preview-url` in faq-integration.md. |
| Ad slots, consent widgets, or other host-dependent third-party scripts misbehave in the pane | Those scripts key off the top-level origin, which is the Contentstack app inside an iframe. Expected, not a preview defect. |

## How to hand it over
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ setup, use `setup-docs.md` instead.
url.hostname = CONTENTSTACK_CDN_HOST; // e.g. cdn.contentstack.io
}
```
For GraphQL the equivalent is swapping `graphql.contentstack.com` → `graphql-preview.contentstack.com` and adding the same two headers.
For GraphQL the equivalent is swapping `graphql.contentstack.com` → `graphql-preview.contentstack.com` and adding the same two headers. Keep the `access_token` (delivery token) and `Content-Type` headers on every request; the preview headers are added to them, not swapped in.
4. Bypass any cache layer for preview requests. Do not try to fix it with a smarter cache key: the hash is stable for the whole preview session, so any cache on this path serves the first response for the rest of it.
- **verification**: Open the entry in Live Preview, then in DevTools → Network confirm requests go to the `*-preview.contentstack.com` host, carry `preview_token` and `live_preview` headers, and return 200. Edit a field without saving; the preview must update. The Onboarding Check should reach **Setup Complete**.

Expand All @@ -43,8 +43,8 @@ setup, use `setup-docs.md` instead.
- **fix**:
1. Remove `X-Frame-Options` for the preview host and use CSP instead, allowing the Contentstack app origin for the stack's region: `Content-Security-Policy: frame-ancestors 'self' https://app.contentstack.com;` (for example `https://eu-app.contentstack.com` in EU). List the exact app host for each region the stack uses; avoid `https://*.contentstack.com`, which lets any Contentstack subdomain frame the site. `X-Frame-Options` has no allowlist syntax; a bare `SAMEORIGIN` always wins.
2. Apply the header on every route, not just the site root. A route that redirects to a different hostname than the configured Base URL will also fail.
3. If the deployment sits behind platform password protection, note that no header or query parameter can be injected into a browser-originated iframe request from Contentstack. Options: disable protection on the preview deployment, put a server-side proxy in front that injects the header, or use Live Preview → Open in New Tab (a plan-gated feature that renders outside the iframe). Do not put a bypass token in the Base URL: Live Preview appends the entry path into the last query value, so the token and the path both break. See `base-url-query-string-corrupts-preview-url`.
4. Third-party SSO screens that themselves set `X-Frame-Options: DENY` cannot render in the iframe at all. Use Open in New Tab, which renders the preview outside the iframe so those screens can load.
3. If the deployment sits behind platform password protection, note that no header or query parameter can be injected into a browser-originated iframe request from Contentstack. Options: disable protection on the preview deployment, put a server-side proxy in front that injects the header, or, for Live Preview only, use Open in New Tab (a plan-gated feature that renders outside the iframe). Visual Editor does not support Open in New Tab; it always renders the site in its canvas iframe, so it needs one of the other options. Do not put a bypass token in the Base URL: Live Preview appends the entry path into the last query value, so the token and the path both break. See `base-url-query-string-corrupts-preview-url`.
4. Third-party SSO screens that themselves set `X-Frame-Options: DENY` cannot render in the iframe at all. For Live Preview, use Open in New Tab, which renders the preview outside the iframe so those screens can load. Visual Editor has no such mode yet: the preview deployment needs a sign-in path that works when framed, or no SSO in front of it.
5. On the Live Preview Onboarding Check this shows as **Live Preview SDK Not Initialized**, not "Could Not Connect to Website". The reachability gate is a `no-cors` request, which succeeds on any HTTP response, so frame headers and auth screens pass it.
- **verification**: `curl -I https://<preview-host>/<some-deep-route>` and confirm no `X-Frame-Options` and a `frame-ancestors` value that includes the Contentstack app origin. Reload Live Preview; the pane should render.

Expand Down Expand Up @@ -153,7 +153,7 @@ setup, use `setup-docs.md` instead.
- **rendering_modes**: CSR, SSR
- **root_cause**: With `editButton.enable: true` and no `exclude` (the defaults), the SDK renders the button on any page that loads it, inside the iframe or not. It creates `#cslp-tooltip` when the module is **imported**, before `init()` runs, so a production build that imports the SDK and never calls `init()` still has the element; config passed to `init()` is applied only afterwards. It never checks the `live_preview` hash or any session signal. `inIframe()` only matters when `exclude` is set, and a `?cslp-buttons=` query parameter overrides `exclude` unless `includeByQueryParameter: false`. Teams that gate `init()` on a server-read hash also get bitten by App Router soft navigation, which does not re-run that server-side gate while the SDK's button instance persists across route changes.
- **fix**:
1. Add `editButton: { enable: true, exclude: ['outsideLivePreviewPortal'], includeByQueryParameter: false }`. Outside the iframe the button no longer renders, a query parameter cannot bring it back, and Live Preview inside the pane keeps working.
1. Where `init()` runs on the page, add `editButton: { enable: true, exclude: ['outsideLivePreviewPortal'], includeByQueryParameter: false }`. Outside the iframe the button no longer renders, a query parameter cannot bring it back, and Live Preview inside the pane keeps working. This config is applied only by `init()`, so it does nothing for a build that imports the SDK without calling `init()`: use step 2 there.
2. If Live Preview is not used on production at all, keep the SDK out of the production bundle: build with `PURGE_PREVIEW_SDK=true` (or `REACT_APP_PURGE_PREVIEW_SDK=true`), which swaps in a no-op SDK and skips the import-time button, or load the SDK with a dynamic import that runs only for preview requests. The flag works only if the bundler exposes `process.env` to browser code. Skipping `init()` or passing `enable: false` is not enough on its own, because the element is created at import.
3. Do not rely on a server-side hash check alone in an App Router app; soft navigation will not re-evaluate it.
- **verification**: Load the production URL in a normal tab with DevTools open. `document.getElementById('cslp-tooltip')` returns `null`, and no `data-cslp` attributes are present if the SDK runs with `enable: false`.
Expand All @@ -165,12 +165,12 @@ setup, use `setup-docs.md` instead.
- **symptom**: `Access to fetch at 'https://<region>-rest-preview.contentstack.com/v3/...' from origin '<site origin>' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource`, frequently alongside a `401 (Unauthorized)` on the same request. Works on localhost, fails on the deployed dev/staging host.
- **frameworks**: Gatsby, Angular, Next.js, React
- **rendering_modes**: CSR mainly
- **root_cause**: The preview request is genuinely rejected (bad or wrong-type token, or a host for the wrong region), and the error response carries no CORS headers, so the browser reports it as a CORS failure instead of a 401. A block by the site's own Content Security Policy looks different: the console shows a separate `Refused to connect to '<url>' because it violates the following Content Security Policy directive` error, not a CORS message. There is no per-stack "CORS allowlist" on the Contentstack side to add origins to.
- **root_cause**: The preview request is genuinely rejected (bad or wrong-type token, or a host for the wrong region), and the error response carries no CORS headers, so the browser reports it as a CORS failure instead of a 401. A block by the site's own Content Security Policy looks different: the console shows a separate `Refused to connect to '<url>' because it violates the following Content Security Policy directive` error, not a CORS message. Rule out both before looking at the Contentstack side; if both are ruled out and the request still fails, hand the failing request to Contentstack Support.
- **fix**:
1. Fix the token and host first: confirm you are sending a valid **Preview Token** (not a Delivery Token, not a Management Token) in the `preview_token` header, that the preview host matches the stack's region, and regenerate the token if in doubt.
2. Recheck env vars after a stack migration or API key change — a stale API key produces the same 401-behind-CORS shape.
3. Only if the console also shows a `Refused to connect … Content Security Policy` error: allow every Contentstack host the page calls in the site's `connect-src`: the region's delivery, preview, GraphQL and asset hosts as well as the app host. Listing only the app and REST preview hosts usually misses one.
- **verification**: `curl` the same preview URL from a terminal, passing credentials from environment variables rather than inline, for example `curl -s -o /dev/null -w '%{http_code}\n' -H "api_key: $CONTENTSTACK_API_KEY" -H "preview_token: $CONTENTSTACK_PREVIEW_TOKEN" "<preview-url>"`. A 200 proves the token and host are valid and the remaining problem is browser-side. In the browser, the request should complete with `Access-Control-Allow-Origin` present.
- **verification**: `curl` the same preview URL from a terminal, passing credentials from environment variables rather than inline, for example `curl -s -o /dev/null -w '%{http_code}\n' -H "api_key: $CONTENTSTACK_API_KEY" -H "preview_token: $CONTENTSTACK_PREVIEW_TOKEN" "<preview-url>"`. A 200 proves only that the token and host are valid, because `curl` does not enforce CORS; the remaining problem is browser-side. In the browser, the request should complete with `Access-Control-Allow-Origin` present.

---

Expand All @@ -182,7 +182,7 @@ setup, use `setup-docs.md` instead.
- **root_cause**: In SSR the SDK does not fetch content. It puts `?live_preview=<hash>&content_type_uid=...&entry_uid=...` on the URL and reloads, and preview state lives **on the Stack instance**. Failure (A) is having two instances: a long-lived "default" Stack for page queries plus a second Stack passed only to `init()`, so the hash never lands on the instance that queries. Failure (B) is having one module-level instance shared across requests: once any request puts it into preview mode, unrelated requests inherit it, and ordinary delivery traffic starts hitting the preview service with a hash that has expired (hashes are session-scoped and time-limited; the preview session issues a fresh one, so a stored or hardcoded hash eventually fails with the tracker-invalid text above) or belongs to another branch. Under concurrency requests can also overwrite each other's config mid-flight.
- **fix**:
1. Create a fresh Delivery SDK instance **per request**. Do not share a module-level Stack, and pass the instance down through request-scoped context rather than importing a singleton. This one change fixes both failures.
2. Read the hash off the incoming request each time and apply it to that instance: `stack.livePreviewQuery({ live_preview: hash, contentTypeUid, entryUid, preview_timestamp })`.
2. Read the hash off the incoming request each time and apply it to that instance: `stack.livePreviewQuery({ live_preview: hash, contentTypeUid, entryUid, preview_timestamp })`. `live_preview` is the only required key: when it is empty the SDK resets the instance to published content and clears the other keys. The SDK accepts `content_type_uid` / `entry_uid` as well as the camelCase spellings, so passing `request.query` straight through also works when its values are plain strings. `preview_timestamp` is what Timeline needs.
3. Call `livePreviewQuery(...)` **unconditionally** per request, with empty values when there is no hash. Calling it only when a hash exists leaves the previous request's hash in place.
4. Run every query that should show drafts on that instance. A query on an un-hashed instance returns published content.
5. Drop any separate "live preview only" Stack. `init()` does not need a Stack in SSR.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ the fixes here will help.
- **symptom**: Preview works on localhost but not on a deployed environment with identical code and config — the setup status can even show "Setup Complete" and edits go through in the entry form while the rendered page never changes. Or preview stops updating and only recovers in an incognito window or after a hard reload.
- **frameworks**: Next.js, framework-agnostic
- **rendering_modes**: SSR | SSG/ISR | edge
- **root_cause**: A cache sits between the browser and the preview service. Whether or not its key includes the `live_preview` hash, preview requests get a stored payload instead of a live one. Seen with a customer CDN in front of the deployed environment, with an app-level fetch cache, and (on the Contentstack side) with stale JS bundles cached in the browser or CDN after a release. The Contentstack SDKs' own cache policies (`IGNORE_CACHE`, `CACHE_THEN_NETWORK`, `CACHE_ELSE_NETWORK`, `NETWORK_ELSE_CACHE`) do **not** switch themselves off for Live Preview — they cache regardless of the preview config.
- **root_cause**: A cache sits between the browser and the preview service. Whether or not its key includes the `live_preview` hash, preview requests get a stored payload instead of a live one. Seen with a customer CDN in front of the deployed environment, with an app-level fetch cache, and (on the Contentstack side) with stale JS bundles cached in the browser or CDN after a release. SDK-level caching is a separate layer and depends on the SDK: the legacy `contentstack` v3 JavaScript SDK forces `NETWORK_ELSE_CACHE` once a real `live_preview` hash is set, so its cache only serves as a fallback during preview. Do not assume the same of other SDKs.
- **fix**:
1. Bypass every cache layer when a `live_preview` hash is present: no CDN caching of the preview host or the preview route, `cache: 'no-store'` on the fetch. If the app opted into SDK-level caching, also set `Policy.IGNORE_CACHE` in `cacheOptions` — but `cacheOptions` is optional and unset by default, so most apps have no SDK cache to disable and can skip this.
2. Do not cache preview responses at all, under any key. The `live_preview` hash identifies the preview **session**, not the content version, so it stays the same across every edit in that session. Keying a cache on it means the first response is served for the rest of the session and the canvas silently stops updating. There is no cache key that makes preview responses safe to cache — use the preview parameters to *bypass* the cache, never to key it.
3. Exclude the preview environment's base URL from the CDN entirely — the preview service cannot be cached at origin either, which is also why preview responses are measurably slower than CDN delivery (roughly 1s vs 100ms in one measured case). That gap is expected.
1. Bypass every app and CDN cache layer when a `live_preview` hash is present: no CDN caching of the preview host or the preview route, `cache: 'no-store'` on the fetch.
2. Check SDK-level caching separately. Most apps never configure one. If the app did, find out what that SDK does when a preview hash is set before relying on it, and turn it off for preview requests if in doubt.
3. Do not cache preview responses at all, under any key. The `live_preview` hash identifies the preview **session**, not the content version: it does not change when an entry is edited (it rotates only on a 24-hour timer). Keying a cache on it means the first response is served for the rest of the session and the canvas silently stops updating. There is no cache key that makes preview responses safe to cache — use the preview parameters to *bypass* the cache, never to key it.
4. Exclude the preview environment's base URL from the CDN entirely — the preview service cannot be cached at origin either, which is also why preview responses are measurably slower than CDN delivery (roughly 1s vs 100ms in one measured case). That gap is expected.
- **verification**: Compare response headers on the preview request for cache hits. Reproduce in a private window; if the private window works and the normal one does not, it is a cache.

---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function LivePreviewInit() {
ContentstackLivePreview.init({
ssr: false,
mode: "builder",
stackSdk: stack.config,
stackSdk: stack.config, // @contentstack/delivery-sdk; with the legacy contentstack SDK pass the Stack instance
stackDetails: { apiKey: API_KEY, environment: ENVIRONMENT },
});
}, []);
Expand Down Expand Up @@ -115,7 +115,7 @@ render:
ContentstackLivePreview.init({
ssr: false,
mode: "builder",
stackSdk: stack.config,
stackSdk: stack.config, // @contentstack/delivery-sdk; with the legacy contentstack SDK pass the Stack instance
stackDetails: { apiKey: API_KEY, environment: ENVIRONMENT },
});
```
Expand Down
Loading