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

primo-cms-sdk

v0.0.6

Published

Typed client for consuming Primo CMS content from web applications.

Downloads

24

Readme

Primo CMS SDK

Official TypeScript/JavaScript client for working with Primo CMS. Production-ready with built-in caching, type safety, and security features.


Table of Contents

  1. Overview
  2. Installation
  3. Quick Start
  4. API Reference
  5. Authentication
  6. Advanced Usage
  7. Migration Guide
  8. CLI Tool
  9. Examples

Overview

The Primo SDK provides:

  • Type Safety - Full TypeScript support with generated types from your schemas
  • Caching - Built-in memory and Redis cache adapters
  • Security - Webhook signature verification and secure API key handling
  • Error Handling - Typed error classes with automatic retry logic
  • React Hooks - Client component integration with loading states
  • Server Components - Optimized for Next.js App Router
  • CLI Tool - Automatic type generation from your CMS schemas

Features

  • Fetch content by page/section slug
  • List all sections for a page
  • React hooks for client components
  • Webhook signature verification
  • Automatic request retries with exponential backoff
  • Configurable caching (memory, Redis)
  • TypeScript type generation from schemas

Installation

npm install primo-cms-sdk
# or
yarn add primo-cms-sdk
# or
pnpm add primo-cms-sdk

Quick Start

Basic Usage

import { PrimoClient } from "primo-cms-sdk"

// Create client instance
const client = new PrimoClient({
  apiBaseUrl: process.env.PRIMO_API_BASE_URL!,
  siteId: process.env.PRIMO_SITE_ID!,
  environmentKey: process.env.PRIMO_ENVIRONMENT_KEY!,
})

// Fetch a section
const hero = await client.sections.get({ 
  page: "landing", 
  section: "hero" 
})

// Fetch all sections for a page
const sections = await client.sections.list({ page: "landing" })

// Access typed content
console.log(hero.title) // TypeScript knows this exists!

Next.js Integration

Server Components (Recommended)

// lib/primo-server.ts
import "server-only"
import { PrimoClient } from "primo-cms-sdk"
import { cache } from "react"

export const primo = new PrimoClient({
  apiBaseUrl: process.env.PRIMO_API_BASE_URL!,
  siteId: process.env.PRIMO_SITE_ID!,
  environmentKey: process.env.PRIMO_ENVIRONMENT_KEY!,
})

// Cached for the duration of the request
export const getSectionsCached = cache(async (page: string) => {
  return primo.sections.list({ page })
})
// app/page.tsx
import { getSectionsCached } from "@/lib/primo-server"

export const revalidate = 60 // ISR: revalidate every 60 seconds

export default async function HomePage() {
  const sections = await getSectionsCached("home")
  
  return (
    <main>
      {sections.map(section => (
        <SectionRenderer key={section.slug} data={section} />
      ))}
    </main>
  )
}

Client Components (Interactive)

// lib/primo.ts
"use client"

import { PrimoClient } from "primo-cms-sdk"

export const primo = new PrimoClient({
  apiBaseUrl: process.env.NEXT_PUBLIC_API_BASE_URL!,
  siteId: process.env.NEXT_PUBLIC_SITE_ID!,
  environmentKey: process.env.NEXT_PUBLIC_ENVIRONMENT_KEY!,
})
"use client"

import { createUseSection } from "primo-cms-sdk/react"
import { primo } from "@/lib/primo"

const useSection = createUseSection(primo)

export function HeroPreview() {
  const { data, isLoading, error, reload } = useSection({ 
    page: "landing", 
    section: "hero" 
  })
  
  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>
  
  return (
    <div>
      <Hero data={data} />
      <button onClick={reload}>Refresh</button>
    </div>
  )
}

React Hooks

"use client"

import { createUseSection, createUseSections } from "primo-cms-sdk/react"

