@flotiq/nextjs-live-preview
v0.9.0
Published
Support for live preview in Next.JS framework in flotiq-api-sdk
Readme
@flotiq/nextjs-live-preview
A comprehensive Next.js integration for Flotiq CMS that provides real-time live preview based on the new notification + data endpoints. This package allows content editors to see changes while editing content in Flotiq CMS.
Features
- 🔄 Real-time content updates via
WS /ws/notify/:docKey - 📦 Collaboration-aware reads via
GET /data/:docKey?hydration=<n> - 🧭 Identifying-field matching (for example slug-based routes)
- 👥 Collaborator-aware field indicators with
LivePreviewBox - 📱 Flotiq iframe support for seamless CMS integration
- ⚡ Throttled updates to prevent excessive re-renders
- 🎨 Customizable UI components for editor indicators
Installation
npm install @flotiq/nextjs-live-previewRequirements
- React: 19.0.0 or higher
- Next.js: 15.0.0 or higher
- Flotiq API SDK: 0.6.0 or higher
How It Works
- Session Setup: live preview route stores required
spaceId,contentType,objectId,apiKey, plus optionaluserIdandidentifyingFieldsin cookie. - Request Matching: middleware checks SDK content requests against session data (direct object id match or identifying-fields filter match).
- Data Read: matched requests are served from
GET /data/:docKeywith hydration. - Notify Refresh: client hook subscribes to
WS /ws/notify/:docKeyand triggersrouter.refresh()on notifications.
Quick Start
1. Configure the Middleware
Add the live preview middleware to your Flotiq API configuration:
import { Flotiq } from "@flotiq/flotiq-api-sdk";
import { createNextLivePreviewMiddleware } from "@flotiq/nextjs-live-preview";
const api = new Flotiq({
middleware: [createNextLivePreviewMiddleware()],
});Function createNextLivePreviewMiddleware supports provided options:
- flotiqApiKey - optional. It uses
FLOTIQ_API_KEYenv by default. It can be used to override Flotiq API key - singletonTypes - optional. Array of singleton content type definitions that should resolve preview data even when the request does not include an object id or identifying-field filters
2. Create Live Preview API Route
Create the API route that handles live preview mode toggling in app/api/flotiq/live-preview/route.ts file.
import { NextRequest } from "next/server";
import { redirect } from "next/navigation";
import { livePreview } from "@flotiq/nextjs-live-preview/server";
export async function GET(req: NextRequest) {
const redirectPath = req.nextUrl.searchParams.get("redirect") || "/";
/**
* Validate the request.
*
* To validate the request you can use the `Client Authorization Key` passed to the
* live preview URL from **Live Preview** plugin as `editor_key` URL param.
* Make sure to define it in your application's environment variables and pass the same
* `Client Authorization Key` to the plugin settings.
* Without validating this key, everybody could see your latest data.
*
* Example validation:
*
* const clientAuthKey = req.nextUrl.searchParams.get('editor_key');
* if (clientAuthKey !== process.env.FLOTIQ_CLIENT_AUTH_KEY) {
* return new Response('Unauthorized', { status: 401 });
* }
*/
const livePreviewState = await livePreview();
const newLivePreview = req.nextUrl.searchParams.has("live-preview")
? req.nextUrl.searchParams.get("live-preview") === "true"
: !livePreviewState.isEnabled;
if (newLivePreview) {
livePreviewState.enable(req);
} else {
livePreviewState.disable();
}
redirect(redirectPath);
}Live preview route expects these query parameters:
- Required:
spaceId,apiKey,objectId,contentType - Optional:
userId - Optional:
identifyingFields(used for list-request matching) as JSON string, for example:identifyingFields={"slug":"post-slug"}
3. Add Live Preview Status Component
This component keeps router refresh in sync with preview notifications and renders the default live preview banner.
If you don't want to use predefined banner, you can use useLivePreviewStatus hook and create your own component (more info).
import { livePreview } from "@flotiq/nextjs-live-preview/server";
import { LivePreviewStatus } from "@flotiq/nextjs-live-preview/client";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const { isEnabled } = await livePreview();
return (
<html>
<body>
<main>
{isEnabled && (
<LivePreviewStatus editorKey={process.env.FLOTIQ_EDITOR_KEY} />
)}
{children}
</main>
</body>
</html>
);
}Preview Transport
GET /data/:docKey?hydration=<n>is used by middleware to fetch collaboration-aware object state.WS /ws/notify/:docKey?apiKey=<key>&hydration=<n>is used by client hook to receive best-effort refresh notifications (hydrationis currently sent as0by default).:docKeyis built asspace/contentType/objectId.- Client applications do not sync Yjs documents anymore.
- Hydration and relation composition are handled by preview data server, not by this package.
- Matching logic for list requests supports identifying fields from session data (
identifyingFields). - Matching logic for list requests can also be forced for configured singleton content types via
singletonTypes.
Advanced Usage
Live Preview Boxes for Field Editing
Wrap your content fields with LivePreviewBox to show visual editing indicators and enable field-specific interactions:
import { LivePreviewBox } from "@flotiq/nextjs-live-preview/server";
export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
const post = await flotiqApi.ContentBlogpostAPI.get(params.slug, {
hydrate: 1,
});
return (
<article>
<LivePreviewBox data={post} fieldName="title">
<h1>{post.title}</h1>
</LivePreviewBox>
</article>
);
}Custom Live Preview Hook
If you don't want to use LivePreviewStatus, you can use useLivePreviewStatus directly.
This hook refreshes the router after notify websocket events.
Example code with custom banner:
"use client";
import { useLivePreviewStatus } from "@flotiq/nextjs-live-preview/client";
export function CustomLivePreviewIndicator() {
const { isConnected, updatedTime, contentType, objectId } =
useLivePreviewStatus();
if (!isConnected) return null;
return (
<div className="live-preview-custom">
<span>
Editing {contentType}#{objectId}
</span>
{updatedTime && <span>Last update: {updatedTime}</span>}
</div>
);
}Configuration Options
Middleware Options
interface NextLivePreviewMiddlewareOptions {
/**
* Flotiq API key for authorization
* @default process.env.FLOTIQ_API_KEY
*/
flotiqApiKey?: string;
/**
* Singleton content types that should use preview data for list requests
* even when the request does not include identifying filters.
* @default []
*/
singletonTypes?: string[];
}LivePreviewBox Props
interface LivePreviewBoxProps {
/** Field content to wrap */
children: React.ReactNode;
/** Field name in the content object */
fieldName: string;
/** Content object data with id, internal.contentType and collaborator metadata */
data: {
id: string;
__collaborators?: unknown[]; // Injected by Flotiq Collab Gateway v2
internal: {
contentType: string;
};
};
/** Scroll offset for auto-focus (default: 0) */
scrollTopOffset?: number;
/** Use light color theme for editor indicators (default: false) */
lightColors?: boolean;
/** Additional CSS class name */
className?: string;
/** Outline offset in pixels (default: 6) */
outlineOffset?: number;
}LivePreviewStatus Props
interface LivePreviewStatusProps {
/** Client Authorization key */
editorKey?: string;
/** Additional CSS class name */
className?: string;
/**
* Maximum time (in ms) to wait for a page refresh to finish.
* If Next.js doesn't complete the refresh in time, it is treated as timed out.
* Set to `-1` to disable timeout-based resolution.
* Enabled by default due to [known Next.js App Router refresh issues](#known-nextjs-app-router-refresh-issues).
* @default 500
*/
refreshTimeout?: number;
/**
* When enabled, the timeout duration automatically adjusts based on the average
* duration of previous successful refreshes (plus a 50% buffer).
* Requires `refreshTimeout` to provide the initial baseline sample.
* Enabled by default due to [known Next.js App Router refresh issues](#known-nextjs-app-router-refresh-issues).
* @default true
*/
enableAdaptiveTimeout?: boolean;
/**
* After this many consecutive refresh timeouts, live preview falls back to a hard browser reload.
* Set to `0` or `-1` to disable this fallback.
* Enabled by default due to [known Next.js App Router refresh issues](#known-nextjs-app-router-refresh-issues).
* @default 5
*/
hardReloadAfterConsecutiveTimeouts?: number;
}The useLivePreviewStatus hook accepts the same refresh timing options.
useLivePreviewStatus({
refreshTimeout: 500,
enableAdaptiveTimeout: true,
hardReloadAfterConsecutiveTimeouts: 5,
});Migration from <0.8.0 to >=0.8.0
Version 0.8.0 switches collaboration to Flotiq Collaboration Gateway v2 and updates the outward-facing live preview API around LivePreviewBox and endpoint configuration.
What Changed
- Collaboration protocols switched to Flotiq Collaboration Gateway v2
LivePreviewBoxno longer reads collaborator state from global in-memory room state.LivePreviewBoxno longer acceptsobjectIdandctdNameprops. These are now derived from thedataprop.LivePreviewBoxsupportsoutlineOffsetfor customizing outline spacing.- Server-side preview reads now use
WEBSOCKET_ENDPOINTor fall back toNEXT_PUBLIC_WEBSOCKET_ENDPOINT. - Client-side live preview notifications use
NEXT_PUBLIC_WEBSOCKET_ENDPOINT. - Deprecated
clearCacheserver helper was removed. - Deprecated
connectionModemiddleware option was removed. - added option
singletonTypesto specify singleton content that is requested without identifiers
Required Changes
In your codebase
- Update every
LivePreviewBoxusage to pass the full content object asdatainstead ofobjectIdandctdName. - Make sure the object passed to
datacomes from preview-aware SDK reads, such asget(...)orlist(..., { limit: 1 }). Flotiq Collaboration Gateway v2 injects collaborator metadata into that object automatically. - Remove any usage of
clearCacheorconnectionMode. - Set
NEXT_PUBLIC_WEBSOCKET_ENDPOINTfor the browser and, if needed,WEBSOCKET_ENDPOINTfor server-side preview reads. Default variables connect to public Flotiq Collab Gateway (wss://collab-gateway.flotiq.com). - If both env vars are set, point them to the same Flotiq Collaboration Gateway v2 instance.
- Each data type that is designed to have single object only (singletons) should be added in
singletonTypesoption ofcreateNextLivePreviewMiddleware. This will enable live preview for one-off pages and data like global settings, brand, contact info etc.
In Flotiq Dashboard
- Go to Flotiq Dasboard and update Live Preview Plugin to v0.8.1 or higher
LivePreviewBox Migration
Before 0.8.0:
<LivePreviewBox
objectId={post.id}
ctdName="blogpost"
fieldName="title"
>
<h1>{post.title}</h1>
</LivePreviewBox>From 0.8.0 onward:
<LivePreviewBox data={post} fieldName="title">
<h1>{post.title}</h1>
</LivePreviewBox>LivePreviewBox now derives objectId from data.id and ctdName from data.internal.contentType.
Use the object returned by preview-aware SDK get(...) or list(..., { limit: 1 }) reads, and the gateway will inject the collaborator metadata needed for editor indicators.
Logging
Live preview provides configurable log levels for debugging. Both client and server loggers default to info.
Accepted values: silly, debug, info, warn, error.
The default level info produces no output under normal operation. Set to warn to see only refresh timeouts and connection issues, debug to also see fallback decisions, or silly to trace every lifecycle event (WebSocket open/close, refresh start/complete, middleware matching).
| Environment variable | Runtime |
|---|---|
| NEXT_PUBLIC_FLOTIQ_LIVE_PREVIEW_LOG_LEVEL | Client (browser) |
| FLOTIQ_LIVE_PREVIEW_LOG_LEVEL | Server (Node.js) |
Troubleshooting
- Ensure cookies are enabled in your browser
- Check that the API route is correctly implemented
- Verify the
Client Authorization Keymatches in both Flotiq and application - Verify preview endpoint config for both runtimes
- Server-side data reads use
WEBSOCKET_ENDPOINT(or fallbackNEXT_PUBLIC_WEBSOCKET_ENDPOINT) - Client-side notifications use
NEXT_PUBLIC_WEBSOCKET_ENDPOINT - If both are set, point them to the same preview server exposing
/dataand/ws/notify
Known Next.js App Router Refresh Issues
Some Next.js App Router applications can fail to apply router.refresh() reliably during live preview, especially when loading.tsx or other Suspense boundaries are present.
Two practical failure modes have been observed:
router.refresh()does not settle, so later live preview refreshes never start.- the browser receives fresh RSC payload data, but the App Router keeps stale UI until a full browser reload.
To reduce the impact of these upstream issues, live preview enables three mitigations by default:
<LivePreviewStatus
editorKey={process.env.FLOTIQ_EDITOR_KEY}
refreshTimeout={500}
enableAdaptiveTimeout
hardReloadAfterConsecutiveTimeouts={5}
/>refreshTimeout={500}sets the initial upper bound for a soft refresh to settle.enableAdaptiveTimeoutlearns from successful refreshes and usesaverage duration + 50%for the next timeout.hardReloadAfterConsecutiveTimeouts={5}forces a browser reload after five consecutive soft-refresh timeouts.
This is a workaround for known upstream Next.js limitations and bugs around App Router refresh completion:
- https://github.com/vercel/next.js/discussions/58520
- https://github.com/vercel/next.js/discussions/88066
- https://github.com/vercel/next.js/discussions/49810
- https://github.com/vercel/next.js/issues/86151
If you don't like/don't need the defaults as workaround, you can disable them by setting the following options:
refreshTimeout={-1}disables timeout-based resolution.hardReloadAfterConsecutiveTimeouts={0}andhardReloadAfterConsecutiveTimeouts={-1}both disable the hard reload fallback.
