Client for Next.js

The preview-aware client, preview routes and preview banner for Next.js App Router sites, generated from @cairn-cms/client/next's own TSDoc comments.

Functions

createClient

Creates the preview-aware client - bind it once at module scope and use it from any Server Component, Route Handler or Server Action:

// lib/cairn.ts
import { createClient } from "@cairn-cms/client/next";
export const cairn = createClient({ projectId: process.env.NEXT_PUBLIC_CAIRN_PROJECT_ID! });

// app/posts/[slug]/page.tsx
const [post] = await cairn.fetch("post", { where: { slug }, include: ["author"], limit: 1 });
const hero = await cairn.image(post.heroImage, { size: "large" });

The Draft Mode/cookie check runs at most once per request (React cache()), and only reads the cookie once Draft Mode is on - so ordinary visitors don't turn statically rendered pages dynamic. If a preview session has expired or been revoked, reads quietly fall back to published content instead of failing the page. Server-only: the session cookie is HttpOnly, so a Client Component gets data passed down as props.

function createClient(config: CreateClientConfig): CairnNextClient
config.apiUrlstringoptionalOverride for local development; defaults to the hosted cairn API.
config.cdnUrlstringoptionalOverride for local development; defaults to the hosted public asset CDN.
config.cookieNamestringoptionalMust match createPreviewHandler/createPreviewExitHandler/PreviewBanner if changed from the default.
config.datasetstringoptionalDataset that published (non-preview) reads use - defaults to "production". A preview always reads whichever dataset its session was opened for.
config.projectIdstringrequiredThe project's slug.
config.secureCdnUrlstringoptionalOverride for local development; defaults to the hosted secure preview CDN.
config.tokenstringoptionalA project API token, only needed for the write methods. Never used for reads.

Returns: CairnNextClient

createPreviewExitHandler

