JSPM

  • Created
  • Published
  • Downloads 444
  • Score
    100M100P100Q109236F
  • License MIT

TypeScript SDK for PerspectAPI - Cloudflare Workers compatible

Package Exports

  • perspectapi-ts-sdk

Readme

PerspectAPI TypeScript SDK

A comprehensive TypeScript SDK for PerspectAPI, designed to work seamlessly with Cloudflare Workers and other JavaScript environments.

Features

  • 🚀 Cloudflare Workers Compatible - Uses native fetch API, no Node.js dependencies
  • 🔒 Type Safe - Full TypeScript support with comprehensive type definitions
  • 🔄 Automatic Retries - Built-in retry logic with exponential backoff
  • 🛡️ Error Handling - Structured error responses with detailed information
  • 📦 Modular Design - Use individual clients or the complete SDK
  • 🔑 Multiple Auth Methods - Support for JWT tokens and API keys
  • 📊 Comprehensive Coverage - All PerspectAPI endpoints supported
  • 🧩 High-Level Loaders - Drop-in helpers for products, content, and checkout flows with fallbacks
  • 📧 Newsletter Management - Complete newsletter subscription system with double opt-in, preferences, and lists
  • 👥 Site Users - OTP-based customer accounts with metadata, profiles, orders, and subscriptions

Installation

npm install perspectapi-ts-sdk

Quick Start

import { createPerspectApiClient } from 'perspectapi-ts-sdk';

// Initialize with API key
const client = createPerspectApiClient({
  baseUrl: 'https://your-perspectapi-instance.com',
  apiKey: 'your-api-key'
});

// Or initialize with JWT token
const client = createPerspectApiClient({
  baseUrl: 'https://your-perspectapi-instance.com',
  jwt: 'your-jwt-token'
});

// Use the client
async function example() {
  const siteName = 'your-site-name';

  // Get all content
  const content = await client.content.getContent(siteName);
  console.log(content.data);

  // Create new content
  const newContent = await client.content.createContent({
    page_title: 'Hello World',
    page_content: 'This is my first post!',
    page_status: 'publish',
    page_type: 'post'
  });
  
  console.log(newContent.data);
}

### High-Level Loaders (Quick Example)

