JavaScript SDK
Everything the API does is reachable with `fetch` and a bearer token, and that is still true. The SDK is there because four things are tedious to write again on every site: cursor pagination, slug redirects, typed responses, and a view counter that never breaks the page.
Last updated
Install
npm install cmskiteNo dependencies. It ships ESM and CommonJS side by side with its own type declarations, so it works in Next.js, Vite, plain Node and a require call without a build step or an @types package.
import { createCMSKite } from 'cmskite'
const cms = createCMSKite({ apiKey: process.env.CMSKITE_KEY! })
const { items } = await cms.posts.list({ limit: 10 })Three entry points, on purpose
The content client runs anywhere. The tracker needs a browser. Keeping them apart means a server bundle never pulls in code that reaches for document, and a Node script never ships a view counter it cannot use.
| Import | What it holds | Runs where |
|---|---|---|
cmskite | The content client, errors, and every type | Anywhere |
cmskite/browser | The analytics tracker | Browser |
cmskite/react | useTrackView, useCMSKiteAnalytics | React, client components |
Reading content
// A page of posts.
const { items, pagination } = await cms.posts.list({ limit: 10 })
// One post by id.
const post = await cms.posts.get('post_01a0c...')
// One post by its address — what a /blog/[slug] page wants.
const post = await cms.posts.getBySlug('building-better-apps')
// The same, but null instead of a throw when there is no such post.
const post = await cms.posts.findBySlug(slug)
if (!post) return notFound()
// Posts like this one.
const { items } = await cms.posts.related(post.id, { limit: 3 })A list omits post bodies — twenty full articles is a payload nobody asked for — and says so with bodyOmitted: true. Fetching one post always includes it. cms.categories, cms.tags and cms.authors have the same shape.
const post = await cms.posts.getBySlug(params.slug)
if (post.slugRedirectedFrom) redirect(`/blog/${post.slug}`)Pagination without the loop
CMSKite paginates by cursor, not by page number. The cursor is opaque: pass it back, never parse it. For walking everything — a sitemap, a static build — all() is an async iterator, so a site with four thousand posts never holds four thousand posts in memory.
for await (const post of cms.posts.all({ status: 'published' })) {
sitemap.add(`/blog/${post.slug}`)
}Errors
Every failure — a 404, a 429, a timeout, a DNS error — arrives as one CMSKiteError carrying the HTTP status, the API error code and the requestId to quote in a support ticket. One catch, not two.
import { CMSKiteError, ErrorCode } from 'cmskite'
try {
const post = await cms.posts.getBySlug(slug)
} catch (error) {
if (error instanceof CMSKiteError) {
if (error.code === ErrorCode.NotFound) return notFound()
console.error(error.status, error.code, error.requestId)
}
throw error
}A request that failed for a reason worth retrying — a timeout, a 5xx, a 429 — is retried once automatically. A 404 or a bad key is not: retrying a definite answer only makes it slower.
Counting readers
One hook, in a client component. It reports the view when the post renders, and it is safe in an effect that runs on every render — the same post in the same tab is reported once.
'use client'
import { useTrackView } from 'cmskite/react'
export function TrackView({ postId }: { postId: string }) {
useTrackView(postId, { apiKey: process.env.NEXT_PUBLIC_CMSKITE_KEY! })
return null
}The tracker cannot throw, cannot block a render, and cannot double-count. Events are queued and flushed on a timer with sendBeacon, which still delivers after the reader has closed the tab. Nothing awaits a response, because there is nothing in the response.
Which key goes where
This is the one thing worth getting right, and the SDK cannot get it right for you.
| Where the code runs | Environment variable | Why |
|---|---|---|
Server — getStaticProps, a route handler, a Node script | CMSKITE_KEY | Never sent to the browser. |
| Browser — the analytics tracker | NEXT_PUBLIC_CMSKITE_KEY | It is in the bundle, and that is expected. |
A key in a browser bundle is readable by anybody who opens the network tab, so a CMSKite key can only read, only one project, and only from the origins you list. The worst somebody can do with a stolen one is read posts you already publish. Restrict allowed origins on the key before you ship it.
Versioning
Semver, strictly. A patch fixes a bug, a minor adds something you can ignore, and a major is the only kind that can require a change from you. Anything not exported from cmskite, cmskite/browser or cmskite/react is internal and can change in a patch — so do not import from a deep path.
The SDK talks to /v1 and will keep doing so. A v2 API would be a new major of the SDK, not a silent switch underneath a running site.