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

@multocms/connector

v1.5.1

Published

Official JavaScript/TypeScript client library for MultoCMS REST API with automatic authentication and token management

Readme

@multocms/connector

Official JavaScript/TypeScript client library for MultoCMS REST API.

This package provides a type-safe, easy-to-use interface for connecting to MultoCMS APIs with automatic authentication, token management, and request/response handling.

Features

  • Automatic Authentication - Handles API key exchange for JWT tokens
  • Token Management - Automatic token refresh on expiration
  • TypeScript Support - Full type definitions for all endpoints
  • Simple API - Clean, intuitive method names
  • Error Handling - Consistent error responses
  • Environment Variables - Works seamlessly with .env files

Installation

npm install @multocms/connector

Quick Start

1. Set Environment Variables

Create a .env file or set environment variables:

MULTOCMS_BASE_URL=https://your-site.com/api/v1
MULTOCMS_API_KEY=your-key-id.your-secret
MULTOCMS_API_SECRET=your-secret

# Optional: Cache duration in seconds (default: 0 = no cache)
MULTOCMS_CACHE_REVALIDATE=60

Or you can pass them directly to the constructor:

import { MultoCMSClient } from '@multocms/connector';

const client = new MultoCMSClient({
  baseUrl: 'https://your-site.com/api/v1',
  apiKey: 'your-key-id.your-secret',
  apiSecret: 'your-secret',
  cacheRevalidate: 0, // 0 = no cache (default), 60 = cache 60s, false = use Next.js default
});

2. Use the Client

import { MultoCMSClient } from '@multocms/connector';

// Client auto-authenticates with API key if provided
const client = new MultoCMSClient({
  baseUrl: process.env.MULTOCMS_BASE_URL!, // e.g., 'https://your-site.com/api/v1'
  // API key/secret from environment or passed directly
});

// Get the current site (associated with your API key)
// Client will auto-authenticate if API key is provided
const siteResponse = await client.getCurrentSite();

if (siteResponse.success && siteResponse.data) {
  const site = siteResponse.data;
  console.log('Site:', site.display_name);
  console.log('Tagline:', site.tagline);
  
  // Hero banner will be null if not set
  if (site.hero_banner) {
    console.log('Hero banner:', site.hero_banner.url);
    console.log('Sizes available:', Object.keys(site.hero_banner.sizes || {}));
  }
} else {
  console.error('Error getting site:', siteResponse.error);
}

// Get all posts
const response = await client.getPosts();
if (response.success) {
  console.log('Posts:', response.data);
} else {
  console.error('Error:', response.error);
}

// Get a single post by slug
const postResponse = await client.getPost('hello-world', {
  post_type: 'post',
  include: 'author,categories,featured_image',
});

if (postResponse.success) {
  const post = postResponse.data;
  console.log('Post title:', post.title);
  console.log('Featured image:', post.featured_image?.url);
}

Authentication

The client supports two authentication methods:

1. API Key (Recommended for Server-to-Server)

const client = new MultoCMSClient({
  baseUrl: 'https://your-site.com/api/v1',
  apiKey: process.env.MULTOCMS_API_KEY!,
  apiSecret: process.env.MULTOCMS_API_SECRET!,
});

// Client automatically exchanges API key for JWT tokens
await client.authenticate();

2. Manual JWT Token

If you already have tokens:

const client = new MultoCMSClient({
  baseUrl: 'https://your-site.com/api/v1',
  accessToken: 'your-access-token',
  refreshToken: 'your-refresh-token',
});

Token Refresh

The client automatically refreshes tokens when they expire. You can disable this:

const client = new MultoCMSClient({
  baseUrl: 'https://your-site.com/api/v1',
  apiKey: process.env.MULTOCMS_API_KEY!,
  apiSecret: process.env.MULTOCMS_API_SECRET!,
  autoRefresh: false, // Disable automatic refresh
});

Or manually refresh:

await client.refreshAccessToken();

API Methods

Sites

// Get all sites (includes settings for each site)
const sitesResponse = await client.getSites();
if (sitesResponse.success && sitesResponse.data) {
  sitesResponse.data.forEach(site => {
    console.log('Site:', site.display_name);
    console.log('Settings:', site.settings);
    // Each site includes all non-authentication settings
    console.log('Posts per page:', site.settings.posts_per_page);
  });
}