Builds the GET handler for exiting preview mode - mount at app/api/preview/exit/route.ts, and link to it (e.g. from PreviewBanner's exit action) at whatever path that ends up being:

export const GET = createPreviewExitHandler();
function createPreviewExitHandler(config: CreatePreviewExitHandlerConfig): (request: NextRequest) => Promise<Response>
config.cookieNamestringoptional
config.redirectTostringoptionalWhere to send the visitor after exiting - defaults to /.

Returns: (request: NextRequest) => Promise<Response>

createPreviewHandler

Builds the GET handler for your preview route (ROADMAP.md §4) - mount it at whatever path you set as this project's Preview URL in Settings, e.g. app/api/preview/route.ts:

export const GET = createPreviewHandler({
  projectId: process.env.NEXT_PUBLIC_CAIRN_PROJECT_ID,
  resolvePath(resolution) {
    if (resolution.documentType === "post") return `/blog/${resolution.documentId}`;
    return "/";
  },
});

Handles everything else itself: exchanging either the single-use internal grant or reusable external share credential in the ?token= param (a 401 if it is missing, invalid, expired, or already used where applicable), turning on Next's draft mode, remembering the *session* token (not the grant) in its own HttpOnly cookie so later requests can still read draft content, and redirecting.

function createPreviewHandler(config: CreatePreviewHandlerConfig): (request: NextRequest) => Promise<Response>
config.apiUrlstringoptionalOverride for local development; defaults to the hosted cairn API.
config.cookieNamestringoptionalOnly change this if cairn_preview_token collides with something else already on your site. Whatever you pick here, pass the same value to createClient/createPreviewExitHandler/PreviewBanner too - they all need to agree on where the token lives.
config.projectIdstringrequired
config.resolvePath(resolution: PreviewResolution, client: CairnPreviewClient) => string | Promise<string>optionalMaps the session's claims to a real path on your site - the one thing only your own code can know (document type/id → URL). Omit it and every preview link lands on / with draft mode already on, so the visitor can navigate the rest of the site normally from there - a reasonable default, not a broken state.

Returns: (request: NextRequest) => Promise<Response>

PreviewBanner

Drop this in your root layout - it renders nothing outside preview mode, so it's always safe to include unconditionally:

import { PreviewBanner } from "@cairn-cms/client/next";

export default function RootLayout({ children }) {
  return (
    <html><body>
      {children}
      <PreviewBanner projectId={process.env.NEXT_PUBLIC_CAIRN_PROJECT_ID} />
    </body></html>
  );
}

Inside preview mode, shows a small fixed banner with an exit link and mounts the live-reload connection (see PreviewLiveReload) - the Dashboard saving something now shows up without a manual refresh.

function PreviewBanner(__namedParameters: PreviewBannerProps): Promise<Element | null>
__namedParameters.apiUrlstringoptionalOverride for local development; defaults to the hosted cairn API. Also what the live-reload WebSocket connects to.
__namedParameters.cookieNamestringoptionalMust match whatever createPreviewHandler/createPreviewExitHandler were given, if anything other than the default.
__namedParameters.exitHrefstringoptionalWhere the banner's "Exit preview" link points - must match wherever createPreviewExitHandler is mounted.
__namedParameters.messagestringoptional
__namedParameters.projectIdstringrequired

Returns: Promise<Element | null>

CairnNextClient

Returned by createClient().

create

Creates a new draft document. Requires token.

create<T>(type: string, data: T): Promise<Document<T>>
typestringrequired
dataTrequired

Returns: Promise<Document<T>>

delete

Deletes a document outright (draft and published data, and its version history). Requires token.

delete(type: string, id: string): Promise<void>
typestringrequired
idstringrequired

Returns: Promise<void>

fetch

Documents of a type - drafts in preview, published content otherwise. Same filter/select/include/order/pagination options either way (see FetchOptions).

fetch<T>(type: string, options?: FetchOptions<T>): Promise<DeliveredDocument<T>[]>
typestringrequired
options.includestring[]optionalDocument type identifiers to resolve inline wherever referenced. Resolution traverses nested objects/arrays but follows one reference hop. Top-level reference field names are also accepted for compatibility.
options.limitnumberoptionalDefaults to 100, capped at 200 server-side.
options.localestringoptionalWhich locale a localized field (see the project's Settings) resolves to - the requested locale's value if present, else the project's default locale's. Omit to just get the default locale directly; every localized field's raw { [locale]: value } map is always resolved down to a plain value either way, so a project with no locales configured needs nothing extra here at all.
options.orderBy{ direction?: "asc" | "desc"; field: Extract<keyof T, string> | string }[]optionalOne or more sort terms, applied in order; direction defaults to "asc".
options.select(Extract<keyof T, string> | string)[]optionalOnly return these fields - dotted paths reach into nested object fields, e.g. "duration.start". Omit for the full document.
options.skipnumberoptional
options.where{ [K in keyof T]?: WhereValue<T[K]> }optionalFilters, ANDed together.

Returns: Promise<DeliveredDocument<T>[]>

fetchOne

A single document by id - its draft in preview, its published version otherwise (null if it has none).

fetchOne<T>(type: string, id: string, options?: FetchOneOptions): Promise<DeliveredDocument<T> | null>
typestringrequired
idstringrequired
options.includestring[]optional
options.localestringoptional

Returns: Promise<DeliveredDocument<T> | null>

fetchSingleton

A singleton type's one document - draft in preview, published otherwise.

fetchSingleton<T>(type: string, options?: FetchOneOptions): Promise<DeliveredDocument<T> | null>
typestringrequired
options.includestring[]optional
options.localestringoptional

Returns: Promise<DeliveredDocument<T> | null>

image

A URL for an image field - permanent public CDN URL normally, short-lived signed secure-CDN URL in preview. Named presets only (see PreviewImageUrlOptions).

image(source: ImageValue, options?: PreviewImageUrlOptions): Promise<string>
source.assetIdstringrequired
source.filenamestringoptional
options.sizeImageSizePresetoptional

Returns: Promise<string>

preview

What the current preview session grants, or null when this request isn't a preview.

preview(): Promise<PreviewResolution | null>

Returns: Promise<PreviewResolution | null>

publish

Publishes a document's current draft. Requires token.

publish<T>(type: string, id: string): Promise<Document<T>>
typestringrequired
idstringrequired

Returns: Promise<Document<T>>

unpublish

Unpublishes a document - it keeps its draft, but stops being served by the read methods above. Requires token.

unpublish<T>(type: string, id: string): Promise<Document<T>>
typestringrequired
idstringrequired

Returns: Promise<Document<T>>

update

Overwrites a document's draft data. Requires token.

update<T>(type: string, id: string, data: T): Promise<Document<T>>
typestringrequired
idstringrequired
dataTrequired

Returns: Promise<Document<T>>

uploadAsset

Uploads a file as a new asset - use its id as an image/images field's { assetId }. Requires token.

uploadAsset(file: Blob, filename: string): Promise<UploadedAsset>
fileBlobrequired
filenamestringrequired

Returns: Promise<UploadedAsset>

CairnPreviewClient

fetch

Documents of a type in the session's dataset, at whichever of draft/published the session's view says - with the same filter/select/include/order/pagination options as CairnClient.fetch.

fetch<T>(type: string, options?: FetchOptions<T>): Promise<PreviewDocument<T>[]>
typestringrequired
options.includestring[]optionalDocument type identifiers to resolve inline wherever referenced. Resolution traverses nested objects/arrays but follows one reference hop. Top-level reference field names are also accepted for compatibility.
options.limitnumberoptionalDefaults to 100, capped at 200 server-side.
options.localestringoptionalWhich locale a localized field (see the project's Settings) resolves to - the requested locale's value if present, else the project's default locale's. Omit to just get the default locale directly; every localized field's raw { [locale]: value } map is always resolved down to a plain value either way, so a project with no locales configured needs nothing extra here at all.
options.orderBy{ direction?: "asc" | "desc"; field: Extract<keyof T, string> | string }[]optionalOne or more sort terms, applied in order; direction defaults to "asc".
options.select(Extract<keyof T, string> | string)[]optionalOnly return these fields - dotted paths reach into nested object fields, e.g. "duration.start". Omit for the full document.
options.skipnumberoptional
options.where{ [K in keyof T]?: WhereValue<T[K]> }optionalFilters, ANDed together.

Returns: Promise<PreviewDocument<T>[]>

fetchOne

A single document by id, at the session's view - null if no document with that id/type exists in the session's dataset at all (not merely unpublished - see PreviewDocument's own doc comment for that case).

fetchOne<T>(type: string, id: string, options?: FetchOneOptions): Promise<PreviewDocument<T> | null>
typestringrequired
idstringrequired
options.includestring[]optional
options.localestringoptional

Returns: Promise<PreviewDocument<T> | null>

fetchSingleton

A singleton type's one document, auto-created server-side on first preview the same way the Dashboard's own editor does.

fetchSingleton<T>(type: string, options?: FetchOneOptions): Promise<PreviewDocument<T> | null>
typestringrequired
options.includestring[]optional
options.localestringoptional

Returns: Promise<PreviewDocument<T> | null>

resolve

Resolves and validates the session server-side (it can't be checked client-side - verifying it needs a DB lookup that never leaves apps/api). Throws if the session is missing, invalid, expired, or revoked. Call this first, before rendering anything, the same way the preview-route template does to decide where to redirect.

resolve(): Promise<PreviewResolution>

Returns: Promise<PreviewResolution>

Types

CreateClientConfig

apiUrlstringoptionalOverride for local development; defaults to the hosted cairn API.
cdnUrlstringoptionalOverride for local development; defaults to the hosted public asset CDN.
cookieNamestringoptionalMust match createPreviewHandler/createPreviewExitHandler/PreviewBanner if changed from the default.
datasetstringoptionalDataset that published (non-preview) reads use - defaults to "production". A preview always reads whichever dataset its session was opened for.
projectIdstringrequiredThe project's slug.
secureCdnUrlstringoptionalOverride for local development; defaults to the hosted secure preview CDN.
tokenstringoptionalA project API token, only needed for the write methods. Never used for reads.

CreatePreviewExitHandlerConfig

cookieNamestringoptional
redirectTostringoptionalWhere to send the visitor after exiting - defaults to /.

CreatePreviewHandlerConfig

apiUrlstringoptionalOverride for local development; defaults to the hosted cairn API.
cookieNamestringoptionalOnly change this if cairn_preview_token collides with something else already on your site. Whatever you pick here, pass the same value to createClient/createPreviewExitHandler/PreviewBanner too - they all need to agree on where the token lives.
projectIdstringrequired
resolvePath(resolution: PreviewResolution, client: CairnPreviewClient) => string | Promise<string>optionalMaps the session's claims to a real path on your site - the one thing only your own code can know (document type/id → URL). Omit it and every preview link lands on / with draft mode already on, so the visitor can navigate the rest of the site normally from there - a reasonable default, not a broken state.

DeliveredDocument

A delivered document from either API - _publishedAt is null only for a never-published draft seen in preview. What @cairn-cms/client/next's createClient returns, since one call can be served by either side.

type DeliveredDocument<T> = PreviewDocument<T>

FetchOneOptions

The subset of FetchOptions fetchOne/fetchSingleton accept.

includestring[]optional
localestringoptional

FetchOptions

Options for fetch() - filters (ANDed together), field selection, one one-hop reference resolution, sorting, and pagination. Compiles down to the query params apps/api's /published/types/:type/documents route accepts (see packages/db/src/publishedQuery.ts) - this just spares callers from hand-building that query string themselves.

includestring[]optionalDocument type identifiers to resolve inline wherever referenced. Resolution traverses nested objects/arrays but follows one reference hop. Top-level reference field names are also accepted for compatibility.
limitnumberoptionalDefaults to 100, capped at 200 server-side.
localestringoptionalWhich locale a localized field (see the project's Settings) resolves to - the requested locale's value if present, else the project's default locale's. Omit to just get the default locale directly; every localized field's raw { [locale]: value } map is always resolved down to a plain value either way, so a project with no locales configured needs nothing extra here at all.
orderBy{ direction?: "asc" | "desc"; field: Extract<keyof T, string> | string }[]optionalOne or more sort terms, applied in order; direction defaults to "asc".
select(Extract<keyof T, string> | string)[]optionalOnly return these fields - dotted paths reach into nested object fields, e.g. "duration.start". Omit for the full document.
skipnumberoptional
where{ [K in keyof T]?: WhereValue<T[K]> }optionalFilters, ANDed together.

ImageValue

What an image field's value looks like in document data - see @cairn-cms/schema's validator.

assetIdstringrequired
filenamestringoptional

PreviewBannerProps

apiUrlstringoptionalOverride for local development; defaults to the hosted cairn API. Also what the live-reload WebSocket connects to.
cookieNamestringoptionalMust match whatever createPreviewHandler/createPreviewExitHandler were given, if anything other than the default.
exitHrefstringoptionalWhere the banner's "Exit preview" link points - must match wherever createPreviewExitHandler is mounted.
messagestringoptional
projectIdstringrequired

PreviewImageUrlOptions

imageUrl()'s only option in preview mode - a named preset, or omit for the original. Unlike createImageUrl, there's no API-route fallback for anything else (no apiUrl transform path exists on the preview side) - fit/format/quality/explicit dimensions simply aren't offered here.

sizeImageSizePresetoptional

PreviewResolution

What GET .../preview/resolve reports once a token verifies - see apps/api's routes/preview.ts. documentType/documentId are null for a token minted without one (e.g. previewing a whole dataset/branch rather than jumping to one document).

datasetIdstringrequired
documentIdstring | nullrequired
documentTypestring | nullrequired
view"draft" | "published"required