// Single section hook
const useSection = createUseSection(primo)
const { data, isLoading, error, reload } = useSection({ 
  page: "home", 
  section: "hero" 
})

// Multiple sections hook
const useSections = createUseSections(primo)
const { data, isLoading, error, reload } = useSections({ page: "home" })

API Reference

PrimoClient

Main client for interacting with Primo CMS.

const client = new PrimoClient({
  apiBaseUrl: string         // Base URL of Primo API
  siteId: string             // Site ID from Primo CMS
  environmentKey: string     // Environment API key
  timeout?: number           // Request timeout (default: 30000ms)
  retries?: number           // Number of retries (default: 3)
  headers?: Record<string, string>  // Additional headers
})

Sections API

sections.get()

Fetch a single section by page and section slug.

const section = await client.sections.get({
  page: string,      // Page slug (e.g., "landing")
  section: string,   // Section slug (e.g., "hero")
  locale?: string,   // Optional locale (default: site default)
})

// Returns: Section object with typed fields

sections.list()

Fetch all sections for a page.

const sections = await client.sections.list({
  page: string,      // Page slug
  locale?: string,   // Optional locale
})

// Returns: Array of Section objects

Error Handling

The SDK provides typed error classes:

import { 
  PrimoError,                 // Base error class
  PrimoNetworkError,          // Network/connectivity errors
  PrimoAuthenticationError,   // API key invalid
  SectionNotFoundError,       // Section doesn't exist
  PrimoTimeoutError,          // Request timeout
  PrimoValidationError,       // Invalid request parameters
} from "primo-cms-sdk"

try {
  const section = await client.sections.get({ page: "home", section: "hero" })
} catch (error) {
  if (error instanceof SectionNotFoundError) {
    console.log("Section not found, showing default content")
  } else if (error instanceof PrimoAuthenticationError) {
    console.error("Invalid API key")
  } else if (error instanceof PrimoNetworkError) {
    console.error("Network error, retrying...")
  }
}

Authentication

Environment API Keys

# .env.local
PRIMO_API_BASE_URL=http://localhost:5080
PRIMO_SITE_ID=your-site-id
PRIMO_ENVIRONMENT_KEY=your-environment-key

Security Best Practices:

  1. Never commit API keys - Use .env.local (add to .gitignore)
  2. Use server-side only - Keep environment keys in server components
  3. Separate keys per environment - Different keys for dev/staging/production
  4. Rotate regularly - Rotate keys every 90 days
  5. Monitor usage - Set up alerts for suspicious activity

Server-Side Only

// ✅ GOOD: Server Component (key never sent to browser)
import "server-only"
import { PrimoClient } from "primo-cms-sdk"

const client = new PrimoClient({
  apiBaseUrl: process.env.PRIMO_API_BASE_URL!,
  siteId: process.env.PRIMO_SITE_ID!,
  environmentKey: process.env.PRIMO_ENVIRONMENT_KEY!, // Safe on server
})
// ❌ BAD: Client Component (key exposed to browser)
"use client"

const client = new PrimoClient({
  environmentKey: process.env.NEXT_PUBLIC_ENVIRONMENT_KEY, // Exposed!
})

Proxy Pattern for Client Components

// app/api/primo-proxy/[...path]/route.ts
import { primo } from "@/lib/primo-server"

export async function GET(
  request: Request,
  { params }: { params: Promise<{ path: string[] }> }
) {
  const { path } = await params
  const { searchParams } = new URL(request.url)
  const page = searchParams.get("page")!
  const section = searchParams.get("section")

  if (section) {
    const data = await primo.sections.get({ page, section })
    return Response.json(data)
  } else {
    const data = await primo.sections.list({ page })
    return Response.json(data)
  }
}

// Client component uses proxy
"use client"
const response = await fetch(`/api/primo-proxy?page=home&section=hero`)
const data = await response.json()

Advanced Usage

Caching

Memory Cache (Default)

