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): CairnNextClientconfig.apiUrl | string | optional | Override for local development; defaults to the hosted cairn API. |
config.cdnUrl | string | optional | Override for local development; defaults to the hosted public asset CDN. |
config.cookieName | string | optional | Must match createPreviewHandler/createPreviewExitHandler/PreviewBanner if changed from the default. |
config.dataset | string | optional | Dataset that published (non-preview) reads use - defaults to "production". A preview always reads whichever dataset its session was opened for. |
config.projectId | string | required | The project's slug. |
config.secureCdnUrl | string | optional | Override for local development; defaults to the hosted secure preview CDN. |
config.token | string | optional | A 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.cookieName | string | optional | |
config.redirectTo | string | optional | Where 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.apiUrl | string | optional | Override for local development; defaults to the hosted cairn API. |
config.cookieName | string | optional | Only 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.projectId | string | required | |
config.resolvePath | (resolution: PreviewResolution, client: CairnPreviewClient) => string | Promise<string> | optional | Maps 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.apiUrl | string | optional | Override for local development; defaults to the hosted cairn API. Also what the live-reload WebSocket connects to. |
__namedParameters.cookieName | string | optional | Must match whatever createPreviewHandler/createPreviewExitHandler were given, if anything other than the default. |
__namedParameters.exitHref | string | optional | Where the banner's "Exit preview" link points - must match wherever createPreviewExitHandler is mounted. |
__namedParameters.message | string | optional | |
__namedParameters.projectId | string | required |
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>>type | string | required | |
data | T | required |
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>type | string | required | |
id | string | required |
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>[]>type | string | required | |
options.include | string[] | optional | Document 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.limit | number | optional | Defaults to 100, capped at 200 server-side. |
options.locale | string | optional | Which 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 }[] | optional | One or more sort terms, applied in order; direction defaults to "asc". |
options.select | (Extract<keyof T, string> | string)[] | optional | Only return these fields - dotted paths reach into nested object fields, e.g. "duration.start". Omit for the full document. |
options.skip | number | optional | |
options.where | { [K in keyof T]?: WhereValue<T[K]> } | optional | Filters, 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>type | string | required | |
id | string | required | |
options.include | string[] | optional | |
options.locale | string | optional |
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>type | string | required | |
options.include | string[] | optional | |
options.locale | string | optional |
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.assetId | string | required | |
source.filename | string | optional | |
options.size | ImageSizePreset | optional |
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>>type | string | required | |
id | string | required |
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>>type | string | required | |
id | string | required |
Returns: Promise<Document<T>>
update
Overwrites a document's draft data. Requires token.
update<T>(type: string, id: string, data: T): Promise<Document<T>>type | string | required | |
id | string | required | |
data | T | required |
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>file | Blob | required | |
filename | string | required |
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>[]>type | string | required | |
options.include | string[] | optional | Document 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.limit | number | optional | Defaults to 100, capped at 200 server-side. |
options.locale | string | optional | Which 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 }[] | optional | One or more sort terms, applied in order; direction defaults to "asc". |
options.select | (Extract<keyof T, string> | string)[] | optional | Only return these fields - dotted paths reach into nested object fields, e.g. "duration.start". Omit for the full document. |
options.skip | number | optional | |
options.where | { [K in keyof T]?: WhereValue<T[K]> } | optional | Filters, 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>type | string | required | |
id | string | required | |
options.include | string[] | optional | |
options.locale | string | optional |
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>type | string | required | |
options.include | string[] | optional | |
options.locale | string | optional |
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
apiUrl | string | optional | Override for local development; defaults to the hosted cairn API. |
cdnUrl | string | optional | Override for local development; defaults to the hosted public asset CDN. |
cookieName | string | optional | Must match createPreviewHandler/createPreviewExitHandler/PreviewBanner if changed from the default. |
dataset | string | optional | Dataset that published (non-preview) reads use - defaults to "production". A preview always reads whichever dataset its session was opened for. |
projectId | string | required | The project's slug. |
secureCdnUrl | string | optional | Override for local development; defaults to the hosted secure preview CDN. |
token | string | optional | A project API token, only needed for the write methods. Never used for reads. |
CreatePreviewExitHandlerConfig
cookieName | string | optional | |
redirectTo | string | optional | Where to send the visitor after exiting - defaults to /. |
CreatePreviewHandlerConfig
apiUrl | string | optional | Override for local development; defaults to the hosted cairn API. |
cookieName | string | optional | Only 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. |
projectId | string | required | |
resolvePath | (resolution: PreviewResolution, client: CairnPreviewClient) => string | Promise<string> | optional | Maps 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.
include | string[] | optional | |
locale | string | optional |
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.
include | string[] | optional | Document 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. |
limit | number | optional | Defaults to 100, capped at 200 server-side. |
locale | string | optional | Which 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 }[] | optional | One or more sort terms, applied in order; direction defaults to "asc". |
select | (Extract<keyof T, string> | string)[] | optional | Only return these fields - dotted paths reach into nested object fields, e.g. "duration.start". Omit for the full document. |
skip | number | optional | |
where | { [K in keyof T]?: WhereValue<T[K]> } | optional | Filters, ANDed together. |
ImageValue
What an image field's value looks like in document data - see @cairn-cms/schema's validator.
assetId | string | required | |
filename | string | optional |
PreviewBannerProps
apiUrl | string | optional | Override for local development; defaults to the hosted cairn API. Also what the live-reload WebSocket connects to. |
cookieName | string | optional | Must match whatever createPreviewHandler/createPreviewExitHandler were given, if anything other than the default. |
exitHref | string | optional | Where the banner's "Exit preview" link points - must match wherever createPreviewExitHandler is mounted. |
message | string | optional | |
projectId | string | required |
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.
size | ImageSizePreset | optional |
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).
datasetId | string | required | |
documentId | string | null | required | |
documentType | string | null | required | |
view | "draft" | "published" | required |