// Get a single site by ID (numeric or MongoDB ObjectId)
// This also includes all settings
const siteResponse = await client.getSite(2); // or client.getSite('507f1f77bcf86cd799439011');
if (siteResponse.success && siteResponse.data) {
  const site = siteResponse.data;
  console.log('Site name:', site.display_name);
  console.log('Settings:', site.settings);
  // Access specific settings
  console.log('Site title:', site.settings.site_title);
  console.log('Image sizes:', site.settings.image_sizes);
}

// Get the current site (site associated with the API key)
// This is especially useful when using API key authentication
// IMPORTANT: Make sure you've authenticated first!
await client.authenticate(); // Required if using API key

const currentSite = await client.getCurrentSite();
// Or use the shorthand:
const currentSite2 = await client.getSite('current'); // or 'me'

// Check if request was successful
if (currentSite.success && currentSite.data) {
  const site = currentSite.data;
  console.log('Site name:', site.display_name);
  console.log('Site tagline:', site.tagline);
  
  // Access hero banner information (will be null if not set)
  if (site.hero_banner) {
    const heroBanner = site.hero_banner;
    console.log('Hero banner URL:', heroBanner.url);
    console.log('Thumbnail:', heroBanner.sizes?.thumbnail?.url);
    console.log('Medium size:', heroBanner.sizes?.medium?.url);
    console.log('Large size:', heroBanner.sizes?.large?.url);
    
    // Access custom sizes (like "hero")
    if (heroBanner.sizes?.hero) {
      console.log('Hero size:', heroBanner.sizes.hero.url);
      console.log('Hero dimensions:', heroBanner.sizes.hero.width, 'x', heroBanner.sizes.hero.height);
    }
    
    // You can also access any custom size by name
    const customSize = heroBanner.sizes?.['your-custom-size-name'];
    if (customSize) {
      console.log('Custom size URL:', customSize.url);
    }
  } else {
    console.log('No hero banner set for this site');
  }

  // Access site logo (will be null if not set)
  if (site.logo) {
    console.log('Site logo URL:', site.logo.url);
    console.log('Logo alt text:', site.logo.alt_text);
  }

  // Access site favicon (will be null if not set)
  if (site.favicon) {
    console.log('Favicon URL:', site.favicon.url);
  }

  // Access site settings (all settings except authentication)
  console.log('Site settings:', site.settings);
  console.log('Site name:', site.settings.site_title);
  console.log('Posts per page:', site.settings.posts_per_page);
  console.log('Image sizes:', site.settings.image_sizes);
} else {
  console.error('Error:', currentSite.error);
}

Posts

Include Options: The include parameter accepts comma-separated values:

  • author - Include author details
  • categories - Include category terms
  • tags - Include tag terms
  • featured_image - Include featured image details
  • meta_box_fields - Include metabox fields (custom fields from metaboxes)
  • content - Include full post content (for list endpoints)
// List posts with filters
const posts = await client.getPosts({
  status: 'published',
  post_type: 'post',
  page: 1,
  per_page: 20,
  search: 'keyword',
  include: 'author,categories,meta_box_fields',
});

// Access metabox fields (custom fields from metaboxes)
if (posts.success && posts.data) {
  posts.data.forEach(post => {
    if (post.meta_box_fields) {
      // Access metabox fields
      console.log('Event schedule:', post.meta_box_fields.event_schedule);
      console.log('Phone:', post.meta_box_fields.phone);
      console.log('Email:', post.meta_box_fields.email);
      
      // Event schedule example (array of date/time entries)
      if (post.meta_box_fields.event_schedule) {
        post.meta_box_fields.event_schedule.forEach((entry: any) => {
          console.log(`Date: ${entry.date}, Time: ${entry.start_time} - ${entry.end_time}`);
        });
      }
      
      // Post reference example (single post ID or array of post IDs)
      if (post.meta_box_fields.related_posts) {
        // Can be a single post ID (string) or array of post IDs
        const relatedPostIds = Array.isArray(post.meta_box_fields.related_posts) 
          ? post.meta_box_fields.related_posts 
          : [post.meta_box_fields.related_posts];
        console.log('Related post IDs:', relatedPostIds);
      }
    }
  });
}

// Get posts from this month (published this month)
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);

