Your first typed fetch
Install the local SDK archive, configure your CMS URL/workspace/public credential, generate Models from your authorized schema, then fetch one collection. Use the sections below in that order. Replace illustrative content_item/title identifiers with your actual content type and fields.
The same client works in a build script, on a server or in a browser with intentionally public content. Your application chooses the environment variables, rendering lifecycle and cache. No website framework or example application is required.
Build and install the package
@lemonadecms/client is a local ESM package with no runtime library dependencies. It is not published to npm. Obtain this repository and use the build/pack workflow below; npm pack builds the compiled package first.
Replace /path/to/cms and /path/to/your-app with your local checkout/application paths. The package declares Node 22+; exact qualified runtimes and release gates are documented in docs/sdk-release.md. TypeScript declarations require TypeScript 5+.
For a reviewed local candidate, run npm run release:check and npm run release:candidate from packages/cms-client. The candidate command prints a new directory containing the archive, SHA256SUMS and manifest.json. Check the checksum and install that exact archive. These commands do not run regression tests or publish; package ownership, licensing and registry acceptance remain separate release gates. Existing @cms/client consumers can keep their old archive or use the file-alias migration in docs/sdk-release.md; new consumers use @lemonadecms/client.
cd /path/to/cms/packages/cms-client
npm ci
npm test
npm pack
cd /path/to/your-app
npm install /path/to/cms/packages/cms-client/lemonadecms-client-0.1.0.tgz
Configure one shared client
Create cms.js in your application. Replace the URL, workspace UUID and public key. The SDK does not read environment variables itself; pass values from your framework configuration.
The publicKey option accepts an existing pk_ key or a named lmd_pk_ application token created in API access. Named tokens restrict content types, environment and languages, with independent expiry/rotation/revocation. Include referenced types and optional media access; request an explicitly granted locale. Code generation uses the same scoped schema.
For Vite, use VITE_CMS_URL, VITE_CMS_PROJECT_ID and VITE_CMS_PUBLIC_KEY from import.meta.env. Never give a browser bundle a secret key. Keep the content type and field API identifiers aligned with your schema.
import { createCMSClient } from '@lemonadecms/client';
export const cms = createCMSClient({
url: 'https://YOUR_CMS_HOST',
projectId: 'YOUR_WORKSPACE_UUID',
publicKey: 'YOUR_PUBLIC_KEY',
timeoutMs: 10000 // Optional; choose a budget for your application.
});
Environment variables and validation
The SDK does not load .env files or require fixed variable names. Your application maps configuration to url, projectId and publicKey; a trusted server maps secretKey for optional preview. No new CMS server setting is needed to use the client.
A common server mapping uses CMS_URL for the absolute HTTP(S) base address, CMS_PROJECT_ID for the workspace UUID, CMS_PUBLIC_KEY for published reads and optional CMS_SECRET_KEY for private preview. An environment is selected by its credential, not by a new framework setting. Use HTTPS outside local development. Never put a key in the URL.
Construction rejects missing or invalid URLs, workspace UUIDs, credential formats and key kinds before sending a request. It also rejects a non-object configuration safely, without reflecting its value. CMSConfigurationError identifies configuration failures; the CMS independently validates scope, expiry and revocation on every request. A correctly formatted key is not proof of access.
If you choose an optional CMS_TIMEOUT_MS variable, convert it to a number before passing timeoutMs. The SDK accepts only integer milliseconds from 1 to 2147483647. Omit the option to retain the existing no-default-deadline behavior. An empty value or invalid number fails validation; never silently replace it with a working default.
Only public configuration may use VITE_, NEXT_PUBLIC_, PUBLIC_ or NUXT_PUBLIC_ prefixes. Read CMS_SECRET_KEY only in server-owned code. Keep private values out of HTML, serialized state, logs, browser bundles and static output. Do not log process.env; use the safe configuration error category and option name when diagnosing setup.
import { createCMSClient } from '@lemonadecms/client';
const cms = createCMSClient({
url: process.env.CMS_URL,
projectId: process.env.CMS_PROJECT_ID,
publicKey: process.env.CMS_PUBLIC_KEY,
...(process.env.CMS_TIMEOUT_MS === undefined ? {} : {
timeoutMs: Number(process.env.CMS_TIMEOUT_MS)
})
});
// Construction validates values before the first request.
// Your framework owns loading .env and exposing public variables.
Generate types from your content models
Content-type definitions → authorized schema → generated Models → typed createCMSClient. Define your fields in the Studio, generate the model file in your application, and let TypeScript check the calls you write. You do not maintain a second set of interfaces by hand.
The workflow is similar to generating types from a database schema, but Lemonade uses content-type definitions as the source. Entry field values are stored as JSON; inspecting raw database table columns cannot describe your individual content fields. The CLI reads the authorized schema API, with no database connection or database credentials.
Run from the application where the local SDK archive is installed. First load CMS_URL, CMS_PROJECT_ID and CMS_PUBLIC_KEY into that terminal's exported environment using your shell or secret manager. The CLI does not load .env files. The command below names the credential variable; never replace CMS_PUBLIC_KEY after --credential-env with the actual token value.
Replace production with your environment key or UUID. The delivery credential selects the environment; the CLI verifies the requested workspace and environment against the returned schema. It can generate only content types visible to that credential. Use a suitably scoped public read token; no management credential is needed.
Import the generated Models map and pass it to createCMSClient<Models>. TypeScript checks model and field names, selected fields and compatible filter values. References, images and choice values retain their generated types. Generated types are compile-time assistance; they do not validate arbitrary JSON at runtime or grant access.
Fields allow absence and null, including fields currently required for publication: historical snapshots, selected-field projections and incomplete drafts may omit values. Handle those cases when rendering, as the example's fallback does.
# CMS_URL, CMS_PROJECT_ID and CMS_PUBLIC_KEY must already be exported.
npx --no-install lemonade types generate \
--url "$CMS_URL" \
--workspace "$CMS_PROJECT_ID" \
--environment production \
--credential-env CMS_PUBLIC_KEY \
--out src/cms-models.ts
import { createCMSClient } from '@lemonadecms/client';
import type { Models } from './cms-models.js';
// Node server or build script; your application loads its environment.
function requiredEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing ${name}`);
return value;
}
export const cms = createCMSClient<Models>({
url: requiredEnv('CMS_URL'),
projectId: requiredEnv('CMS_PROJECT_ID'),
publicKey: requiredEnv('CMS_PUBLIC_KEY')
});
// Replace content_item/title/position with your own API identifiers.
const page = await cms.content('content_item').list({
select: ['title', 'position'],
filters: [{field: 'position', op: 'gte', value: 1}]
});
console.log(page.data[0]?.fields.title ?? 'Untitled');
// Unknown model/field names and incompatible filter values fail type checking.
Keep generated types in sync
Regenerate after changing content-type definitions and review the generated diff with your application changes. Do not hand-edit generated files. Each environment can have a different schema; use the matching workspace, environment and credential for the application you are checking.
Add --check to the same command in CI to compare without overwriting. Exit 0 means the generated output matches; exit 3 means it is missing or different. Other failures, such as invalid credentials or an unavailable CMS, must fail the job too. These are commands you can add to your pipeline, not a claim that your CI is already configured.
For reproducible offline generation, export a reviewed schema snapshot, then use --file. Schema snapshots contain definitions and metadata rather than entry values; review what you commit and never include credentials. The generator accepts TypeScript by default, plus --language csharp or --language python. Existing cms-codegen TypeScript commands remain supported.
C# outputs .NET 8+ System.Text.Json transport models; Python outputs Python 3.11+ TypedDict annotations without runtime dependencies. Generated files are data types; the separate local native clients below provide networking. Raw REST metadata uses names such as published_at; the JavaScript SDK converts top-level metadata to names such as publishedAt. The Node 22+ CLI is a development tool, not a runtime requirement for generated C#/Python applications.
npx --no-install lemonade types generate \
--url "$CMS_URL" \
--workspace "$CMS_PROJECT_ID" \
--environment production \
--credential-env CMS_PUBLIC_KEY \
--out src/cms-models.ts \
--check
npx --no-install lemonade schema pull \
--url "$CMS_URL" \
--workspace "$CMS_PROJECT_ID" \
--environment production \
--credential-env CMS_PUBLIC_KEY \
--out schema.json
npx --no-install lemonade types generate --file schema.json --out src/cms-models.ts
npx --no-install lemonade types generate --file schema.json --language csharp --out Models/Content.cs
npx --no-install lemonade types generate --file schema.json --language python --out app/content_models.py
Fetch a collection and one entry
list() returns one page; get(id) returns one entry. Select limits fields, not authorization. Omitting select returns all fields. Get an ID from page.data or a previous collection response; get() returns a 404 error when the entry is unavailable.
Read each entry’s values under fields. The id and publishedAt metadata sit outside fields. Empty collections are data: []; optional and historical values can be absent or null, even when the current schema makes a field required.
import { cms } from './cms.js';
const page = await cms.content('content_item').list({
select: ['title', 'position', 'summary'], limit: 5
});
for (const entry of page.data) console.log(entry.id, entry.fields.title);
if (page.data[0]) {
const entry = await cms.content('content_item').get(page.data[0].id);
console.log(entry.fields);
}
{
"data": [
{
"id": "11111111-1111-4111-8111-111111111111",
"fields": {
"position": 1,
"title": "Getting started",
"summary": "Example summary"
},
"publishedAt": "2026-10-15T12:00:00Z"
}
],
"meta": {
"limit": 5,
"nextCursor": null,
"hasNextPage": false,
"hasPreviousPage": false,
"previousCursor": null
}
}
Use readable page numbers
Use list({page: 1, pageSize: 6}) for familiar numbered pagination. page defaults to 1 and pageSize defaults to 25 when this mode is selected; pageSize accepts 1–100. The response adds meta.page and meta.pageSize alongside existing metadata.
Choose either page/pageSize or limit/after/before. Numbered browsing skips at most 10,000 entries; use cursor pagination or lazy pages()/entries() for larger sequential reads. Each page reads current published content, so edits can move results between numbered pages. Exact totals are not calculated.
import { cms } from './cms.js';
const page = await cms.content('content_item').list({page: 2, pageSize: 6});
console.log(page.meta.page, page.data);
if (page.meta.hasNextPage) {
const next = await cms.content('content_item').list({page: page.meta.page + 1, pageSize: 6});
console.log(next.data);
}
Filter, sort, translate and paginate
Collections support up to ten AND filter clauses and 4 KB of filter JSON. eq checks equality (null matches missing values); in accepts 1–100 values. gt/gte/lt/lte are supported for numbers, dates and UTC date/times. Fields and values must match your schema.
Sort by one supported scalar field using asc or desc. Text, choice, color, boolean, number, date and date/time support sorting. Reference/media and Markdown/long-text fields do not. Dates use ISO strings; timestamps must include a UTC offset.
The example selects title/position/summary, filters position >= 1 and sorts by position. REST accepts existing filters/sort JSON query parameters and readable aliases: filter[position][gte]=1&sort=-position&select=title,position. Do not combine filter with filters. RPC uses JSON values; GraphQL exposes typed inputs. Locale examples require Spanish to be enabled and its content published.
Readable filters use filter[FIELD][OP]=VALUE for eq/gt/gte/lt/lte. For in, repeat filter[FIELD][in][]=VALUE. Numbers, true/false and null are JSON literals; ordinary text is unquoted. Quote literal-looking text, such as filter[title][eq]="123", to retain its string type. sort=position is ascending; sort=-position is descending. URLSearchParams handles escaping. The same schema validation, ten-clause and 4 KB limits apply.
import { cms } from './cms.js';
const options = {
select: ['title', 'position', 'summary'],
locale: 'es', limit: 5,
filters: [{field: 'position', op: 'gte', value: 1}],
sort: {field: 'position', direction: 'asc'}
};
const first = await cms.content('content_item').list(options);
if (first.meta.nextCursor) {
const next = await cms.content('content_item').list({
...options, after: first.meta.nextCursor
});
console.log(next.data);
}
Return to the previous page
The SDK exposes previousCursor, hasNextPage and hasPreviousPage alongside the existing nextCursor and limit. Use before with the previousCursor; omit after. New metadata is optional when reading older CMS installations.
import { cms } from './cms.js';
const options = {limit: 5, sort: {field: 'position', direction: 'asc'}};
const first = await cms.content('content_item').list(options);
if (first.meta.nextCursor) {
const second = await cms.content('content_item').list({...options, after: first.meta.nextCursor});
if (second.meta.previousCursor) {
const previous = await cms.content('content_item').list({...options, before: second.meta.previousCursor});
console.log(previous.data);
}
}
Read multiple pages lazily
list() reads one page. pages() yields pages; entries() yields individual entries. Both iterators move forward and fetch only when your loop needs more data. Breaking the loop stops further requests. They preserve selection, filters, sort, locale and preview/public mode; an optional after resumes from a saved boundary.
The default budget is 1,000 pages; maxPages accepts 1–10,000. Reaching the budget with more content raises RangeError instead of silently returning an incomplete result. Save the last page’s nextCursor to resume. Repeated cursors raise CMSResponseError. AbortSignal cancellation and request failures propagate without retries.
The iterator holds one page of entries at a time plus bounded cursor history. It does not create a frozen export snapshot: concurrent publishing or changes to sort values can move entries. For user-facing Previous/Next controls use list() with after or before. Older servers can still supply forward pages; backward metadata requires an upgraded CMS.
import { cms } from './cms.js';
const controller = new AbortController();
for await (const page of cms.content('content_item').pages({
select: ['title', 'position'], limit: 50, maxPages: 100,
sort: {field: 'position', direction: 'asc'}, signal: controller.signal
})) {
console.log(page.data);
// Save page.meta.nextCursor if you need to resume later.
// break stops without fetching another page.
}
for await (const entry of cms.content('content_item').entries({
select: ['title'], limit: 25, maxPages: 100
})) {
console.log(entry.fields.title);
}
Use JavaScript, React or Vue
The client is framework-independent. Your framework owns state, rendering, loading, errors and caching. Create cms.js using the configuration above, then use the appropriate example below.
The runnable consumers in examples/delivery provide configuration forms, list, Next page and get workflows. From the CMS root, build the SDK; then run npm install, npm run build and npm start in examples/delivery. Open localhost:4173/plain.html, /react.html or /vue.html. They accept public keys only.
import { cms } from './cms.js';
const output = document.createElement('pre');
output.textContent = 'Loading…';
document.body.append(output);
try {
const page = await cms.content('content_item').list({limit: 5});
output.textContent = page.data.length ? JSON.stringify(page.data, null, 2) : 'No published entries.';
} catch (error) { output.textContent = error.message; }
import { useEffect, useState } from 'react';
import { cms } from './cms.js';
export default function ContentItems() {
const [items, setItems] = useState([]);
const [status, setStatus] = useState('Loading…');
useEffect(() => {
const controller = new AbortController();
cms.content('content_item').list({limit: 5, signal: controller.signal})
.then(page => {
if (!controller.signal.aborted) {
setItems(page.data); setStatus(page.data.length ? '' : 'No published entries.');
}
})
.catch(error => { if (!controller.signal.aborted) setStatus(error.message); });
return () => controller.abort();
}, []);
return <section><p role="status">{status}</p><pre>{JSON.stringify(items, null, 2)}</pre></section>;
}
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { cms } from './cms.js';
const items = ref([]), status = ref('Loading…');
const controller = new AbortController();
onMounted(async () => {
try {
const page = await cms.content('content_item').list({limit: 5, signal: controller.signal});
items.value = page.data; status.value = page.data.length ? '' : 'No published entries.';
} catch (error) { if (!controller.signal.aborted) status.value = error.message; }
});
onUnmounted(() => controller.abort());
</script>
<template>
<section><p role="status">{{ status }}</p><pre>{{ JSON.stringify(items, null, 2) }}</pre></section>
</template>
Use Nuxt with SSR
The complete recipe is maintained in examples/nuxt-guide. Install the local SDK tarball there, npm install, then npm run typecheck and npm run dev. Configure the public values below using Nuxt runtime environment variables.
Public reads use useAsyncData: Nuxt fetches during SSR and transfers the payload to the browser. Include workspace, model and locale in cache keys when making them configurable. Do not cache drafts together with public content.
The server-only preview utility requires an authorization callback before constructing a preview client. This SDK helper is not mounted as an HTTP route. The optional website-preview recipes use the separate entry-scoped, one-use grant protocol described in docs/site-preview.md. Integrate it with your application’s verified server session and permission checks; a query parameter or public key is not user authorization.
export default defineNuxtConfig({
runtimeConfig: {
cmsSecretKey: '',
cmsPreviewUrl: '',
cmsStudioOrigin: '',
public: { cms: { url: '', projectId: '', publicKey: '' } }
}
});
NUXT_PUBLIC_CMS_URL=https://YOUR_CMS_HOST
NUXT_PUBLIC_CMS_PROJECT_ID=YOUR_WORKSPACE_UUID
NUXT_PUBLIC_CMS_PUBLIC_KEY=YOUR_PUBLIC_KEY
# Server only; needed only for previews
NUXT_CMS_SECRET_KEY=YOUR_SECRET_KEY
<script setup lang="ts">
import { createCMSClient } from '@lemonadecms/client';
const config = useRuntimeConfig();
const cms = createCMSClient(config.public.cms);
const { data: page, status, error } = await useAsyncData(
`cms:${config.public.cms.projectId}:content_item:en:published`,
() => cms.content('content_item').list({limit: 5, locale: 'en'})
);
</script>
<template>
<main>
<h1>Published content</h1>
<p v-if="status === 'pending'" role="status">Loading…</p>
<p v-else-if="error" role="alert">Could not load content. Check configuration and publication.</p>
<p v-else-if="!page?.data.length">No published entries.</p>
<pre v-else>{{ JSON.stringify(page.data, null, 2) }}</pre>
</main>
</template>
import { createCMSPreviewClient } from '@lemonadecms/client/server';
import { createError, type H3Event } from 'h3';
import { useRuntimeConfig } from '#imports';
export async function readPreview(
event: H3Event,
authorize: (event: H3Event) => Promise<boolean>
) {
// Supply a trusted server function that verifies your user's session/permissions.
if (await authorize(event) !== true) throw createError({statusCode: 403});
const config = useRuntimeConfig(event);
const preview = createCMSPreviewClient({
url: config.public.cms.url,
projectId: config.public.cms.projectId,
secretKey: config.cmsSecretKey
});
return preview.content('content_item').list({limit: 5});
}
// No public route is provided. Never use () => true in an actual application.
Connect Next.js or SvelteKit with secure preview
The repository includes runnable examples/next-guide and examples/sveltekit-guide recipes on Node 24. Each has its own pinned dependencies, .env.example and README. Build the local @lemonadecms/client first, install the chosen recipe, fill its ignored server environment, then start it on port 4175 or 4176. Existing CMS/framework applications are not upgraded.
Published pages use the same typed SDK for article lists/details, numbered pagination and language selection. Next.js uses Server Components with request-time rendering; SvelteKit uses server load functions and the request's fetch. Server-only modules own configuration, and page responses are not shared caches. Replace the illustrative model map with generated types for your schema.
For saved-draft preview, an Owner configures the article destination in the matching environment as the website origin plus /lemonade/preview. CMS_PREVIEW_URL must match exactly; CMS_STUDIO_ORIGIN is the browser-visible studio. Keep CMS_SECRET_KEY server-only without NEXT_PUBLIC_, PUBLIC_ or VITE_ prefixes. The existing one-use exchange produces an HttpOnly entry/language-bound session, not an anonymous arbitrary draft proxy.
Both recipes reuse the shared server protocol and safe article renderer. Preview responses remain private/no-store, no-referrer, noindex and studio-frame constrained; expired or revoked access never falls back to published/unguarded drafts. Exit revokes the session. Unsaved editor changes are excluded. Markdown is escaped text; allowed rich text uses the shared safe renderer. These recipes implement entry preview; the Astro journal additionally demonstrates optional impact comparisons. Any website can implement the framework-independent impact contract with its own renderer. Ordinary CMS client reads require no preview destination.
Run task frameworks:check for type/build checks and task frameworks:verify with a local CMS for disposable HTTP/browser/revocation and bundle checks. These are local Node integrations, not edge/static-export/provider or deployment qualification. If embedded cross-site cookies are blocked, open a new tab instead of weakening the origin/session policy.
npm --prefix packages/cms-client ci
npm --prefix packages/cms-client run build
npm --prefix examples/next-guide ci
npm --prefix examples/sveltekit-guide ci
# Copy/fill the chosen recipe's .env.example as .env; never commit it.
npm --prefix examples/next-guide run dev
# Or: npm --prefix examples/sveltekit-guide run dev
Use Python or .NET
The repository also includes independent local REST packages: packages/lemonade-python (Python 3.11+, synchronous standard-library reads) and packages/lemonade-dotnet (.NET 8+, asynchronous HttpClient reads). They are not published to PyPI or NuGet. Build/install instructions and the full query/error reference are in docs/native-clients.md in the checkout.
Map your own environment configuration to the client options: CMS_URL, CMS_PROJECT_ID and CMS_PUBLIC_KEY are suggested application variable names. Both reject invalid settings before HTTP. The CMS independently enforces credential grants, environment/model/language scope, expiry and revocation. No new CMS server variables or website framework are required.
Generate models with the existing lemonade types generate command and --language python or --language csharp. Use a generated model with the native package's entry/page envelope. Historical, projected and incomplete fields can be absent/null. Python dictionaries retain snake_case metadata; .NET exposes Id, Fields, PublishedAt and Meta.NextCursor. Raw field values and signed media URLs are preserved.
Both support list/get, selection, languages, filters/sort, cursor/numbered pages and lazy bounded forward iteration. They do not add writes, retries, caches or GraphQL/RPC wrappers. Python timeout is optional socket I/O seconds, not a total deadline or async cancellation. .NET Timeout is an optional per-request/body deadline; reads accept CancellationToken. Set a budget appropriate for your application.
Private preview uses Python create_preview_client or .NET LemonadeClient.CreatePreview on a trusted server. Authorize your user before returning drafts; never put secret keys or private responses in public/static output. Registry publishing and deployment remain paused. Native packages need no Node runtime; only optional model generation uses the existing Node CLI.
import os
from lemonadecms_client import create_client
from models import Model_content_item
cms = create_client(url=os.environ['CMS_URL'],
project_id=os.environ['CMS_PROJECT_ID'],
public_key=os.environ['CMS_PUBLIC_KEY'], timeout=10)
page = cms.content('content_item', Model_content_item).list(
select=['title'], locale='en', limit=25)
for entry in page['data']:
print(entry['fields'].get('title'))
using LemonadeCMS.Client;
using Lemonade.Content;
using var cms = LemonadeClient.CreatePublished(new PublishedOptions {
Url = Environment.GetEnvironmentVariable("CMS_URL")!,
ProjectId = Environment.GetEnvironmentVariable("CMS_PROJECT_ID")!,
PublicKey = Environment.GetEnvironmentVariable("CMS_PUBLIC_KEY")!,
Timeout = TimeSpan.FromSeconds(10)
});
var page = await cms.Content<Model_content_item>("content_item")
.ListAsync(new() { Select = ["title"], Locale = "en", Limit = 25 });
foreach (var entry in page.Data) Console.WriteLine(entry.Fields.Field_title);
Read drafts on your server
Use the separate server entry point with a secret key (sk_) or named preview token (lmd_sk_) through secretKey. Authorize the requesting user first in your own application; importing this factory does not authorize your users. The same list/get methods and options work for previews.
createCMSClient rejects secret keys. The server entry point rejects browser imports. Never render or serialize the secret into HTML, logs, client configuration or component props.
import { createCMSPreviewClient } from '@lemonadecms/client/server';
const preview = createCMSPreviewClient({
url: process.env.CMS_URL!,
projectId: process.env.CMS_PROJECT_ID!,
secretKey: process.env.CMS_SECRET_KEY!
});
const drafts = await preview.content('content_item').list({locale: 'es'});
// Return only to a user your server has authorized to see drafts.
Choose build-time or request-time fetching
For static generation (SSG), run published reads in your build and await completion before writing pages. Use pages() or entries() with an explicit maxPages for larger collections. A failed or incomplete traversal should fail the build instead of silently shipping missing pages. Publishing or unpublishing in the CMS does not rebuild or remove an already generated website; your build trigger and cache invalidation are application responsibilities.
For server rendering (SSR), fetch inside your framework's server request lifecycle. Pass its fetch implementation when appropriate and propagate request cancellation through signal. Keep user-specific data and draft results out of shared caches and browser bundles.
The SDK requests no-store and does not cache or deduplicate. If your application caches published data, separate workspace/environment, credential scope, model, language and query variants, and choose an explicit expiry/invalidation policy. Never cache private previews as public content. Preserve signed media URLs and refresh expiring ones rather than treating them as permanent static addresses.
For browser rendering, use public credentials only, show loading/empty/error states and abort work when the view is no longer needed. REST, GraphQL and JSON-RPC remain independent APIs; the JavaScript SDK currently wraps REST.
import { cms } from './cms.js';
for await (const entry of cms.content('content_item').entries({
limit: 25, maxPages: 100
})) {
// Write your static page using entry.fields.
// Rendering, output paths and safe HTML handling belong to your build.
}
// Any fetch, validation or maxPages error should fail this build.
Handle errors and cancellation
CMSConfigurationError means invalid URL/workspace/key settings. TypeError can mean invalid query options or a network failure. CMSRequestError includes HTTP status and server code; CMSResponseError means an invalid response envelope.
The SDK does not automatically retry, cache or deduplicate requests. Integrate your application’s data library if needed. Pass AbortController.signal to list/get and abort when a component unmounts; cancellation preserves the signal's reason, normally AbortError.
Set timeoutMs on createCMSClient or createCMSPreviewClient to opt into a request deadline: a positive integer from 1 to 2147483647 milliseconds. There is no default deadline. CMSTimeoutError means that this budget expired while fetching or reading JSON. Each iterator page gets its own deadline; time spent processing a yielded page does not consume another request's budget. Your injected fetch should honor AbortSignal so cancellation can stop the underlying work.
Combining timeoutMs with a caller signal requires native AbortSignal.any; Node 22+ provides it. In a runtime without it, this optional combination fails locally with CMSConfigurationError instead of weakening cancellation. Existing requests without timeoutMs keep their original signal behavior. A timeout cannot preempt synchronous JavaScript or undo work in your consuming application.
import { CMSRequestError, CMSTimeoutError } from '@lemonadecms/client';
import { cms } from './cms.js';
const controller = new AbortController();
try {
const page = await cms.content('content_item').list({signal: controller.signal});
console.log(page.data);
} catch (error) {
if (error instanceof CMSTimeoutError) { /* show a timeout and let the user retry */ }
else if (controller.signal.aborted) { /* caller cancelled */ }
else if (error instanceof CMSRequestError) {
console.error(error.status, error.code, error.message);
} else throw error;
}
// Call controller.abort() from your framework's unmount cleanup.
References, images and publication
A parent cannot publish until its referenced entries and assets are published. Published dependencies cannot be unpublished while used, and deletion is blocked while draft or published references remain.
REST and RPC expand reference IDs into delivery objects with id/fields; the SDK preserves those nested wire shapes. Expansion is bounded, so keep the target ID when deeper fields are absent. GraphQL traverses references only when selected: related { id fields { title } }.
Image/file delivery includes metadata and a short-lived URL. Keep that URL intact; fetch fresh content to obtain a new URL after expiry. Asset requests recheck availability and key validity. PDFs download rather than display inline. Draft metadata stays private until publication.
Treat Markdown as source, and render it with a renderer that excludes raw HTML. Do not inject arbitrary source into innerHTML.