```typescript
import {
  loadProducts,
  loadPosts,
  createCheckoutSession
} from 'perspectapi-ts-sdk';

const loaders = {
  client: createPerspectApiClient({ baseUrl, apiKey }),
  siteName: 'my-museum-site'
};

const products = await loadProducts(loaders);
const posts = await loadPosts(loaders);

const checkout = await createCheckoutSession({
  ...loaders,
  items: [{ productId: products[0].id, quantity: 1 }],
  successUrl: 'https://example.com/success',
  cancelUrl: 'https://example.com/cancel'
});

📚 See docs/loaders.md for full walkthroughs, including fallback data, custom logging, and Stripe price resolution.

Configuring Caching

Pass a cache block when you create the client to enable caching. The SDK falls back to an in-memory store in development; in production you can supply any adapter that satisfies the CacheAdapter interface.

import { PerspectApiClient } from 'perspectapi-ts-sdk';
import { myCacheAdapter } from './cache-adapter';

const perspect = new PerspectApiClient({
  baseUrl: 'https://api.perspect.co',
  apiKey: env.PERSPECT_API_KEY,
  cache: {
    adapter: myCacheAdapter,
    defaultTtlSeconds: 300,
    keyPrefix: 'storefront', // optional namespace
  },
});

Cloudflare Workers KV example

KV namespaces make a great globally distributed cache. Bind a namespace (e.g. PERSPECT_CACHE) in your Worker and wrap it with a small adapter:

// worker.ts
import { PerspectApiClient, type CacheAdapter } from 'perspectapi-ts-sdk';

const kvAdapter = (kv: KVNamespace): CacheAdapter => ({
  async get(key) {
    const value = await kv.get(key);
    return value ?? undefined;
  },
  async set(key, value, options) {
    const ttl = options?.ttlSeconds;
    await kv.put(key, value, ttl ? { expirationTtl: ttl } : undefined);
  },
  async delete(key) {
    await kv.delete(key);
  },
  async deleteMany(keys) {
    await Promise.all(keys.map(key => kv.delete(key)));
  },
});

export default {
  async fetch(request: Request, env: Env) {
    const perspect = new PerspectApiClient({
      baseUrl: env.PERSPECT_API_URL,
      apiKey: env.PERSPECT_API_KEY,
      cache: {
        adapter: kvAdapter(env.PERSPECT_CACHE),
        defaultTtlSeconds: 300,
        keyPrefix: 'storefront',
      },
    });

    const products = await perspect.products.getProducts('museum-indian-art');
    return new Response(JSON.stringify(products.data), {
      headers: { 'Content-Type': 'application/json' },
    });
  },
};

When PerspectAPI sends a webhook—or when your Worker mutates data directly—call perspect.cache.invalidate({ tags: [...] }) using the tags emitted by the SDK (products:site:<site>, content:slug:<site>:<slug>, content:category:<site>:<category_slug>, etc.) so stale entries are purged immediately.

Webhook-driven cache invalidation

Most deployments rely on PerspectAPI webhooks to bust cache entries whenever content changes. Your app needs an HTTP endpoint that:

  1. Verifies the webhook signature (or shared secret).
  2. Maps the webhook payload to the cache tags that were used when reading data.
  3. Calls client.cache.invalidate({ tags }).

Below is a minimal Cloudflare Worker handler; the same flow works in Express, Fastify, etc.—just swap the request parsing.

// worker.ts
import { PerspectApiClient } from 'perspectapi-ts-sdk';

const kvAdapter = (kv: KVNamespace) => ({
  get: (key: string) => kv.get(key),
  set: (key: string, value: string, options?: { ttlSeconds?: number }) =>
    options?.ttlSeconds
      ? kv.put(key, value, { expirationTtl: options.ttlSeconds })
      : kv.put(key, value),
  delete: (key: string) => kv.delete(key),
});

const perspect = new PerspectApiClient({
  baseUrl: env.PERSPECT_API_URL,
  apiKey: env.PERSPECT_API_KEY,
  cache: { adapter: kvAdapter(env.PERSPECT_CACHE), defaultTtlSeconds: 300 },
});

type WebhookEvent =
  | { type: 'product.updated'; site: string; slug?: string; id?: number }
  | { type: 'content.published'; site: string; slug: string; id: number }
  | { type: 'category.updated'; site: string; slug: string };

const tagMap: Record<string, (e: WebhookEvent) => string[]> = {
  'product.updated': event => [
    `products`,
    `products:site:${event.site}`,
    event.slug ? `products:slug:${event.site}:${event.slug}` : '',
    event.id ? `products🆔${event.id}` : '',
  ],
  'content.published': event => [
    `content`,
    `content:site:${event.site}`,
    `content:slug:${event.site}:${event.slug}`,
    `content🆔${event.id}`,
  ],
  'category.updated': event => [
    `categories`,
    `categories:site:${event.site}`,
    `categories:product:${event.site}:${event.slug}`,
    `products:category:${event.site}:${event.slug}`,
    `content:category:${event.site}:${event.slug}`,
  ],
};

export default {
  async fetch(request: Request, env: Env) {
    const url = new URL(request.url);

    if (url.pathname === '/webhooks/perspect' && request.method === 'POST') {
      const event = (await request.json()) as WebhookEvent;

      // TODO: validate shared secret / signature before proceeding

      const buildTags = tagMap[event.type];
      if (buildTags) {
        const tags = buildTags(event).filter(Boolean);
        if (tags.length) {
          await perspect.cache.invalidate({ tags });
        }
      }

      return new Response(null, { status: 202 });
    }

    // ...rest of your app
    return new Response('ok');
  },
};

🔁 Adjust the WebhookEvent union and tagMap to match the actual payloads you receive. PerspectAPI webhooks also carry version IDs and environment metadata that you can use for more granular targeting if needed.


## Image Transformations

The SDK includes utilities for transforming images using Cloudflare's Image Resizing service:

```typescript
import { transformMediaItem, buildImageUrl } from 'perspectapi-ts-sdk';

