@zeno-cms/sdk
v1.3.8
Published
Official SDK for Zeno CMS - Fetch content, manage assets, send emails and more
Readme
@zeno-cms/sdk
Official TypeScript SDK for Zeno CMS - a headless content management system.
Installation
npm install @zeno-cms/sdk
# or
yarn add @zeno-cms/sdk
# or
pnpm add @zeno-cms/sdk
# or
bun add @zeno-cms/sdkQuick Start
1. Configure
Add your API key to the .env file in your project root:
PUBLIC_ZENO_API_KEY=zeno_your-project_your-api-key2. Generate types + client
npx zeno generate # npm
bunx zeno generate # bun
pnpm dlx zeno generate # pnpm
yarn dlx zeno generate # yarnThe CLI reads the API key from PUBLIC_ZENO_API_KEY in your .env and auto-detects the frontend name from the key format (zeno_<name>_<secret>). Output defaults to src/generated/zeno-types.ts.
3. Use the SDK
// Import the pre-configured client, types, and CmsEntry wrapper
import { zeno, type Blog, type CmsEntry } from '@/generated/zeno-types'
// Fetch entries (type-safe!)
const { data: posts } = await zeno.getEntries('blog', {
status: 'published',
limit: 10
})
// Cast with CmsEntry<T> for type-safe access
const typedPosts = (posts as CmsEntry<Blog>[] | null) ?? []
typedPosts.forEach(post => {
console.log(post.data.title) // TS auto-completion!
})Features
- Single command setup -
npx zeno generate(orbunx,pnpm dlx,yarn dlx) creates client + types from your.env - TypeScript first - Full type safety with auto-generated interfaces
- Localization built-in - Multi-language support with automatic fallback
- Resilient - Automatic retry with exponential backoff on transient errors
- Stale-while-revalidate - Framework hooks show cached data instantly while refetching
- Works everywhere - Node.js, React, Next.js, Vue, Svelte, Angular, Astro, Vanilla JS
Generated File
Running npx zeno generate (or the equivalent for your package manager) creates a file that exports:
zeno— Pre-configuredZenoCMSclient instance (for server-side / direct usage)ZENO_API_KEY— API key constant (for framework Providers/Plugins)CmsEntry<T>— Generic entry wrapper for type-safe data access- TypeScript interfaces — One per collection (
Blog,Prodotti, etc.) - Utility types —
ZenoLocale,ZenoCollectionSlug,ZenoCollectionMap
// src/generated/zeno-types.ts (auto-generated, do not edit)
import { ZenoCMS } from '@zeno-cms/sdk'
export const ZENO_API_KEY = 'zeno_my-project_xxx'
export const zeno = new ZenoCMS({ apiKey: ZENO_API_KEY })
/** Typed entry wrapper — cast entries for type-safe data access */
export interface CmsEntry<T> {
id: string
data: T
status: string
created_at: string
updated_at: string
[key: string]: unknown
}
export type ZenoLocale = 'it' | 'en'
export interface Blog {
title: string
content: string
cover_image: string
published_at: string
}
export interface Prodotti {
nome: string
prezzo: number
descrizione: string
}
export type ZenoCollectionSlug = 'blog' | 'prodotti'
export interface ZenoCollectionMap {
blog: Blog
prodotti: Prodotti
}API Reference
Initialization
The recommended way is to import the pre-configured client from the generated file:
import { zeno } from '@/generated/zeno-types'
const { data } = await zeno.getEntries('blog')You can also create a client manually if needed:
import { ZenoCMS } from '@zeno-cms/sdk'
const zeno = new ZenoCMS({
apiKey: 'zeno_my-project_xxx',
defaultLocale: 'en',
cacheTTL: 60, // Cache TTL in seconds (default: 60, set 0 to disable)
maxRetries: 3, // Retry attempts on transient errors (default: 3, set 0 to disable)
})
// Clear cached data manually (e.g. after a content update)
zeno.clearCache()Methods
getProject()
Get project information.
const { data } = await zeno.getProject()
// { id, name, slug, status, created_at }getCollections(params?)
Get all collections.
const { data, meta } = await zeno.getCollections({
page: 1,
limit: 10
})getCollection(slug)
Get a single collection by slug.
const { data } = await zeno.getCollection('blog')
// { id, name, slug, fields, ... }getEntries(collectionSlug?, params?)
Get entries with filtering, pagination, and localization.
const { data, meta } = await zeno.getEntries('blog', {
// Pagination
page: 1,
limit: 10,
sortBy: 'created_at',
sortOrder: 'desc',
// Filtering
status: 'published',
search: 'typescript',
// Date filtering
dateFrom: '2024-01-01',
dateTo: '2024-12-31',
dateField: 'publishedAt', // Optional: filter on data.publishedAt instead of created_at
// Localization
locale: 'it',
fallback_locale: 'en'
})
// meta contains: { total, page, limit, totalPages, locale, fallback_used, available_locales }getEntry(entryId, params?)
Get a single entry by ID.
const { data, meta } = await zeno.getEntry('entry-uuid', {
locale: 'it',
fallback_locale: 'en'
})getAssets(params?)
Get project assets.
const { data } = await zeno.getAssets({
page: 1,
limit: 20,
mimeType: 'image'
})resolveAsset(assetId, transform?)
Resolve an asset UUID to its public URL. Returns the URL string directly, or undefined if not found.
const imageUrl = await zeno.resolveAsset('asset-uuid')
// "https://...supabase.co/storage/v1/object/public/assets/..."
// With image transforms
const thumbUrl = await zeno.resolveAsset('asset-uuid', {
width: 400,
height: 300,
quality: 75,
resize: 'cover'
})resolveAssets(assetIds, transform?)
Resolve multiple asset UUIDs in a single query. Returns a Map<string, string>.
const urls = await zeno.resolveAssets([
entry.data.cover,
entry.data.avatar,
entry.data.gallery
])
urls.get(entry.data.cover) // "https://..."getLocales()
Get available locales for the project.
const { data } = await zeno.getLocales()
// [{ code: 'it', name: 'Italian', native_name: 'Italiano', is_default: true }]sendEmail(params)
Send an email via Zeno's email API.
const { data, error } = await zeno.sendEmail({
to: '[email protected]',
subject: 'Contact Form',
message: 'Hello!',
fromName: 'My App',
replyTo: '[email protected]'
})
if (error) console.error(error)
else console.log('Email sent:', data)Response Format
All methods return a consistent ZenoResponse<T>:
interface ZenoResponse<T> {
data: T | null
error: string | null
meta?: {
total?: number
page?: number
limit?: number
totalPages?: number
timestamp: string
locale?: string
fallback_used?: boolean
available_locales?: string[]
}
}
// Check for errors
const { data, error } = await zeno.getEntries('blog')
if (error) {
console.error('Error:', error)
} else {
console.log('Data:', data)
}Framework Integrations
Built-in hooks, composables, stores and signals for every major framework — zero external dependencies.
React
// main.tsx — Setup provider once
import { ZenoProvider } from '@zeno-cms/sdk/react'
import { ZENO_API_KEY } from '@/generated/zeno-types'
createRoot(document.getElementById('root')!).render(
<ZenoProvider apiKey={ZENO_API_KEY}>
<App />
</ZenoProvider>
)// components/BlogList.tsx — Use hooks
import { useEntries } from '@zeno-cms/sdk/react'
import type { Blog, CmsEntry } from '@/generated/zeno-types'
export function BlogList() {
const { data: posts, isLoading, error } = useEntries('blog', {
status: 'published'
})
if (isLoading) return <p>Loading...</p>
if (error) return <p>Error: {error}</p>
const typedPosts = (posts as CmsEntry<Blog>[] | null) ?? []
return typedPosts.map(post => (
<article key={post.id}>
<h2>{post.data.title}</h2>
</article>
))
}Available hooks: useEntries, useEntry, useCollections, useCollection, useAssets, useAssetUrl, useProject, useLocales, useZeno
Stale-while-revalidate: All hooks implement SWR — on refetch, the previous data stays visible while new data loads in the background.
isLoadingis onlytrueon the initial fetch. This applies to all framework integrations (React, Vue, Svelte, Angular).
Vue
// main.ts — Setup plugin once
import { createZeno } from '@zeno-cms/sdk/vue'
import { ZENO_API_KEY } from '@/generated/zeno-types'
app.use(createZeno({ apiKey: ZENO_API_KEY }))<!-- components/BlogList.vue -->
<script setup lang="ts">
import { useEntries } from '@zeno-cms/sdk/vue'
import type { Blog, CmsEntry } from '@/generated/zeno-types'
const { data: posts, loading } = useEntries('blog', {
status: 'published'
})
// Cast for type-safe access in template
const typedPosts = computed(() => (posts.value as CmsEntry<Blog>[] | null) ?? [])
</script>
<template>
<p v-if="loading">Loading...</p>
<article v-for="post in typedPosts" :key="post.id">
<h2>{{ post.data.title }}</h2>
</article>
</template>Available composables: useEntries, useEntry, useCollections, useCollection, useAssets, useAssetUrl, useProject, useLocales, useZeno
Svelte
<!-- +layout.svelte — Setup once -->
<script lang="ts">
import { initZeno } from '@zeno-cms/sdk/svelte'
import { ZENO_API_KEY } from '$lib/generated/zeno-types'
let { children } = $props()
initZeno({ apiKey: ZENO_API_KEY })
</script>
{@render children()}<!-- components/BlogList.svelte -->
<script lang="ts">
import { useEntries } from '@zeno-cms/sdk/svelte'
import type { CmsEntry } from '@/generated/zeno-types'
const { data: posts, loading, error } = useEntries('blog', {
status: 'published'
})
// Cast for type-safe access — $posts is the reactive store value
</script>
{#if $loading}
<p>Loading...</p>
{:else}
{#each ($posts as CmsEntry<Blog>[] | null) ?? [] as post}
<article>
<h2>{post.data.title}</h2>
</article>
{/each}
{/if}Available stores: useEntries, useEntry, useCollections, useCollection, useAssets, useAssetUrl, useProject, useLocales, useZeno
Angular
// app.config.ts — Setup provider once
import { provideZeno } from '@zeno-cms/sdk/angular'
import { ZENO_API_KEY } from '@/generated/zeno-types'
export const appConfig = {
providers: [
provideZeno({ apiKey: ZENO_API_KEY })
]
}// components/blog-list.component.ts
import { Component, computed } from '@angular/core'
import { useEntries } from '@zeno-cms/sdk/angular'
import type { Blog, CmsEntry } from '@/generated/zeno-types'
@Component({
selector: 'app-blog-list',
template: `
@if (posts.loading()) { <p>Loading...</p> }
@for (post of typedPosts(); track post.id) {
<article>
<h2>{{ post.data.title }}</h2>
</article>
}
`
})
export class BlogListComponent {
posts = useEntries('blog', { status: 'published' })
typedPosts = computed(() => (this.posts.data() as CmsEntry<Blog>[] | null) ?? [])
}Available signals: useEntries, useEntry, useCollections, useCollection, useAssets, useAssetUrl, useProject, useLocales, injectZeno
Next.js (Server Components)
// app/blog/page.tsx — Server-side, uses pre-configured client
import { zeno, type Blog, type CmsEntry } from '@/generated/zeno-types'
export default async function BlogPage() {
const { data: posts } = await zeno.getEntries('blog', {
status: 'published'
})
const typedPosts = (posts as CmsEntry<Blog>[] | null) ?? []
return typedPosts.map(post => (
<article key={post.id}>
<h2>{post.data.title}</h2>
</article>
))
}For Client Components, use ZenoProvider + hooks from @zeno-cms/sdk/react with ZENO_API_KEY from the generated file.
Astro
---
import { zeno, type Blog, type CmsEntry } from '@/generated/zeno-types'
const { data: posts } = await zeno.getEntries('blog', {
status: 'published'
})
const typedPosts = (posts as CmsEntry<Blog>[] | null) ?? []
---
<html>
<body>
{typedPosts.map(post => (
<article>
<h2>{post.data.title}</h2>
</article>
))}
</body>
</html>Vanilla JavaScript (Browser)
<script src="https://unpkg.com/@zeno-cms/sdk/dist/zeno.min.js"></script>
<script>
const zeno = new ZenoCMS({ apiKey: 'zeno_my-project_xxx' })
zeno.getEntries('blog', { status: 'published' })
.then(({ data }) => console.log(data))
</script>Security
API Key Validation
All API keys are validated server-side on every request. The API key format is:
zeno_<project-slug>_<secret>- The
<secret>portion is generated automatically when you create a project - You can regenerate the secret anytime from Project Settings > API Key
- Old secrets stop working immediately after regeneration
Domain Whitelisting (Browser/Vanilla JS)
When using the SDK in a browser environment, the API key is visible in the source code. To protect your project from unauthorized usage, you can configure allowed domains:
- Go to Project Settings
- Add your domains to the Allowed Domains list
- Requests from unauthorized domains will receive a 403 error
Supported domain formats:
example.com- exact domain*.example.com- all subdomainslocalhost:3000- local development
If the allowed domains list is empty, requests are accepted from any origin.
Client-Side Security Model
The SDK communicates with the Zeno CMS API through a public endpoint. Like other client-side SDKs (Firebase, Stripe, Algolia), the connection details are embedded in the bundle — this is by design and does not expose any secret.
Why this is safe:
- All data access is protected by server-side security policies. The public endpoint only serves data that the access rules explicitly allow — no admin or write access is possible.
- Every request is validated against your Zeno API key server-side, which acts as a second layer of access control.
- You can further restrict access by configuring Domain Whitelisting (see above), so only your domains can make requests.
In short: the public endpoint visible in your bundle does not grant any access beyond what the server-side policies permit. Sensitive credentials are never exposed — they exist only on the server side.
Best Practices
.env: Contains your API key (PUBLIC_ZENO_API_KEY) — already in.gitignoreby conventionsrc/generated/: Generated file with baked API key — add to.gitignore- Client-side (Browser): Configure allowed domains in project settings
- Regenerate the key if you suspect it has been compromised
CLI — Type Generation
The SDK includes a CLI that auto-generates TypeScript types and a pre-configured client from your CMS schema, and registers a contract so the CMS can warn you about breaking changes.
Setup
Add your API key to the .env file in your project root:
PUBLIC_ZENO_API_KEY=zeno_your-project_your-api-keyThe frontend name is auto-detected from the API key format (zeno_<name>_<secret>). Output defaults to src/generated/zeno-types.ts.
Generate types + client
npx zeno generate # npm
bunx zeno generate # bun
pnpm dlx zeno generate # pnpm
yarn dlx zeno generate # yarnThis will:
- Fetch the full schema from the CMS
- Generate a
.tsfile with: pre-configured client (zeno,ZENO_API_KEY),CmsEntry<T>wrapper, and typed interfaces for every collection - Register a "contract" in the CMS (which fields your frontend uses)
Usage
// Server-side — import the pre-configured client, types, and CmsEntry wrapper
import { zeno, type Blog, type CmsEntry } from '@/generated/zeno-types'
const { data: posts } = await zeno.getEntries('blog', {
status: 'published'
})
// Type-safe access with CmsEntry<T>
const typedPosts = (posts as CmsEntry<Blog>[] | null) ?? []
typedPosts.forEach(post => {
console.log(post.data.title) // fully typed
})// Client-side frameworks — import ZENO_API_KEY for the Provider
import { ZENO_API_KEY } from '@/generated/zeno-types'
// React: <ZenoProvider apiKey={ZENO_API_KEY}>
// Vue: app.use(createZeno({ apiKey: ZENO_API_KEY }))
// Svelte: initZeno({ apiKey: ZENO_API_KEY })
// Angular: provideZeno({ apiKey: ZENO_API_KEY })CLI flags
npx zeno generate --api-key=zeno_xxx # Override API key (instead of .env)
npx zeno generate --output=src/types.ts # Override output path
npx zeno generate --name=my-site # Override frontend name (instead of auto-detect)
# Same flags work with any package manager:
# bunx zeno generate --api-key=zeno_xxx
# pnpm dlx zeno generate --output=src/types.tsThe CLI also supports alternative env var names: ZENO_API_KEY, VITE_ZENO_API_KEY, NEXT_PUBLIC_ZENO_API_KEY, NUXT_PUBLIC_ZENO_API_KEY.
Automate in build
{
"scripts": {
"prebuild": "zeno generate",
"build": "astro build"
}
}Note:
prebuilduseszeno generatewithout a runner prefix — this works because thezenobinary is resolved fromnode_modules/.bin/via your package manager's script runner. If you need to run it outside of a script, usenpx/bunx/pnpm dlx/yarn dlx.
Types are regenerated on every build. If the CMS schema changes, just rebuild.
Frontend contract
When you run zeno generate, the CLI also registers a contract in the CMS — a list of collections and fields your frontend uses. If someone renames or removes a field used by your frontend, the CMS field editor shows a warning banner.
TypeScript
Full TypeScript support with exported types:
import {
ZenoCMS,
ZenoConfig,
ZenoResponse,
Collection,
Entry,
Asset,
ImageTransformOptions,
Locale,
Project,
Field,
PaginationParams,
FilterParams,
EmailParams
} from '@zeno-cms/sdk'License
MIT
