Skip to content

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

Examplebash
npm install cmskite

No 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.

Examplets
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.

ImportWhat it holdsRuns where
cmskiteThe content client, errors, and every typeAnywhere
cmskite/browserThe analytics trackerBrowser
cmskite/reactuseTrackView, useCMSKiteAnalyticsReact, client components

Reading content

Examplets
// 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.

Examplets
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.

Examplets
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.

Examplets
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.

app/blog/[slug]/track-view.tsxtsx
'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 runsEnvironment variableWhy
Server — getStaticProps, a route handler, a Node scriptCMSKITE_KEYNever sent to the browser.
Browser — the analytics trackerNEXT_PUBLIC_CMSKITE_KEYIt 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.