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

@bernierllc/content-management-suite

v2.2.1

Published

Comprehensive content management suite with editorial workflows, content types, and admin UI

Readme

/* Copyright (c) 2025 Bernier LLC

This file is licensed to the client under a limited-use license. The client may use and modify this code only within the scope of the project it was delivered for. Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC. */

Content Management Suite

A comprehensive content management suite with editorial workflows, content types, and admin UI built with TypeScript and Tamagui.

Features

  • 🎯 Editorial Workflows: Configurable content workflows with stages, transitions, and permissions
  • 📝 Content Types: Pluggable content type system supporting text, images, audio, and video
  • ⚡ Auto-save: Intelligent auto-save with backoff retry and debounce
  • 🗑️ Soft Delete: Configurable soft delete with user visibility options
  • 🎨 Modern UI: Tamagui-based components with responsive design
  • 🔧 Configuration: File + database override + environment variable configuration
  • 🔌 Plugin System: Extensible plugin architecture for custom functionality
  • 🛡️ Security: JWT authentication, role-based permissions, and rate limiting
  • 📊 Analytics: Built-in analytics and metrics collection
  • 🌐 API: RESTful API with comprehensive endpoints
  • 📱 Admin UI: NeverAdmin integration for workflow configuration
  • 🔍 Search: Full-text search across all content types
  • 📈 Performance: Caching, compression, and optimization

Installation

npm install @bernierllc/content-management-suite

Quick Start

import { createContentManagementSuite } from '@bernierllc/content-management-suite';

const suite = createContentManagementSuite({
  config: {
    server: {
      port: 3000,
      host: 'localhost'
    }
  }
});

await suite.start();
console.log('Content Management Suite started on http://localhost:3000');

Configuration

The suite supports comprehensive configuration through multiple sources:

1. File Configuration

Create a content-management.config.json file:

{
  "server": {
    "port": 3000,
    "host": "localhost",
    "cors": {
      "origin": "*",
      "credentials": true
    }
  },
  "database": {
    "type": "sqlite",
    "name": "content_management"
  },
  "content": {
    "defaultWorkflow": {
      "id": "standard",
      "name": "Standard Workflow",
      "stages": [
        {
          "id": "write",
          "name": "Write",
          "order": 1,
          "isPublishStage": false,
          "allowsScheduling": false,
          "permissions": ["content.edit"]
        },
        {
          "id": "publish",
          "name": "Publish",
          "order": 2,
          "isPublishStage": true,
          "allowsScheduling": true,
          "permissions": ["content.publish"]
        }
      ]
    }
  }
}

2. Environment Variables

# Server
CONTENT_MANAGEMENT_PORT=3000
CONTENT_MANAGEMENT_HOST=localhost

# Database
CONTENT_MANAGEMENT_DATABASE_TYPE=postgresql
CONTENT_MANAGEMENT_DATABASE_URL=postgresql://user:password@localhost:5432/content_management

# Security
CONTENT_MANAGEMENT_JWT_SECRET=your-secret-key
CONTENT_MANAGEMENT_JWT_EXPIRES_IN=24h

# Integrations
CONTENT_MANAGEMENT_NEVERADMIN_ENABLED=true
CONTENT_MANAGEMENT_NEVERADMIN_URL=https://admin.myapp.com
CONTENT_MANAGEMENT_NEVERADMIN_API_KEY=your-api-key

3. Database Override

Configuration can be overridden through the database for runtime changes:

await suite.updateConfig({
  ui: {
    theme: {
      mode: 'dark'
    }
  }
});

Content Types

The suite comes with four built-in content types:

Text Content

import { TextContentType } from '@bernierllc/content-management-suite';

// Register text content type
suite.registerContentType(TextContentType);

// Create text content
const textContent = {
  title: 'My Blog Post',
  slug: 'my-blog-post',
  body: '<p>Hello world!</p>',
  excerpt: 'A brief description',
  tags: ['blog', 'example'],
  categories: ['general'],
  author: {
    id: 'user-1',
    name: 'John Doe',
    email: '[email protected]'
  }
};

Image Content

import { ImageContentType } from '@bernierllc/content-management-suite';

// Register image content type
suite.registerContentType(ImageContentType);

// Create image content
const imageContent = {
  title: 'Beautiful Landscape',
  slug: 'beautiful-landscape',
  description: 'A stunning mountain landscape',
  altText: 'Mountain landscape with sunset',
  imageUrl: 'https://example.com/images/landscape.jpg',
  tags: ['landscape', 'nature'],
  categories: ['photography'],
  author: {
    id: 'user-1',
    name: 'John Doe',
    email: '[email protected]'
  }
};