const thisMonthsPosts = await client.getPosts({
  status: 'published',
  date_from: startOfMonth.toISOString(),
  date_to: endOfMonth.toISOString(),
  date_field: 'published_at', // Optional, defaults to 'published_at'
});

// Get posts published in a specific date range
const dateRangePosts = await client.getPosts({
  status: 'published',
  date_from: '2025-01-01T00:00:00Z',
  date_to: '2025-01-31T23:59:59Z',
  date_field: 'published_at',
});

// Filter by creation date instead
const createdThisMonth = await client.getPosts({
  date_from: startOfMonth.toISOString(),
  date_to: endOfMonth.toISOString(),
  date_field: 'created_at',
});

// Filter posts by term (taxonomy term)
// Get posts with a specific term by ID
const postsByTerm = await client.getPosts({
  term_id: '507f1f77bcf86cd799439011',
  status: 'published',
});

// Get posts with a specific term by slug
const postsByTermSlug = await client.getPosts({
  term_slug: 'technology',
  status: 'published',
});

// Get posts with multiple terms (posts that have ANY of the terms)
const postsByMultipleTerms = await client.getPosts({
  term_ids: '507f1f77bcf86cd799439011,507f1f77bcf86cd799439012',
  status: 'published',
});

// Filter by term and restrict to a specific taxonomy
const categoryPosts = await client.getPosts({
  term_slug: 'technology',
  taxonomy: 'category',
  status: 'published',
});

// Featured image is always included if present (no need to request it)
if (posts.success && posts.data) {
  posts.data.forEach(post => {
    if (post.featured_image) {
      console.log('Featured image URL:', post.featured_image.url);
      console.log('Alt text:', post.featured_image.alt_text);
      console.log('MIME type:', post.featured_image.mime_type);
      
      // Access different sizes
      if (post.featured_image.sizes) {
        console.log('Thumbnail:', post.featured_image.sizes.thumbnail?.url);
        console.log('Medium:', post.featured_image.sizes.medium?.url);
        console.log('Large:', post.featured_image.sizes.large?.url);
        
        // Access custom sizes
        const customSize = post.featured_image.sizes['your-custom-size'];
        if (customSize) {
          console.log('Custom size:', customSize.url);
        }
      }
    }
  });
}

// Get single post by ID or slug
const post = await client.getPost('hello-world', {
  post_type: 'post',
  include: 'author,categories,tags,meta_box_fields',
});

// Access metabox fields
if (post.success && post.data?.meta_box_fields) {
  const metaFields = post.data.meta_box_fields;
  console.log('Event schedule:', metaFields.event_schedule);
  console.log('Phone:', metaFields.phone);
  
  // Post reference fields (can be single ID or array of IDs)
  if (metaFields.related_posts) {
    const relatedPostIds = Array.isArray(metaFields.related_posts) 
      ? metaFields.related_posts 
      : [metaFields.related_posts];
    console.log('Related post IDs:', relatedPostIds);
  }
  
  // ... other metabox fields
}

// Featured image is always included
if (post.success && post.data?.featured_image) {
  const featuredImage = post.data.featured_image;
  console.log('Featured image:', featuredImage.url);
  console.log('Dimensions:', featuredImage.width, 'x', featuredImage.height);
}

// Create a post
const newPost = await client.createPost({
  title: 'My New Post',
  content: '<p>Post content</p>',
  excerpt: 'Post excerpt',
  status: 'published',
  post_type: 'post',
});

// Update a post (full)
await client.updatePost(postId, {
  title: 'Updated Title',
  content: '<p>Updated content</p>',
});

// Partially update a post
await client.patchPost(postId, {
  status: 'published',
});

// Delete a post
await client.deletePost(postId);

// Get post meta (custom fields)
const metaResponse = await client.getPostMeta(postId);
if (metaResponse.success) {
  metaResponse.data?.forEach(meta => {
    console.log(`${meta.meta_key}: ${meta.meta_value}`);
  });
}

// Update post meta (batch update with object)
await client.updatePostMeta(postId, {
  '_mb_phone': '(123) 456-7890',
  '_mb_email': '[email protected]',
});

// Update post meta (batch update with array)
await client.updatePostMeta(postId, [
  { meta_key: '_mb_phone', meta_value: '(123) 456-7890' },
  { meta_key: '_mb_email', meta_value: '[email protected]' },
]);

