Skip to content

API Client

import { createKenresoftClient, KenresoftApiError } from '@kenresoft-cms/astro';
export const cms = createKenresoftClient({
url: import.meta.env.PUBLIC_KENRESOFT_CMS_URL,
});
Option Purpose
url The CMS API base URL (required). Use '/cms' in the browser behind the proxy
previewToken Binds the client to one request’s Live Preview token
cookies The incoming request’s cookie header, so server-side calls know who is signed in
fetch Override the global fetch (testing). cookies is ignored when you supply one

The package is a thin fetch wrapper with no Astro dependency. It is meant to be used through a bundler (Astro, Vite, esbuild) with TypeScript’s moduleResolution set to Bundler, which is the default in Astro projects. Importing its built files directly from plain Node.js ESM is not supported. It talks only to public, auth and signed-in account routes, never the admin API.

const posts = await cms.entries.list({ contentType: 'blog-post' }); // Entry[] ([] if none)
const featured = await cms.entries.list({ contentType: 'blog-post', featured: true }); // only Featured entries
const post = await cms.entries.get({ contentType: 'blog-post', slug }); // Entry | null

Only published entries are returned. A missing slug and a draft both resolve to null, identical from the outside. Any other non-2xx response throws KenresoftApiError. entries.preview({ contentType, slug, token }) fetches with an explicit preview token.

cms.media.url({ id }); // string: URL of the file, no request made
await cms.media.get({ id }); // { altText, contentType, width, height } | null
await cms.media.byFolder({ slug }); // items in a media folder ([] if the folder is unknown)
try {
await cms.forms.submit({ formSlug: 'contact', data: { name: 'Ada', message: 'Hi' } });
} catch (err) {
if (err instanceof KenresoftApiError && err.issues) {
// err.issues: { path: (string | number)[], message: string }[]
}
}

submit sends a plain object as JSON. For a form with a file field, pass a FormData instead and it is sent as multipart/form-data:

const data = new FormData(formElement); // includes the <input type="file" name="document">
await browserCms.forms.submit({ formSlug: 'support', data });

To pass a Turnstile token for a form with the human check, add turnstileToken: cms.forms.submit({ formSlug, data, turnstileToken }). On a failure, KenresoftApiError.code is set when the API sends one (for example TURNSTILE_REQUIRED). turnstileToken and code need @kenresoft-cms/astro 0.10.0 or later.

It throws for any non-2xx response: 400 (with issues, or a TURNSTILE_REQUIRED code), 401 (the form requires an account and nobody is signed in), 403 (untrusted Origin), 404, 409 (the form refuses duplicates and this one was already submitted, or an entry the form is about is closed), 429, or 500 (a file couldn’t be stored; nothing was kept, so it’s safe to retry). It sends the session cookie, so for a form that requires an account call it from the browser through the proxy. FormData needs @kenresoft-cms/astro 0.8.0 or later; older versions only send JSON.

For forms that require a website account, the signed-in account’s own submissions. Every call is scoped to the session’s account: another account’s submission id behaves exactly like one that doesn’t exist.

await browserCms.account.submissions.list({ form: 'support' }); // AccountSubmissionSummary[] (form optional)
await browserCms.account.submissions.get({ id }); // AccountSubmissionDetail | null
browserCms.account.submissions.fileUrl({ id, mediaId }); // string: a download link, no request made
await browserCms.account.submissions.sendMessage({ id, body, files }); // AccountSubmissionMessage

A detail has the submission’s data, stage, stages, stageHistory, messages (each from: 'staff' | 'account', with its attachments) and files (the files submitted with the form, keyed by field name). The inbox status, test flag and staff email addresses are never included. sendMessage takes up to 5 Files; it needs a trusted Origin, so call it from browser code through the proxy. fileUrl downloads only for the signed-in owner.

Server-side, use cmsForRequest(Astro.request).account.submissions.get({ id }) to render the same data with the visitor’s cookie. Requires @kenresoft-cms/astro 0.8.0 or later.

await cms.pages.list(); // { id, route, title }[] of published pages
await cms.pages.resolve({ route: '/about' }); // Page | null (previewToken honored)
await cms.pages.preview({ route, token });
await cms.reusableBlocks.get({ id }); // ReusableBlock | null
await cms.routePatterns.list(); // { contentTypeSlug, routePattern }[]

See Rendering pages.

await cms.settings.general(); // also contact(), social(), navigation(), footer(), seo()
await cms.globalVariables.list(); // Record<string, string>, {} if none
await cms.system.status(); // { emailConfigured, authSecretConfigured, turnstileSiteKey }

Each settings module resolves its typed shape, or {} (never null) if it has never been saved. See Content model. system.status() is unauthenticated and deployment-wide — turnstileSiteKey is the main reason a frontend calls it: read the Turnstile site key from here instead of needing your own copy in a PUBLIC_ env var. See Authentication.

cms.auth is documented in Authentication. cms.commerce (categories, products, cart, checkout, payments, customer) exists for the Commerce plugin and returns 404 errors when it is not installed or enabled.

KenresoftApiError has status, message, optional issues (form validation) and optional code (a machine-readable code from Better Auth, such as EMAIL_NOT_VERIFIED). See Error codes.

Export Purpose
getPreviewToken(input) Reads preview_token from a URL, URL string or Request. Returns null when absent
createCmsProxy(options) Same-origin proxy handler. See proxy
resolveRoute, resolveSiteRoute, matchRoutePattern Pure URL-to-content resolution
resolveNavigationItems(items, pages) Resolves a navigation item’s page reference to its route
registerBlockRenderer, resolveBlockRenderer Developer override registry for block components
renderField, registerFieldRenderer, resolveFieldRenderer Read-only display shapes for field values

There is deliberately no contentTypes.list(): the public API does not expose field definitions.