Audio Content

import { AudioContentType } from '@bernierllc/content-management-suite';

// Register audio content type
suite.registerContentType(AudioContentType);

// Create audio content
const audioContent = {
  title: 'My Podcast Episode',
  slug: 'my-podcast-episode',
  description: 'A great podcast episode',
  audioUrl: 'https://example.com/audio/episode.mp3',
  duration: 1800, // 30 minutes
  metadata: {
    artist: 'John Doe',
    album: 'Tech Talk Podcast',
    year: 2023
  },
  tags: ['podcast', 'technology'],
  categories: ['podcast'],
  author: {
    id: 'user-1',
    name: 'John Doe',
    email: '[email protected]'
  }
};

Video Content

import { VideoContentType } from '@bernierllc/content-management-suite';

// Register video content type
suite.registerContentType(VideoContentType);

// Create video content
const videoContent = {
  title: 'My Video Tutorial',
  slug: 'my-video-tutorial',
  description: 'A comprehensive tutorial',
  videoUrl: 'https://example.com/videos/tutorial.mp4',
  duration: 1800, // 30 minutes
  dimensions: {
    width: 1920,
    height: 1080
  },
  metadata: {
    director: 'John Doe',
    genre: 'Educational',
    year: 2023
  },
  tags: ['tutorial', 'video'],
  categories: ['tutorial'],
  author: {
    id: 'user-1',
    name: 'John Doe',
    email: '[email protected]'
  }
};

Editorial Workflows

Configure custom editorial workflows:

const customWorkflow = {
  id: 'editorial',
  name: 'Editorial Workflow',
  description: 'Multi-stage editorial workflow',
  stages: [
    {
      id: 'write',
      name: 'Write',
      order: 1,
      isPublishStage: false,
      allowsScheduling: false,
      permissions: ['content.edit']
    },
    {
      id: 'edit',
      name: 'Edit',
      order: 2,
      isPublishStage: false,
      allowsScheduling: false,
      permissions: ['content.edit']
    },
    {
      id: 'review',
      name: 'Review',
      order: 3,
      isPublishStage: false,
      allowsScheduling: false,
      permissions: ['content.review']
    },
    {
      id: 'publish',
      name: 'Publish',
      order: 4,
      isPublishStage: true,
      allowsScheduling: true,
      permissions: ['content.publish']
    }
  ],
  transitions: [
    {
      id: 'write-to-edit',
      from: 'write',
      to: 'edit',
      permissions: ['content.edit']
    },
    {
      id: 'edit-to-review',
      from: 'edit',
      to: 'review',
      permissions: ['content.review']
    },
    {
      id: 'review-to-publish',
      from: 'review',
      to: 'publish',
      permissions: ['content.publish']
    }
  ]
};

await suite.workflowService.createWorkflow(customWorkflow);

API Endpoints

Content Management

  • GET /api/content - List content
  • GET /api/content/:id - Get content by ID
  • POST /api/content - Create content
  • PUT /api/content/:id - Update content
  • DELETE /api/content/:id - Delete content
  • POST /api/content/:id/publish - Publish content
  • POST /api/content/:id/schedule - Schedule content
  • POST /api/content/:id/unpublish - Unpublish content

Workflow Management

  • GET /api/workflows - List workflows
  • GET /api/workflows/:id - Get workflow by ID
  • POST /api/workflows - Create workflow
  • PUT /api/workflows/:id - Update workflow
  • DELETE /api/workflows/:id - Delete workflow

Content Type Management

  • GET /api/content-types - List content types
  • GET /api/content-types/:id - Get content type by ID
  • POST /api/content-types - Create content type
  • PUT /api/content-types/:id - Update content type
  • DELETE /api/content-types/:id - Delete content type

Configuration Management

  • GET /api/config - Get configuration
  • PUT /api/config - Update configuration

User Management

  • GET /api/users - List users
  • GET /api/users/:id - Get user by ID
  • POST /api/users - Create user
  • PUT /api/users/:id - Update user
  • DELETE /api/users/:id - Delete user

UI Components

Content Editor

import { ContentEditor } from '@bernierllc/content-management-suite';