// Get a product with media
const product = await client.products.getProduct('mysite', 123);
const media = product.data.media?.[0];

if (media) {
  // Generate all responsive sizes automatically
  const urls = transformMediaItem('https://api.perspect.co', media);
  
  console.log(urls.thumbnail);  // 150x150 cover crop
  console.log(urls.small);      // 400px wide
  console.log(urls.medium);     // 800px wide
  console.log(urls.large);      // 1200px wide
  console.log(urls.original);   // Original with auto format
}

// Or build custom transformations
const customUrl = buildImageUrl(
  'https://api.perspect.co',
  'media/mysite/photo.jpg',
  {
    width: 400,
    height: 300,
    fit: 'cover',
    format: 'webp',
    quality: 85
  }
);

Features:

  • ✅ On-the-fly image resizing (no pre-generated thumbnails)
  • ✅ Automatic format optimization (WebP/AVIF)
  • ✅ Responsive image support with srcset
  • ✅ CDN caching at the edge
  • ✅ Bandwidth savings

📚 See docs/image-transforms.md for complete documentation, examples, and best practices.

High-Level Data Loaders

The SDK now ships with convenience loaders that wrap the lower-level REST clients. They help you:

  • normalise products (including nested media arrays and Stripe IDs)
  • search products server-side with limit/offset support and filter by category names or IDs (no hard-coded IDs)
  • fetch published posts/pages with sensible defaults
  • fall back to sample data when PerspectAPI is not configured (useful for local previews)
  • create Stripe checkout sessions by automatically resolving price IDs

All loaders accept a shared LoaderOptions object:

import {
  loadProducts,
  loadProductBySlug,
  loadPosts,
  createCheckoutSession,
  type LoaderOptions
} from 'perspectapi-ts-sdk';

const options: LoaderOptions = {
  client: createPerspectApiClient({ baseUrl, apiKey }),
  siteName: 'museum-indian-art',
  logger: console,              // optional
  fallbackProducts: [],         // optional
  fallbackPosts: []             // optional
};

const products = await loadProducts(options);
const single = await loadProductBySlug({ ...options, slug: 'sunset-painting' });
const posts = await loadPosts(options);

await createCheckoutSession({
  ...options,
  items: [{ productId: products[0].id, quantity: 1 }],
  successUrl: 'https://example.com/success',
  cancelUrl: 'https://example.com/cancel'
});

// Filter products by multiple category names/IDs while combining search
const decorPillows = await loadProducts({
  ...options,
  category: ['Home Decor', 'Seasonal Sale'],
  categoryIds: [12],
  search: 'pillow',
  offset: 24,
  limit: 12
});

More real-world scenarios—including Cloudflare Workers, Remix/Next.js loaders, custom logger implementations, and checkout price resolution—are documented in docs/loaders.md.

Authentication

API Key Authentication

const client = createPerspectApiClient({
  baseUrl: 'https://api.example.com',
  apiKey: 'pk_your_api_key_here'
});

JWT Token Authentication

const client = createPerspectApiClient({
  baseUrl: 'https://api.example.com',
  jwt: 'your.jwt.token'
});

// Or authenticate after initialization
await client.auth.signIn({
  email: 'user@example.com',
  password: 'password'
});

Dynamic Authentication

const client = createPerspectApiClient({
  baseUrl: 'https://api.example.com'
});

// Set authentication later
client.setApiKey('pk_your_api_key');
// or
client.setAuth('your.jwt.token');

// Clear authentication
client.clearAuth();

API Reference

Content Management

