JavaScript client
@avvyr/cms is a framework-agnostic client for the delivery API. It runs in Node 18+, edge runtimes and the browser. On Nuxt, use the Nuxt layer instead; it builds on this package. On Next.js, use this client directly from Server Components; it works alongside @avvyr/storefront-next.
npm install @avvyr/cms
Create a client
import { createCmsClient } from '@avvyr/cms'
const cms = createCmsClient({
siteId: process.env.CMS_SITE_ID!,
key: process.env.CMS_KEY!, // public key: published content
previewKey: process.env.CMS_PREVIEW_KEY, // server only: drafts
locale: 'sv', // default: the site's default language
// baseUrl: 'https://api.avvyr.com',
// versionTtlMs: 5000, timeoutMs: 15000, fetch, headers
})
Read content
const page = await cms.page('/om-oss', { locale: 'sv' })
const home = await cms.page('/')
const article = await cms.entry('6701f0c2a4d3e5f6a7b8c9d1')
const latest = await cms.entries({ type: 'article', sort: '-publishedAt', limit: 10 })
const nav = await cms.singleton('navigation')
const manifest = await cms.manifest()
const { manifest: m, documents } = await cms.bundle()
const hits = await cms.search('soffa', { limit: 5 })
const draft = await cms.draftPage('/om-oss', { locale: 'sv', previewToken })
| Method | Delivery route |
|---|---|
version({ fresh? }) | GET /version |
page(path, { locale, version }) | GET /v/{version}/{locale}/pages/{path} |
entry(id, ...) | GET /v/{version}/{locale}/entries/{id} |
entries({ type, folder, deep, ids, sort, limit, offset, include, locale, version }) | GET /v/{version}/{locale}/entries |
singleton(type, ...) | GET /v/{version}/{locale}/singletons/{type} |
manifest(...), bundle(...) | GET /v/{version}/{locale}/manifest, /bundle |
changes(from, to) | GET /changes |
search(q, { locale, type, limit }) | GET /{locale}/search |
draftPage(path, { locale, previewToken }), draftEntry(id, ...) | GET /draft/{locale}/... |
The client asks /version first and remembers it for versionTtlMs (5 seconds). Every content request then goes to an immutable /v/{version}/... URL, which your CDN and the browser can cache forever. If the version moved on, the client follows the 302 and remembers the new version. invalidateVersion() forgets it at once.
Errors
import { CmsNotFoundError } from '@avvyr/cms'
try {
const page = await cms.page(path)
} catch (err) {
if (err instanceof CmsNotFoundError) return notFound()
throw err
}
All errors extend CmsError (status, errorCode, details): CmsNotFoundError (404), CmsAuthError (401/403), CmsValidationError (400), CmsConflictError (409), CmsNetworkError and CmsConfigError.
Render blocks
A blocks field is a list of { id, type, fields }. Map each type to a component, and skip unknown types so a new block type in the CMS never breaks the page. In React:
import type { CmsBlock } from '@avvyr/cms'
import { Hero } from './blocks/Hero'
import { TextSection } from './blocks/TextSection'
const blocks: Record<string, React.ComponentType<{ fields: any, first: boolean }>> = {
hero: Hero,
textSection: TextSection,
}
export function CmsBlocks({ items }: { items?: CmsBlock[] | null }) {
return (items ?? []).map((block, i) => {
const Component = blocks[block.type]
return Component
? <div key={block.id} data-cms-block={block.id} data-cms-type={block.type} style={{ display: 'contents' }}>
<Component fields={block.fields} first={i === 0} />
</div>
: null
})
}
The data-cms-block wrapper is only needed for live editing.
Images
import { mediaUrl, mediaSrcset } from '@avvyr/cms'
mediaUrl(fields.image, { width: 800, format: 'webp', quality: 80 })
mediaSrcset(fields.image, [400, 800, 1200], { format: 'avif' })
Both use the Assets API transforms on the published URL. SVG and GIF are returned unchanged (isPassthroughMedia). Use fields.image.alt for alt text, and focalPoint ([x, y] from 0 to 1) for object-position when you crop.
Rich text
import { renderRichTextHtml, richTextToPlainText } from '@avvyr/cms'
const html = renderRichTextHtml(fields.body) // escaped HTML, unsafe hrefs dropped
const description = richTextToPlainText(fields.body).slice(0, 160)
In a component framework, walk the document yourself for full control, and use each link mark's attrs.href, which the delivery API fills in for links to entries.
TypeScript types
npx avvyr-cms types --out cms.d.ts generates interfaces for your types, such as PageDocument and HeroBlock. See Types and fields.
import type { PageDocument } from './cms'
const page = await cms.page<PageDocument['fields']>('/om-oss')