<ContentEditor
  content={content}
  onChange={(content) => console.log('Content changed:', content)}
  onSave={(content) => console.log('Save:', content)}
  onPublish={(content) => console.log('Publish:', content)}
  onSchedule={(content, date) => console.log('Schedule:', content, date)}
  showToolbar={true}
  showStatusBar={true}
  showWordCount={true}
  showCharacterCount={true}
  placeholder="Start writing your content..."
  maxCharacters={10000}
  targetWordCount={1000}
/>

Workflow Components

import { 
  WorkflowStepper, 
  StageActionButtons, 
  WorkflowTimeline,
  WorkflowAdminConfig 
} from '@bernierllc/content-management-suite';

// Workflow stepper
<WorkflowStepper
  workflow={workflow}
  currentStage={currentStage}
  onStageChange={(stage) => console.log('Stage changed:', stage)}
/>

// Stage action buttons
<StageActionButtons
  stage={currentStage}
  content={content}
  onAction={(action) => console.log('Action:', action)}
/>

// Workflow timeline
<WorkflowTimeline
  workflow={workflow}
  content={content}
  onStageClick={(stage) => console.log('Stage clicked:', stage)}
/>

// Workflow admin configuration
<WorkflowAdminConfig
  workflow={workflow}
  onChange={(workflow) => console.log('Workflow changed:', workflow)}
  onSave={(workflow) => console.log('Workflow saved:', workflow)}
/>

Content List

import { ContentList } from '@bernierllc/content-management-suite';

<ContentList
  content={contentList}
  onContentSelect={(content) => console.log('Content selected:', content)}
  onContentEdit={(content) => console.log('Edit content:', content)}
  onContentDelete={(content) => console.log('Delete content:', content)}
  onContentPublish={(content) => console.log('Publish content:', content)}
  view="table" // table, list, grid, kanban
  showSearch={true}
  showFilters={true}
  showSorting={true}
  showPagination={true}
  pageSize={20}
/>

Plugin System

Create custom plugins:

const analyticsPlugin = {
  name: 'analytics',
  version: '1.0.0',
  description: 'Analytics plugin for content management',
  dependencies: ['@bernierllc/analytics'],
  install: async (suite) => {
    console.log('Installing analytics plugin');
    // Initialize analytics
  },
  uninstall: async (suite) => {
    console.log('Uninstalling analytics plugin');
    // Cleanup analytics
  },
  enable: async (suite) => {
    console.log('Enabling analytics plugin');
    // Enable analytics tracking
  },
  disable: async (suite) => {
    console.log('Disabling analytics plugin');
    // Disable analytics tracking
  }
};

suite.registerPlugin(analyticsPlugin);

Middleware System

Add custom middleware:

suite.addMiddleware({
  name: 'request-logger',
  handler: (req, res, next) => {
    console.log(`${req.method} ${req.path} - ${new Date().toISOString()}`);
    next();
  },
  order: 1
});

Hook System

Add custom hooks:

suite.addHook({
  name: 'content:created',
  handler: async (content) => {
    console.log('New content created:', content.id);
    // Send notification, update analytics, etc.
  },
  priority: 1
});

Security

JWT Authentication

const suite = createContentManagementSuite({
  config: {
    security: {
      jwt: {
        secret: 'your-super-secret-jwt-key',
        expiresIn: '24h',
        issuer: 'content-management-suite'
      }
    }
  }
});

Role-Based Permissions

const suite = createContentManagementSuite({
  config: {
    security: {
      permissions: {
        enabled: true,
        defaultRole: 'user',
        roles: [
          {
            name: 'admin',
            permissions: ['*'],
            description: 'Full administrative access'
          },
          {
            name: 'editor',
            permissions: ['content.edit', 'content.publish', 'content.schedule'],
            description: 'Content editing and publishing'
          },
          {
            name: 'author',
            permissions: ['content.edit'],
            description: 'Content creation and editing'
          },
          {
            name: 'user',
            permissions: ['content.view'],
            description: 'Content viewing only'
          }
        ]
      }
    }
  }
});

Performance

Caching

const suite = createContentManagementSuite({
  config: {
    performance: {
      cache: {
        enabled: true,
        ttl: 300, // 5 minutes
        maxSize: 1000
      }
    }
  }
});

Compression

const suite = createContentManagementSuite({
  config: {
    performance: {
      compression: {
        enabled: true,
        level: 6
      }
    }
  }
});

Rate Limiting

const suite = createContentManagementSuite({
  config: {
    performance: {
      rateLimit: {
        enabled: true,
        windowMs: 15 * 60 * 1000, // 15 minutes
        max: 100
      }
    }
  }
});

Logging

