JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 23
  • Score
    100M100P100Q77638F
  • License UNLICENSED

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

Package Exports

  • @bernierllc/content-management-suite
  • @bernierllc/content-management-suite/prisma
  • @bernierllc/content-management-suite/ui
  • @bernierllc/content-management-suite/utils

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: 'john@example.com'
  }
};

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: 'john@example.com'
  }
};

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: 'john@example.com'
  }
};

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: 'john@example.com'
  }
};

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.