// Update a single post meta value
await client.updatePostMetaValue(postId, '_mb_phone', '(123) 456-7890');

Media

// List media files
const media = await client.getMedia({
  page: 1,
  per_page: 20,
  search: 'image',
  mime_type: 'image/jpeg',
});

// Get single media file
const mediaItem = await client.getMediaItem(mediaId);

// Upload a media file
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
const file = fileInput.files?.[0];

if (file) {
  const uploadResponse = await client.uploadMedia(file, {
    alt_text: 'A beautiful image',
    caption: 'Image caption',
    folder_id: '507f1f77bcf86cd799439011', // Optional: upload to specific folder
  });

  if (uploadResponse.success && uploadResponse.data) {
    console.log('Uploaded media ID:', uploadResponse.data.id);
    console.log('File URL:', uploadResponse.data.filepath);
    console.log('Image sizes:', uploadResponse.data.sizes);
  } else {
    console.error('Upload failed:', uploadResponse.error);
  }
}

// Upload with File object (Node.js)
import fs from 'fs';
const fileBuffer = fs.readFileSync('./image.jpg');
const blob = new Blob([fileBuffer], { type: 'image/jpeg' });
const file = new File([blob], 'image.jpg', { type: 'image/jpeg' });

const uploadResponse = await client.uploadMedia(file, {
  alt_text: 'My image',
});

// Upload to a specific folder by name
const folderResponse = await client.getFolderByName('Images');
if (folderResponse.success && folderResponse.data) {
  const uploadResponse = await client.uploadMedia(file, {
    alt_text: 'My image',
    folder_id: folderResponse.data.id,
  });
}

Media Folders

// List all folders
const folders = await client.getFolders();

// List folders in a specific parent folder
const subfolders = await client.getFolders({
  parent_id: '507f1f77bcf86cd799439011',
});

// Search folders by name
const searchResults = await client.getFolders({
  search: 'images',
});

// Get folder by exact name (convenience method)
const folderResponse = await client.getFolderByName('Images');
if (folderResponse.success && folderResponse.data) {
  console.log('Folder ID:', folderResponse.data.id);
  console.log('File count:', folderResponse.data.file_count);
  console.log('Subfolder count:', folderResponse.data.subfolder_count);
}

// Get folder by name in a specific parent
const subfolderResponse = await client.getFolderByName('2025', '507f1f77bcf86cd799439011');

Menus

// List all menus
const menus = await client.getMenus();

// Get menu by ID
const menu = await client.getMenu(menuId);

// Get menu by location slug
const mainMenu = await client.getMenuByLocation('primary');

Taxonomies & Terms

// List all taxonomies
const taxonomies = await client.getTaxonomies();

// Get single taxonomy
const categoryTax = await client.getTaxonomy('category');

// List terms for a taxonomy
const categories = await client.getTerms('category', {
  page: 1,
  per_page: 50,
});

// Get single term
const category = await client.getTerm('category', termId);

// Create a term
const newCategory = await client.createTerm('category', {
  name: 'News',
  slug: 'news',
  description: 'News category',
});

// Update a term
await client.updateTerm('category', termId, {
  name: 'Updated Category',
});

// Delete a term
await client.deleteTerm('category', termId);

Users

// List users (requires manage_users permission)
const users = await client.getUsers({
  page: 1,
  per_page: 20,
  search: 'john',
});

// Get current authenticated user
const currentUser = await client.getCurrentUser();

// Get single user
const user = await client.getUser(userId);

// Create user
const newUser = await client.createUser({
  username: 'johndoe',
  email: '[email protected]',
  password: 'secure-password',
  first_name: 'John',
  last_name: 'Doe',
});

// Update user
await client.updateUser(userId, {
  first_name: 'Jane',
});

// Delete user
await client.deleteUser(userId);

Settings

// Get all settings
const settings = await client.getSettings();

// Get single setting
const siteName = await client.getSetting('site_name');

// Update a setting
await client.updateSetting('site_name', 'My Site', {
  type: 'string',
  description: 'Site name',
});

Response Format

All methods return a consistent response format:

