npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/sdk

Quick Start

1. Configure

Add your API key to the .env file in your project root:

PUBLIC_ZENO_API_KEY=zeno_your-project_your-api-key

2. Generate types + client

npx zeno generate        # npm
bunx zeno generate       # bun
pnpm dlx zeno generate   # pnpm
yarn dlx zeno generate   # yarn

The 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 (or bunx, 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-configured ZenoCMS client 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 typesZenoLocale, 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. isLoading is only true on 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:

  1. Go to Project Settings
  2. Add your domains to the Allowed Domains list
  3. Requests from unauthorized domains will receive a 403 error

Supported domain formats:

  • example.com - exact domain
  • *.example.com - all subdomains
  • localhost: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 .gitignore by convention
  • src/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-key

The 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   # yarn

This will:

  1. Fetch the full schema from the CMS
  2. Generate a .ts file with: pre-configured client (zeno, ZENO_API_KEY), CmsEntry<T> wrapper, and typed interfaces for every collection
  3. 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.ts

The 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: prebuild uses zeno generate without a runner prefix — this works because the zeno binary is resolved from node_modules/.bin/ via your package manager's script runner. If you need to run it outside of a script, use npx/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