// Get all content with pagination
const content = await client.content.getContent('your-site-name', {
  page: 1,
  limit: 20,
  page_status: 'publish',
  page_type: 'post',
  slug_prefix: 'blog'  // Optional: filter by slug prefix
});

// Get content by ID
const post = await client.content.getContentById(123);

// Get content by slug
const page = await client.content.getContentBySlug('your-site-name', 'about-us');

// Get content by category slug
const categoryContent = await client.content.getContentByCategorySlug(
  'your-site-name',
  'news',
  {
    page: 1,
    limit: 20,
    page_type: 'post'
  }
);

// Create new content
const newPost = await client.content.createContent({
  page_title: 'My New Post',
  page_content: '<p>Content goes here</p>',
  content_markdown: '# My New Post\n\nContent goes here',
  page_status: 'draft',
  page_type: 'post',
  slug: 'my-new-post'
});

// Update content
const updated = await client.content.updateContent(123, {
  page_title: 'Updated Title',
  page_status: 'publish'
});

// Delete content
await client.content.deleteContent(123);

// Publish/unpublish content
await client.content.publishContent(123);
await client.content.unpublishContent(123);

Products & E-commerce

// Get all products
const products = await client.products.getProducts('my-site', {
  page: 1,
  limit: 20,
  isActive: true,
  slug_prefix: 'shop'  // Optional: filter by slug prefix
});

// Create new product
const product = await client.products.createProduct({
  name: 'Amazing Product',
  description: 'Product description',
  price: 29.99,
  currency: 'USD',
  sku: 'PROD-001'
});

// Update product inventory
await client.products.updateProductInventory(123, {
  quantity: 10,
  operation: 'add',
  reason: 'Restocked'
});

// Get product by slug and site name (NEW!)
const productBySlug = await client.products.getProductBySlug('my-site', 'awesome-laptop');
console.log('Product:', productBySlug.data?.name);
console.log('Variants:', productBySlug.data?.variants);

// Get products by category slug (NEW!)
const categoryProducts = await client.products.getProductsByCategorySlug(
  'my-site',
  'electronics',
  {
    page: 1,
    limit: 20,
    published: true,
    search: 'phone'
  }
);
console.log('Products:', categoryProducts.data?.data);
console.log('Category info:', categoryProducts.data?.category);

Organizations & Sites

// Get organizations
const orgs = await client.organizations.getOrganizations();

// Create new organization
const org = await client.organizations.createOrganization({
  name: 'My Company',
  description: 'Company description'
});

// Get sites
const sites = await client.sites.getSites({
  organizationId: 1
});

// Create new site
const site = await client.sites.createSite({
  name: 'My Website',
  domain: 'example.com',
  organizationId: 1
});

API Key Management

// Get all API keys
const apiKeys = await client.apiKeys.getApiKeys();

// Create new API key
const newKey = await client.apiKeys.createApiKey({
  name: 'Frontend App Key',
  description: 'API key for frontend application',
  permissions: ['content:read', 'products:read'],
  expiresAt: '2024-12-31T23:59:59Z'
});

console.log('New API key:', newKey.data.key);

// Test API key validity
const testResult = await client.apiKeys.testApiKey('pk_test_key');
console.log('Key valid:', testResult.data.valid);

Webhooks

// Get all webhooks
const webhooks = await client.webhooks.getWebhooks();

// Create new webhook
const webhook = await client.webhooks.createWebhook({
  name: 'Order Notifications',
  url: 'https://myapp.com/webhooks/orders',
  provider: 'stripe',
  events: ['payment_intent.succeeded', 'payment_intent.payment_failed'],
  isActive: true
});

// Test webhook
const testResult = await client.webhooks.testWebhook(webhook.data.id);
console.log('Webhook test:', testResult.data);

Checkout & Payments