import { CachedPrimoClient, MemoryCacheAdapter } from "primo-cms-sdk"

const cache = new MemoryCacheAdapter({ 
  ttl: 300,           // 5 minutes
  maxSize: 100,       // Max 100 entries
})

const client = new CachedPrimoClient({
  apiBaseUrl: process.env.PRIMO_API_BASE_URL!,
  siteId: process.env.PRIMO_SITE_ID!,
  environmentKey: process.env.PRIMO_ENVIRONMENT_KEY!,
}, cache)

// Subsequent calls within 5 minutes use cache
await client.sections.get({ page: "home", section: "hero" }) // API call
await client.sections.get({ page: "home", section: "hero" }) // From cache

Redis Cache

import { CachedPrimoClient, RedisCacheAdapter } from "primo-cms-sdk"
import Redis from "ioredis"

const redis = new Redis(process.env.REDIS_URL)
const cache = new RedisCacheAdapter(redis, { 
  ttl: 300,                    // 5 minutes
  keyPrefix: "primo:",         // Key prefix
})

const client = new CachedPrimoClient({
  apiBaseUrl: process.env.PRIMO_API_BASE_URL!,
  siteId: process.env.PRIMO_SITE_ID!,
  environmentKey: process.env.PRIMO_ENVIRONMENT_KEY!,
}, cache)

Custom Cache Adapter

import { CacheAdapter } from "primo-cms-sdk"

class CustomCacheAdapter implements CacheAdapter {
  async get<T>(key: string): Promise<T | null> {
    // Your cache retrieval logic
  }

  async set<T>(key: string, value: T, ttl?: number): Promise<void> {
    // Your cache storage logic
  }

  async delete(key: string): Promise<void> {
    // Your cache deletion logic
  }

  async clear(): Promise<void> {
    // Clear all cache
  }
}

const client = new CachedPrimoClient(config, new CustomCacheAdapter())

Webhooks

Verify webhook signatures to ensure requests come from Primo CMS:

// app/api/webhooks/primo/route.ts
import { verifyWebhookSignature } from "primo-cms-sdk/webhooks"
import { revalidatePath } from "next/cache"

export async function POST(request: Request) {
  const body = await request.text()
  const signature = request.headers.get("x-primo-signature")
  
  // Verify signature
  const isValid = verifyWebhookSignature({
    payload: body,
    signature: signature!,
    secret: process.env.PRIMO_WEBHOOK_SECRET!,
  })
  
  if (!isValid) {
    return new Response("Invalid signature", { status: 401 })
  }
  
  const data = JSON.parse(body)
  
  // Handle webhook event
  if (data.event === "content.published") {
    // Revalidate affected paths
    revalidatePath(`/${data.page}`)
  }
  
  return new Response("OK", { status: 200 })
}

Retry Logic

The SDK automatically retries failed requests:

const client = new PrimoClient({
  apiBaseUrl: process.env.PRIMO_API_BASE_URL!,
  siteId: process.env.PRIMO_SITE_ID!,
  environmentKey: process.env.PRIMO_ENVIRONMENT_KEY!,
  retries: 3,              // Number of retries (default: 3)
  retryDelay: 1000,        // Initial delay in ms (default: 1000)
  retryBackoff: 2,         // Backoff multiplier (default: 2)
  timeout: 30000,          // Request timeout (default: 30000ms)
})

// Retry strategy:
// 1st attempt: immediate
// 2nd attempt: 1s delay
// 3rd attempt: 2s delay (1s * 2^1)
// 4th attempt: 4s delay (1s * 2^2)

Logging

const client = new PrimoClient({
  apiBaseUrl: process.env.PRIMO_API_BASE_URL!,
  siteId: process.env.PRIMO_SITE_ID!,
  environmentKey: process.env.PRIMO_ENVIRONMENT_KEY!,
  logger: {
    info: (message, meta) => console.log(message, meta),
    warn: (message, meta) => console.warn(message, meta),
    error: (message, meta) => console.error(message, meta),
  },
})