const suite = createContentManagementSuite({
  config: {
    logging: {
      level: 'info',
      format: 'json',
      file: {
        enabled: true,
        path: './logs',
        maxSize: '10MB',
        maxFiles: 5
      },
      console: {
        enabled: true,
        colorize: true
      }
    }
  }
});

Integrations

NeverAdmin Integration

const suite = createContentManagementSuite({
  config: {
    integrations: {
      neverAdmin: {
        enabled: true,
        url: 'https://admin.myapp.com',
        apiKey: 'your-api-key',
        syncInterval: 300000 // 5 minutes
      }
    }
  }
});

NeverHub Integration

const suite = createContentManagementSuite({
  config: {
    integrations: {
      neverHub: {
        enabled: true,
        url: 'https://hub.myapp.com',
        apiKey: 'your-api-key',
        packageDiscovery: true
      }
    }
  }
});

Error Handling

suite.addHook({
  name: 'error',
  handler: async (error) => {
    console.error('Suite error:', error);
    // Send to error tracking service
  }
});

Graceful Shutdown

process.on('SIGINT', async () => {
  console.log('Received SIGINT, shutting down gracefully...');
  try {
    await suite.stop();
    console.log('Suite stopped successfully');
    process.exit(0);
  } catch (error) {
    console.error('Error during shutdown:', error);
    process.exit(1);
  }
});

Development vs Production

const isDevelopment = process.env.NODE_ENV === 'development';

const suite = createContentManagementSuite({
  config: {
    server: {
      port: isDevelopment ? 3000 : 80,
      host: isDevelopment ? 'localhost' : '0.0.0.0'
    },
    logging: {
      level: isDevelopment ? 'debug' : 'info',
      format: isDevelopment ? 'text' : 'json'
    },
    security: {
      jwt: {
        secret: isDevelopment ? 'dev-secret' : process.env.JWT_SECRET
      }
    }
  }
});

Testing

# Run unit tests
npm test

# Run Playwright tests
npm run test:playwright

# Run all tests
npm run test:all

Examples

See the examples.ts file for comprehensive usage examples:

  • Basic setup
  • Advanced configuration
  • Custom content types
  • Plugin system
  • Error handling
  • API usage
  • Graceful shutdown
  • Environment-specific setup

AI Namespace (suite.ai)

The suite.ai namespace provides AI-powered content operations. All methods work through thin service interfaces you supply — the suite does not hard-code a specific AI provider.

Configuration

import { createContentManagementSuite } from '@bernierllc/content-management-suite';
import type {
  AiTagService, AiExcerptService, AiSeoService,
  AiPredictService, AiAltTextService,
} from '@bernierllc/content-management-suite';

const suite = createContentManagementSuite({
  ai: {
    tagService: myTagService,           // implements AiTagService
    excerptService: myExcerptService,   // implements AiExcerptService
    seoService: mySeoService,           // implements AiSeoService
    predictService: myPredictService,   // implements AiPredictService
    altTextService: myAltTextService,   // implements AiAltTextService
  },
});

suite.ai.tag(contentId, options?)

Suggest tags and categories for a content item.

const result = await suite.ai.tag('content-123', {
  maxTags: 10,           // default 10
  maxCategories: 3,      // default 3
  existingTags: ['typescript', 'cms'],
  existingCategories: ['Engineering'],
});

// result: AiTagResult
// {
//   contentId: 'content-123',
//   tags: [{ name: string; confidence: number; reason: string }],
//   categories: [{ name: string; confidence: number; reason: string }],
//   generatedAt: '2026-05-28T00:00:00.000Z'
// }
  • Confidence scores are 0.0–1.0.
  • When existingTags are provided, the service should prefer matching those before suggesting new ones.
  • Persists with type: 'tagging'.
  • Emits ai:tagged event with the full AiTagResult.
  • Throws NotFoundError if contentId does not exist.

suite.ai.enhance(contentId, options?) — excerpt generation extension

When generateExcerpt: true, returns AiExcerptResult instead of EnhancedContent.

// Standard enhancement (unchanged behaviour)
const enhanced = await suite.ai.enhance('content-123', { tone: 'professional' });

// Excerpt generation
const excerpts = await suite.ai.enhance('content-123', {
  generateExcerpt: true,
  platforms: ['social', 'newsletter', 'seo', 'email-subject'],
});

// excerpts: AiExcerptResult
// {
//   contentId: 'content-123',
//   excerpts: [{
//     platform: 'social' | 'newsletter' | 'seo' | 'email-subject',
//     text: string,
//     characterCount: number,
//     constraints: { maxLength: number; style: string }
//   }],
//   generatedAt: '...'
// }