// Create Stripe checkout session
const session = await client.checkout.createCheckoutSession({
  priceId: 'price_1234567890',
  successUrl: 'https://myapp.com/success',
  cancelUrl: 'https://myapp.com/cancel',
  customerEmail: 'customer@example.com',
  currency: 'usd',
  shipping_amount: 500,
  shipping_address: {
    country: 'US',
    state: 'CA',
    postal_code: '94110'
  },
  billing_address: {
    country: 'US',
    state: 'CA',
    postal_code: '94110'
  },
  tax: {
    strategy: 'manual_rates',
    customer_identifier: 'acct-123',
    customer_display_name: 'Acme Corp',
    customer_exemption: {
      status: 'exempt',
      tax_id: '99-1234567',
      tax_id_type: 'ein'
    }
  }
});

// Redirect user to checkout
window.location.href = session.data.url;

// Get checkout session status
const sessionStatus = await client.checkout.getCheckoutSession('cs_test_123');
console.log(sessionStatus.data.tax?.amount); // Display assessed tax (if calculated)

Contact Forms

const siteName = 'your-site-name';

// Submit contact form
const submission = await client.contact.submitContact(siteName, {
  name: 'John Doe',
  email: 'john@example.com',
  subject: 'Question about pricing',
  message: 'I have a question about your pricing plans.',
  turnstileToken: 'turnstile-response-token'
});

// Get contact submissions (admin only)
const submissions = await client.contact.getContactSubmissions(siteName, {
  status: 'unread',
  page: 1,
  limit: 50
});

// Update submission status
await client.contact.updateContactStatus(siteName, submission.data.id, 'read');

Newsletter Subscriptions

const siteName = 'your-site-name';

// Subscribe to newsletter (with double opt-in)
const subscription = await client.newsletter.subscribe(siteName, {
  email: 'subscriber@example.com',
  name: 'Jane Doe',
  list_ids: ['list_default'],  // Optional: subscribe to specific lists
  frequency: 'weekly',          // instant, daily, weekly, monthly
  topics: ['news', 'updates'],  // Optional: topic preferences
  double_opt_in: true,          // Default: true for GDPR compliance
  turnstile_token: 'token'      // Optional: Turnstile verification
});

// Confirm subscription via email token
const confirmed = await client.newsletter.confirmSubscription(
  siteName,
  'confirmation-token-from-email'
);

// Unsubscribe
const unsubscribed = await client.newsletter.unsubscribe(siteName, {
  email: 'subscriber@example.com',
  reason: 'Too many emails'  // Optional feedback
});

// One-click unsubscribe (from email link)
await client.newsletter.unsubscribeByToken(siteName, 'unsubscribe-token');

// Update preferences
await client.newsletter.updatePreferences(siteName, 'subscriber@example.com', {
  frequency: 'monthly',
  topics: ['product-updates'],
  email_format: 'html',  // html, text, or both
  timezone: 'America/New_York',
  track_opens: false,
  track_clicks: false
});

// Check subscription status
const status = await client.newsletter.getStatus(siteName, 'subscriber@example.com');
console.log('Subscribed:', status.data.subscribed);
console.log('Status:', status.data.status); // pending, confirmed, unsubscribed

// Get available newsletter lists
const lists = await client.newsletter.getLists(siteName);
console.log('Available lists:', lists.data.lists);

Newsletter Admin Functions

// Get all subscriptions (admin only)
const subscriptions = await client.newsletter.getSubscriptions(siteName, {
  page: 1,
  limit: 100,
  status: 'confirmed',
  list_id: 'list_weekly',
  search: 'john',
  startDate: '2024-01-01',
  endDate: '2024-12-31'
});

// Update subscription status
await client.newsletter.updateSubscriptionStatus(
  siteName,
  'sub_123',
  'unsubscribed',
  'Admin action: User requested via support'
);

// Bulk operations
await client.newsletter.bulkUpdateSubscriptions(siteName, {
  ids: ['sub_123', 'sub_456'],
  action: 'add_to_list',
  list_id: 'list_special_offers'
});