Migration Guide

Migrating from raw API calls to the SDK:

Before: Raw API

const response = await fetch(
  `${apiUrl}/api/sites/${siteId}/content/by-slug/${page}__${section}`,
  {
    headers: {
      "X-Environment-Key": environmentKey,
      "Accept": "application/json",
    },
  }
)

if (!response.ok) {
  throw new Error(`API request failed: ${response.status}`)
}

const data = await response.json()

After: SDK

import { PrimoClient } from "primo-cms-sdk"

const client = new PrimoClient({
  apiBaseUrl: process.env.PRIMO_API_BASE_URL!,
  siteId: process.env.PRIMO_SITE_ID!,
  environmentKey: process.env.PRIMO_ENVIRONMENT_KEY!,
})

const section = await client.sections.get({ page, section })

Benefits:

  • Type Safety - TypeScript knows the response shape
  • Error Handling - Typed error classes
  • Retry Logic - Automatic retries on failure
  • Caching - Optional built-in caching
  • Validation - Request/response validation

CLI Tool

Generate TypeScript types from your Primo CMS schemas.

Installation

# Install globally
npm install -g @primo/cms-cli

# Or use with npx
npx @primo/cms-cli generate

Configuration

Create .primo.config.json:

{
  "apiBaseUrl": "http://localhost:5080",
  "siteId": "your-site-id",
  "environmentKey": "your-environment-key",
  "outputDir": "./src/lib/types",
  "generateZod": true
}

Commands

Generate Types (One-Time)

# Generate types now
primo generate

# Specify output directory
primo generate --output ./src/types

# Generate Zod schemas
primo generate --zod

Watch for Changes

# Continuously watch for schema changes
primo watch

# Poll every 30 seconds
primo watch --interval 30000

Generated Output

// src/lib/types/sections.ts

/**
 * Hero section
 * Used on landing pages
 */
export interface HeroSection {
  slug: string
  title: string
  subtitle?: string
  cta_text: string
  cta_url: string
  background_image?: string
}

/**
 * Features section
 */
export interface FeaturesSection {
  slug: string
  title: string
  features: Array<{
    title: string
    description: string
    icon?: string
  }>
}

/**
 * Union type of all sections
 */
export type Section = HeroSection | FeaturesSection

// Zod schemas (if --zod flag used)
export const HeroSectionSchema = z.object({
  slug: z.string(),
  title: z.string(),
  subtitle: z.string().optional(),
  cta_text: z.string(),
  cta_url: z.string(),
  background_image: z.string().optional(),
})

Usage

import type { HeroSection } from "./lib/types/sections"

function Hero({ data }: { data: HeroSection }) {
  return (
    <div>
      <h1>{data.title}</h1>
      {data.subtitle && <p>{data.subtitle}</p>}
      <a href={data.cta_url}>{data.cta_text}</a>
    </div>
  )
}

Examples

Marketing Site Example

See examples/marketing-site/ for a complete Next.js 15 implementation:

  • Server Components with ISR
  • React hooks for client components
  • Webhook integration
  • Type-safe section components
  • Dynamic section rendering
cd examples/marketing-site
npm install
npm run dev

Key Files

  • lib/primo-server.ts - Server-side client
  • lib/primo.ts - Client-side client
  • components/sections/ - Typed section components
  • app/api/webhooks/primo/route.ts - Webhook handler
  • app/page.tsx - Homepage with SSR

Reference

Related Documentation

Package Info

  • Package: primo-cms-sdk
  • Version: Check package.json
  • License: MIT
  • Repository: GitHub

Getting Help

  • GitHub Issues - Report bugs or request features
  • Documentation - Check docs/ folder
  • Examples - See examples/ directory

The SDK is production-ready and actively maintained. For questions or contributions, see the main repository README.