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
- Overview
- Installation
- Quick Start
- API Reference
- Authentication
- Advanced Usage
- Migration Guide
- CLI Tool
- 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-sdkQuick 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 fieldssections.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 objectsError 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-keySecurity Best Practices:
- Never commit API keys - Use
.env.local(add to.gitignore) - Use server-side only - Keep environment keys in server components
- Separate keys per environment - Different keys for dev/staging/production
- Rotate regularly - Rotate keys every 90 days
- 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§ion=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 cacheRedis 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 generateConfiguration
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 --zodWatch for Changes
# Continuously watch for schema changes
primo watch
# Poll every 30 seconds
primo watch --interval 30000Generated 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 devKey Files
lib/primo-server.ts- Server-side clientlib/primo.ts- Client-side clientcomponents/sections/- Typed section componentsapp/api/webhooks/primo/route.ts- Webhook handlerapp/page.tsx- Homepage with SSR
Reference
Related Documentation
- Main README - Project overview
- Backend API - API documentation
- docs/API-REFERENCE.md - Complete API reference
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.