// List management
const newList = await client.newsletter.createList(siteName, {
  list_name: 'VIP Customers',
  slug: 'vip-customers',
  description: 'Exclusive updates for VIP customers',
  is_public: false,
  is_default: false,
  double_opt_in: true,
  welcome_email_enabled: true
});

await client.newsletter.updateList(siteName, 'list_123', {
  description: 'Updated description',
  is_public: true
});

// Get statistics
const stats = await client.newsletter.getStatistics(siteName, {
  startDate: '2024-01-01',
  endDate: '2024-12-31',
  list_id: 'list_weekly'
});
console.log('Total subscribers:', stats.data.totalSubscribers);
console.log('Open rate:', stats.data.engagementMetrics.averageOpenRate);

// Export subscriptions
const exportData = await client.newsletter.exportSubscriptions(siteName, {
  format: 'csv',  // csv, json, or xlsx
  status: 'confirmed',
  list_id: 'list_weekly'
});
console.log('Download URL:', exportData.data.downloadUrl);

// Import subscriptions
const importResult = await client.newsletter.importSubscriptions(siteName, {
  subscriptions: [
    { email: 'user1@example.com', name: 'User One', lists: ['list_weekly'] },
    { email: 'user2@example.com', name: 'User Two', lists: ['list_daily'] }
  ],
  skip_confirmation: false,  // Skip double opt-in for imported users
  update_existing: true       // Update if email already exists
});
console.log('Imported:', importResult.data.imported);
console.log('Failed:', importResult.data.failed);

📚 For complete newsletter documentation, see docs/newsletter.md

Site Users (Customer Accounts)

Site users are per-site customer accounts with OTP-based authentication, separate from admin users.

const siteName = 'your-site-name';

// Request OTP for login/signup (with optional metadata)
await client.siteUsers.requestOtp(
  siteName,
  {
    email: 'user@example.com',
    waitlist: true,  // Optional: mark as waitlist signup
    metadata: {      // Optional: set metadata at signup time
      signupSource: 'landing-page',
      referralCode: 'FRIEND123',
      interests: ['product-updates']
    }
  },
  csrfToken
);

// Verify OTP and get JWT token
const response = await client.siteUsers.verifyOtp(
  siteName,
  { email: 'user@example.com', code: '123456' },
  csrfToken
);

const { token, user } = response.data;
client.setAuth(token);  // Set auth for subsequent requests

// Get current user profile
const { data } = await client.siteUsers.getMe(siteName);
console.log(data.user);      // Basic user fields + metadata
console.log(data.profile);   // Key-value profile data

// Update user profile and metadata
await client.siteUsers.updateMe(
  siteName,
  {
    first_name: 'John',
    last_name: 'Doe',
    metadata: {
      preferences: { theme: 'dark' },
      tags: ['premium'],
      customField: 'value'
    }
  },
  csrfToken
);

// Set profile key-values (phone, addresses, etc.)
await client.siteUsers.setProfileValue(
  siteName,
  'phone',
  '+1-555-0123',
  csrfToken
);

await client.siteUsers.setProfileValue(
  siteName,
  'address_shipping',
  JSON.stringify({
    line1: '123 Main St',
    city: 'San Francisco',
    state: 'CA',
    postal_code: '94102',
    country: 'US'
  }),
  csrfToken
);

// Get order history
const orders = await client.siteUsers.getOrders(siteName, {
  limit: 50,
  offset: 0
});

// Get subscriptions
const subscriptions = await client.siteUsers.getSubscriptions(siteName);

// Cancel subscription
await client.siteUsers.cancelSubscription(siteName, 'sub_123', csrfToken);

// Logout
await client.siteUsers.logout(siteName);
client.setAuth(null);

📚 For complete site users documentation including waitlist management, metadata patterns, cross-domain authentication, and complete examples, see docs/site-users.md

Configuration Options

interface PerspectApiConfig {
  baseUrl: string;           // Required: API base URL
  apiKey?: string;          // Optional: API key for authentication
  jwt?: string;             // Optional: JWT token for authentication
  timeout?: number;         // Optional: Request timeout in ms (default: 30000)
  retries?: number;         // Optional: Number of retries (default: 3)
  headers?: Record<string, string>; // Optional: Additional headers
}