Platform character limits: social ≤280, newsletter ≤500, seo ≤160, email-subject ≤60.

When no AiExcerptService is configured the suite falls back to truncating the content body to fit each platform's limit.

  • Persists with type: 'excerpt'.
  • Emits ai:excerpts:generated event.
  • Throws NotFoundError if contentId does not exist.

suite.ai.suggest(contentId, options?) — SEO analysis extension

When seo: true, returns AiSeoResult instead of Suggestion[].

// Standard suggestions (unchanged behaviour)
const suggestions = await suite.ai.suggest('content-123');

// SEO analysis
const seo = await suite.ai.suggest('content-123', {
  seo: true,
  siteUrl: 'https://example.com',
  internalPages: [{ url: '/blog', title: 'Blog' }],
});

// seo: AiSeoResult
// {
//   contentId: string,
//   overallScore: number,         // 0-100
//   keywords: {
//     primaryKeyword: string,
//     secondaryKeywords: string[],
//     density: number,
//     recommendations: string[]
//   },
//   metaDescription: {
//     current: string | null,
//     suggested: string,          // ≤160 chars
//     lengthAssessment: string,
//     includesKeyword: boolean
//   },
//   headingStructure: { structure, issues, suggestions },
//   internalLinking: {
//     existingLinks: string[],
//     suggestedLinks: { url, anchorText }[]  // [] when internalPages not provided
//   },
//   readability: { score, gradeLevel, averageSentenceLength, recommendations },
//   generatedAt: string
// }
  • Requires seoService in ai config when seo: true.
  • Persists with type: 'seo'.
  • Emits ai:seo:analyzed event.
  • Throws NotFoundError if contentId does not exist.

suite.ai.predict(contentId, options?)

Predict content performance before publishing.

const prediction = await suite.ai.predict('content-123', {
  audience: 'software developers',
  category: 'Engineering',
  timezone: 'America/Denver',       // default: 'America/Denver'
  platforms: ['website', 'social', 'newsletter'],
});

// prediction: AiPredictionResult
// {
//   contentId: string,
//   engagementScore: number,        // 0-100
//   audienceFitScore: number,       // 0-100
//   qualityScore: number,           // 0-100
//   publishTimeSuggestions: [{
//     platform: string,
//     suggestedDay: string,
//     suggestedTimeUtc: string,
//     suggestedTimeLocal: string,
//     reason: string
//   }],
//   contentStrengths: string[],
//   contentWeaknesses: string[],
//   generatedAt: string
// }
  • Persists with type: 'prediction'.
  • Emits ai:predicted event.
  • Throws NotFoundError if contentId does not exist.

suite.ai.generateAltText(imageUrl, options?)

Generate WCAG-compliant alt text and caption for an image URL. Operates on an image URL directly — does not require a contentId.

const result = await suite.ai.generateAltText('https://example.com/photo.jpg', {
  contentContext: 'Hero image for a blog post about TypeScript',
  language: 'en',               // default: 'en'
  maxAltTextLength: 125,        // default: 125
  maxCaptionLength: 250,        // default: 250
});

// result: AiAltTextResult
// {
//   imageUrl: string,
//   altText: string,
//   altTextLength: number,
//   caption: string,
//   captionLength: number,
//   isDecorative: boolean,
//   imageDescription: string,
//   generatedAt: string
// }
  • Throws ProviderCapabilityError if the configured provider does not support vision.
  • Throws InternalError if no altTextService is configured.
  • Storage key is a SHA-256 hash of the normalized imageUrl.
  • Persists with type: 'alt-text'.
  • Emits ai:alt-text:generated event.

AI Events

| Event | Payload | |-------|---------| | ai:tagged | AiTagResult | | ai:excerpts:generated | AiExcerptResult | | ai:seo:analyzed | AiSeoResult | | ai:predicted | AiPredictionResult | | ai:alt-text:generated | AiAltTextResult |


Errors

| Class | Code | When thrown | |-------|------|-------------| | NotFoundError | NOT_FOUND | contentId does not exist (tag, enhance, suggest, predict) | | InternalError | INTERNAL_ERROR | Required service not configured | | ProviderCapabilityError | PROVIDER_CAPABILITY_ERROR | Provider lacks required capability (e.g. vision for alt text) |

All error classes are exported from the package entry point.

License

Copyright (c) 2025 Bernier LLC. Licensed under limited-use license.