interface ApiResponse<T> {
  success: boolean;
  data?: T;              // Present when success === true
  error?: {              // Present when success === false
    code: string;
    message: string;
    details?: any;
  };
  meta?: {
    timestamp: string;
    version: string;
    pagination?: {
      total: number;
      count: number;
      per_page: number;
      current_page: number;
      total_pages: number;
    };
  };
  links?: {
    self?: string;
    next?: string;
    prev?: string;
    first?: string;
    last?: string;
  };
}

Error Handling

const response = await client.getPost('invalid-post');

if (!response.success) {
  console.error('Error Code:', response.error?.code);
  console.error('Error Message:', response.error?.message);
  
  switch (response.error?.code) {
    case 'NOT_FOUND':
      // Handle 404
      break;
    case 'UNAUTHORIZED':
      // Handle 401 - might need to re-authenticate
      await client.authenticate();
      break;
    case 'FORBIDDEN':
      // Handle 403 - insufficient permissions
      break;
    default:
      // Handle other errors
  }
}

TypeScript Support

Full TypeScript types are included:

import { MultoCMSClient, Post, Site, Media, User } from '@multocms/connector';

const client = new MultoCMSClient({
  baseUrl: process.env.MULTOCMS_BASE_URL!,
});

const response = await client.getPost('hello-world');
if (response.success) {
  // response.data is typed as Post
  const post: Post = response.data;
  
  // TypeScript knows about all Post properties
  console.log(post.title);      // ✅ string
  console.log(post.slug);        // ✅ string
  console.log(post.featured_image?.url); // ✅ string | undefined
}

Utility Methods

// Manually set tokens
client.setAccessToken('token');
client.setRefreshToken('refresh-token');

// Get current access token
const token = client.getAccessToken();

// Clear tokens (logout)
client.logout();

Environment Variables

The client automatically reads these environment variables:

  • MULTOCMS_BASE_URL - Base URL for the API (required)
  • MULTOCMS_API_KEY - API key ID.secret format
  • MULTOCMS_API_SECRET - API secret (if not in key format)
  • MULTOCMS_ACCESS_TOKEN - Pre-existing access token
  • MULTOCMS_REFRESH_TOKEN - Pre-existing refresh token

Examples

Next.js API Route

// app/api/posts/route.ts
import { MultoCMSClient } from '@multocms/connector';

export async function GET() {
  const client = new MultoCMSClient({
    baseUrl: process.env.MULTOCMS_BASE_URL!,
  });

  const response = await client.getPosts({
    status: 'published',
    per_page: 10,
  });

  if (response.success) {
    return Response.json(response.data);
  }

  return Response.json(
    { error: response.error?.message },
    { status: 400 }
  );
}

React Server Component

// app/posts/page.tsx
import { MultoCMSClient } from '@multocms/connector';

export default async function PostsPage() {
  const client = new MultoCMSClient({
    baseUrl: process.env.MULTOCMS_BASE_URL!,
  });

  const response = await client.getPosts({
    status: 'published',
    per_page: 10,
  });

  if (!response.success) {
    return <div>Error: {response.error?.message}</div>;
  }

  return (
    <div>
      <h1>Posts</h1>
      {response.data?.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <div dangerouslySetInnerHTML={{ __html: post.excerpt }} />
        </article>
      ))}
    </div>
  );
}

Node.js Script

// scripts/sync-posts.ts
import { MultoCMSClient } from '@multocms/connector';
import dotenv from 'dotenv';

dotenv.config();

async function syncPosts() {
  const client = new MultoCMSClient({
    baseUrl: process.env.MULTOCMS_BASE_URL!,
  });

  const response = await client.getPosts({
    status: 'published',
    per_page: 100,
  });

  if (response.success) {
    console.log(`Found ${response.data?.length} posts`);
    response.data?.forEach((post) => {
      console.log(`- ${post.title} (${post.slug})`);
    });
  }
}

syncPosts();

Changelog

v1.3.7 (Latest)

  • NEW: Configurable cache revalidation via cacheRevalidate option or MULTOCMS_CACHE_REVALIDATE env var
  • FIX: Fixed authentication retry logic to properly rebuild fetch options with updated headers
  • Set to 0 for no caching (default), 60 for 60 seconds, etc.
  • Set to false to use Next.js default caching behavior

License

MIT

Support

For issues, questions, or contributions, please visit the MultoCMS Web Site.