Error Handling

The SDK provides structured error handling:

import { createApiError } from 'perspectapi-ts-sdk';

try {
  const content = await client.content.getContentById(999);
} catch (error) {
  const apiError = createApiError(error);
  
  console.log('Error message:', apiError.message);
  console.log('Status code:', apiError.status);
  console.log('Error code:', apiError.code);
  console.log('Details:', apiError.details);
}

Cloudflare Workers Usage

The SDK is fully compatible with Cloudflare Workers:

// worker.ts
import { createPerspectApiClient } from 'perspectapi-ts-sdk';

export default {
  async fetch(request: Request, env: any): Promise<Response> {
    const client = createPerspectApiClient({
      baseUrl: env.PERSPECT_API_URL,
      apiKey: env.PERSPECT_API_KEY
    });

    try {
      const content = await client.content.getContent(env.PERSPECT_SITE_NAME, { limit: 10 });
      
      return new Response(JSON.stringify(content.data), {
        headers: { 'Content-Type': 'application/json' }
      });
    } catch (error) {
      return new Response('Error fetching content', { status: 500 });
    }
  }
};

Advanced Usage

Using Individual Clients

import { HttpClient, ContentClient } from 'perspectapi-ts-sdk';

// Create HTTP client
const http = new HttpClient({
  baseUrl: 'https://api.example.com',
  apiKey: 'your-api-key'
});

// Use individual client
const contentClient = new ContentClient(http);
const content = await contentClient.getContent('your-site-name');

Custom Headers

const client = createPerspectApiClient({
  baseUrl: 'https://api.example.com',
  apiKey: 'your-api-key',
  headers: {
    'X-Custom-Header': 'custom-value',
    'User-Agent': 'MyApp/1.0.0'
  }
});

Timeout and Retries

const client = createPerspectApiClient({
  baseUrl: 'https://api.example.com',
  apiKey: 'your-api-key',
  timeout: 60000,  // 60 seconds
  retries: 5       // Retry up to 5 times
});

Slug Prefix Filtering

The SDK supports filtering content and products by slug_prefix to organize your content by URL structure:

// Filter blog posts
const blogPosts = await client.content.getContent('mysite', {
  slug_prefix: 'blog',      // /blog/post-1, /blog/post-2
  page_status: 'publish'
});

// Filter news articles
const news = await client.content.getContent('mysite', {
  slug_prefix: 'news',      // /news/article-1, /news/article-2
  page_status: 'publish'
});

// Filter shop products
const shopProducts = await client.products.getProducts('mysite', {
  slug_prefix: 'shop',      // /shop/product-1, /shop/product-2
  isActive: true
});

// Filter artwork products
const artwork = await client.products.getProducts('mysite', {
  slug_prefix: 'artwork',   // /artwork/painting-1, /artwork/sculpture-1
  isActive: true
});

// Combine with other filters
const results = await client.content.getContent('mysite', {
  slug_prefix: 'blog',
  page_status: 'publish',
  page_type: 'post',
  search: 'javascript',     // Search within blog posts only
  page: 1,
  limit: 20
});

Use Cases:

  • Organize blog posts, news, and documentation separately
  • Separate shop products from artwork or digital downloads
  • Create multi-section websites with distinct URL structures
  • Filter content/products by section for navigation

See examples/slug-prefix-examples.ts for 20+ real-world examples!

TypeScript Support

The SDK is written in TypeScript and provides comprehensive type definitions:

import type { 
  Content, 
  Product, 
  ApiResponse, 
  PaginatedResponse 
} from 'perspectapi-ts-sdk';

// Fully typed responses
const content: ApiResponse<Content> = await client.content.getContentById(123);
const products: PaginatedResponse<Product> = await client.products.getProducts('your-site-name');

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Support