@lemonadecms/client
v0.2.0
Published
Framework-independent content delivery and developer tooling for Lemonade CMS
Readme
@lemonadecms/client
Framework-independent JavaScript and TypeScript SDK for reading Lemonade CMS
content. Published-content requests use REST. The package includes TypeScript
declarations, schema type generation and the lemonade developer CLI.
Install
Install the published package in your application:
npm install @lemonadecms/client
# or
pnpm add @lemonadecms/clientMaintainers can test a future version using its reviewed candidate archive.
Check the supplied SHA256SUMS, then use the actual archive path:
npm install --ignore-scripts /absolute/path/to/reviewed-client.tgz
# or
pnpm add /absolute/path/to/reviewed-client.tgzThe package uses ESM and declares Node.js 22+. TypeScript consumers need
TypeScript 5+. Browser reads require standard fetch, URL, URLSearchParams
and AbortController; browser builds must use public credentials only. No
runtime library or framework dependency is required.
Configure and read content
Version 0.2.0 connects to https://api.lemonadecms.com automatically. Strict hostname routing returns 404 for API URLs on https://lemonadecms.com. Upgrade earlier clients to 0.2.0 and change explicit CLI/configured-target URLs to https://api.lemonadecms.com. Explicit CLI settings still take precedence; no redirect or fallback is attempted. Configure your
workspace and delivery key; there is no URL or local CMS option. Your website
can run locally while reading content from Lemonade production. Installing the
package does not create an account, workspace or delivery key.
Replace the workspace and key placeholders with values from your Lemonade workspace. Replace content_item and title with your content type and field
API identifiers, and publish an entry before fetching it.
import { createCMSClient } from '@lemonadecms/client';
const cms = createCMSClient({
projectId: 'YOUR_WORKSPACE_UUID',
publicKey: 'YOUR_PUBLIC_DELIVERY_KEY',
});
const page = await cms.content('content_item').list({
select: ['title'],
limit: 10,
});
for (const entry of page.data) {
console.log(entry.id, entry.fields.title);
}
const first = page.data[0];
if (first) {
const entry = await cms.content('content_item').get(first.id);
console.log(entry.fields, entry.publishedAt);
}Entry values are under fields; metadata includes id and publishedAt.
The client also exposes contentType (ID/key/name), projectId,
environment (ID/key) and resolved locale. Field projections retain metadata.
For older CMS responses, the SDK derives only the request's model key/workspace
and explicit locale; unavailable metadata stays absent. Metadata properties are
optional in TypeScript for compatibility with existing manually constructed
entries. Use entry.contentType?.key, not a custom fields.contentType field.
Generated Models describes fields; ContentEntry<Models['content_item'],
'content_item'> from the SDK describes the complete entry. Expanded reference
metadata remains raw snake_case. Published 0.1.0 does not include these additions.
An empty collection is data: []. Historical or selected fields can be missing
or null, so handle both when rendering. Continue a collection using
page.meta.nextCursor as the next request's after value while keeping the
other query options unchanged.
Use an existing pk_ public delivery key or an lmd_pk_ application token with
the required published-content grants. Anyone with that credential can read
the content it permits. Browser login cookies do not authorize SDK requests.
The credential selects the content environment; there is no separate SDK
environment override. Ask your Owner for a credential belonging to the intended
environment. The server rechecks current access, expiry and revocation on every
request.
Find one entry
In the updated source build, findOne() returns the first matching complete
entry or null. It makes one request with limit=1 and preserves existing
filters, search, sort, select, locale, includeRemote and signal options. It does not check
uniqueness. Pagination/limit options are rejected; HTTP/authorization, transport,
cancellation/deadline and malformed-response errors still reject. get(id) keeps
its existing UUID lookup and 404 behavior. Published 0.1.0 lacks these helpers.
const entry = await cms.content('content_item').findOne({
filters: [{ field: 'title', op: 'eq', value: 'Welcome' }],
select: ['title'],
});
console.log(entry?.fields.title ?? 'No matching content');Counts, existence and bulk reads
These methods require the current SDK and CMS backend. They are available in both published and server preview clients:
import { CMSNotFoundError } from '@lemonadecms/client';
const content = cms.content('content_item');
const filters = [{ field: 'title', op: 'eq', value: 'Welcome' }];
const total = await content.count({ filters });
const available = await content.exists({ filters });
try {
const required = await content.findOneOrThrow({ filters, select: ['title'] });
const entries = await content.getMany([required.id], { select: ['title'] });
console.log(total, available, entries[0]?.fields.title);
} catch (error) {
if (!(error instanceof CMSNotFoundError)) throw error;
console.log('Required content is unavailable');
}count()/exists() accept filters, search, locale and signal, using direct database
queries without downloading entries or expanding references. Count includes all
matches across pages; separate requests can see different publication states.
getMany() accepts up to 100 UUIDs and select/locale/includeRemote/signal. One
request returns entries in input order, preserving duplicate positions and using
null for missing/unpublished/foreign IDs in an otherwise authorized model.
Empty input returns [] without I/O after validation. Split larger ID sets into
batches; use the existing lazy entries()/pages() methods for collections.
findOneOrThrow() shares findOne options and throws CMSNotFoundError only for
a successful empty match. Authorization, HTTP, malformed-response, network,
cancellation and deadline failures retain their existing errors. Preview fields
may still be missing/null. No automatic collection-download fallback occurs.
Advanced filters, search and sorting
Updated source builds support nested and/or/not, ne/not_in, and text
eqi, contains/containsi, starts_with/starts_withi, ends_with/ends_withi.
Existing filter arrays remain AND. Limits are ten leaves, 31 nodes, depth four
and 4 KiB; groups are nonempty. SQL wildcard characters are treated literally.
Positive comparisons on missing values are false; NOT and ne/not_in complement
them, including missing values. Use eq/ne null for explicit null checks.
const matches = await cms.content('content_item').search('welcome', {
fields: ['title'],
select: ['title'],
filters: {
or: [
{ field: 'title', op: 'containsi', value: 'WELCOME' },
{ not: { field: 'title', op: 'eq', value: 'Archived' } },
],
},
sort: [
{ field: 'position', direction: 'desc' },
{ field: 'title', direction: 'asc' },
],
limit: 10,
});
console.log(matches.data);fields scopes matching to 1–24 text/long_text/markdown fields; select projects
the returned fields. Omit fields to search all top-level fields of those types.
Query accepts 1–200 Unicode codepoints, up to 512 UTF-8 bytes and 16 whitespace
terms. PostgreSQL simple-config plain search requires every parsed term, with
cover-density relevance order by default. Explicit 1–3 unique sort fields override
relevance; NULLs stay last and UUID breaks ties. No stemming, fuzzy/prefix/phrase
search or recursive rich-text/reference search is implied. Matching and ranking
use only the authorized locale/fallback and published/preview snapshot.
search() makes one normal paginated collection request. Pass the same
search: {query, fields} object to list/findOne/findOneOrThrow/count/exists/pages/
entries to count or traverse matches. Keep the full query and ordered sort fields
unchanged when following cursors. Existing single-sort syntax remains supported.
Signed cursors have an 8 KiB limit. Unusually large Unicode text anchors that
exceed it after compression return invalid_query; choose more compact scalar
sort fields in that case.
These methods require the updated SDK/backend. Runtime/peer/optional dependencies
remain empty; no external search service is used.
Rich-text helpers
import { richTextToHtml, richTextToPlainText } from '@lemonadecms/client';
// Replace rich_body with a rich_text field from your schema.
const richEntry = await cms.content('content_item').findOne({ select: ['rich_body'] });
const html = richTextToHtml(richEntry?.fields.rich_body);
const plainText = richTextToPlainText(richEntry?.fields.rich_body);Both accept nullable/unknown version-1 rich-text JSON and work in browsers or servers without a framework or DOM. HTML escapes text/attributes and renders only supported nodes/marks, safe links and matching expanded image/entry-caption resources. Unavailable embeds are omitted; entry routes are never invented. Plain text retains authored text with block/list/hard-break newlines, omitting embeds and link URLs. Plain text is not escaped HTML: use text interpolation.
Traversal is bounded to 500 nodes, depth 8, 200 children per node, 20 embeds, 20,000 UTF-8 bytes per text and 64 KiB of rendered source strings. Unsupported versions, invalid roots/containers or exhausted budgets return empty strings; unsupported parts are omitted. No raw HTML, resource-field recursion, I/O or automatic Markdown conversion is performed. HTML URL/alt bounds and exact preview-image conventions are documented in the SDK guide. The helpers are also exported from the server entry; preview authorization remains your responsibility.
Types, queries and framework examples
The SDK guide covers generated Models,
field selection, filtering, sorting, localization, cursor and numbered
pagination, errors, cancellation, images, and JavaScript/React/Vue/Nuxt examples.
Generate types from your own authorized schema and regenerate them after model
changes. Types help at compile time; they do not authenticate a request or
validate every field at runtime.
There is no automatic caching or retry policy. Your application owns rendering,
loading and error states, cache invalidation and safe HTML handling. Pass an
AbortSignal to cancel a read. Optional timeoutMs sets a per-request deadline;
combining a timeout with a caller signal requires AbortSignal.any.
Choose an environment for generated types
The updated source CLI offers an interactive picker from configured targets:
npx lemonade types generateCreate lemonade.config.json in your application. Replace both workspace
placeholders with your workspace UUID; the target labels distinguish environments.
{
"version": 1,
"targets": {
"production": {
"url": "https://api.lemonadecms.com",
"workspace": "YOUR_WORKSPACE_UUID",
"environment": "production",
"credentialEnv": "CMS_PUBLIC_KEY"
},
"dev": {
"url": "https://api.lemonadecms.com",
"workspace": "YOUR_WORKSPACE_UUID",
"environment": "dev",
"credentialEnv": "CMS_DEV_PUBLIC_KEY"
}
}
}Keep actual credentials in your current directory's .env or process environment;
process values win. A dev target needs a dev-scoped credential. The credential
selects access; the CLI verifies the returned workspace/environment before writing.
The picker lists configured targets, not environments discovered from the CMS.
Use arrows and Enter. Production is initially selected when configured; otherwise the first target alphabetically. Escape/Ctrl+C/EOF exits 130 without network or output-file writes. Terminal input mode is restored on success, cancellation/error. Missing/revoked/mismatched credentials fail without replacing existing types.
Explicit --target, --environment, --url, --workspace or --credential-env
bypasses the picker, as do LEMONADE_TARGET, LEMONADE_ENVIRONMENT or
CMS_ENVIRONMENT. CI, --json, --check, piped input/output, --file and other
commands never prompt. --config can choose the target configuration file.
Without configured targets, existing defaults remain unchanged. For automation:
npx lemonade types generate --target dev --checkVersion 0.2.0 includes the picker; historical 0.1.0 does not. It uses native Node APIs and retains zero runtime, peer or optional dependencies.
Private previews
For draft reads, use createCMSPreviewClient from
@lemonadecms/client/server in trusted server code with a server-only
secretKey. Authenticate and authorize the requesting user in your application
before fetching or returning drafts. Do not create an anonymous preview proxy.
Never put preview credentials or private responses in browser bundles, public
environment variables, shared caches, static output or logs. The server export
rejects browser use; that guard does not replace your application's authorization.
@lemonadecms/client/preview is a separate optional click-to-edit helper for an
already authorized website preview. It provides no preview credential or draft
fetch client.
Guides
License
This SDK package is licensed under the MIT License. Its license does not grant a license to the entire CMS repository or the separate native clients.
Visual Composer
The optional Visual Composer adds approved nested sections, inspector editing and
private live updates. A developer connects the website registry and explicitly
enables unsaved projection on its trusted preview destination. Save draft and
Publish remain separate. Component text can be translated by stable block ID;
each language keeps its live tree until explicitly published. Existing form and
saved-draft previews remain supported. The browser helper is the dependency-free
@lemonadecms/client/composer export.
