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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@bernierllc/content-type-blog-post

v1.0.2

Published

Blog post content type with rich TipTap editor, SEO metadata, database storage, and web publishing

Downloads

31

Readme

@bernierllc/content-type-blog-post

Blog post content type with rich TipTap editor, SEO metadata, database storage, and web publishing capabilities for modern content management systems.

Installation

npm install @bernierllc/content-type-blog-post

Peer Dependencies

This package requires PostgreSQL client:

npm install pg

Usage

Basic Setup

import { BlogPostContentType } from '@bernierllc/content-type-blog-post';
import { Pool } from 'pg';

// Initialize database client
const pool = new Pool({
  connectionString: process.env.DATABASE_URL
});

// Create blog post content type
const blogPost = new BlogPostContentType({
  dbClient: pool,
  tableName: 'blog_posts',
  baseUrl: 'https://example.com'
});

// Initialize database schema
await blogPost.initializeDatabase();

Creating a Blog Post

const result = await blogPost.create({
  title: 'Getting Started with TypeScript',
  slug: 'getting-started-with-typescript',
  content: '<p>TypeScript is a powerful...</p>',
  excerpt: 'Learn the basics of TypeScript in this comprehensive guide.',
  seo: {
    metaTitle: 'Getting Started with TypeScript - Complete Guide',
    metaDescription: 'Learn TypeScript basics, advanced types, and best practices in this comprehensive guide.',
    keywords: ['typescript', 'javascript', 'programming'],
    ogImage: 'https://example.com/images/typescript-guide.jpg'
  },
  author: {
    name: 'John Doe',
    email: '[email protected]',
    avatar: 'https://example.com/avatars/john.jpg'
  },
  tags: ['typescript', 'tutorial', 'beginners'],
  categories: ['Programming', 'Web Development'],
  status: 'draft'
});

if (result.success) {
  console.log('Blog post created:', result.data);
} else {
  console.error('Failed to create blog post:', result.error);
}

Publishing a Blog Post

// Update status to published
const publishResult = await blogPost.update('post-id-here', {
  status: 'published',
  publishedAt: new Date()
});

// Get the publish URL
const url = blogPost.getPublishUrl('getting-started-with-typescript');
// Returns: https://example.com/blog/getting-started-with-typescript

Retrieving Blog Posts

// Get a single blog post
const post = await blogPost.read('post-id-here');

// List all published posts
const publishedPosts = await blogPost.list({ status: 'published' });

// List posts by tag
const taggedPosts = await blogPost.list({ tags: ['typescript'] });

// List posts by author
const authorPosts = await blogPost.list({
  author: { name: 'John Doe' }
});

Updating a Blog Post

const updateResult = await blogPost.update('post-id-here', {
  title: 'Updated Title',
  content: '<p>Updated content...</p>',
  seo: {
    metaTitle: 'Updated Meta Title',
    metaDescription: 'Updated meta description'
  }
});

Deleting a Blog Post

const deleteResult = await blogPost.delete('post-id-here');

if (deleteResult.success) {
  console.log('Blog post deleted successfully');
}

API Reference

BlogPostContentType

Main class for managing blog post content.

Constructor

constructor(config?: {
  dbClient?: any;
  tableName?: string;
  baseUrl?: string;
})

Parameters:

  • dbClient - PostgreSQL client instance (from pg package)
  • tableName - Database table name (default: 'blog_posts')
  • baseUrl - Base URL for published posts (default: '')

Methods

initializeDatabase()

Initialize database schema with blog posts table.

async initializeDatabase(): Promise<BlogPostResult<void>>
create(metadata)

Create a new blog post.

async create(metadata: Partial<BlogPostMetadata>): Promise<BlogPostResult<BlogPostMetadata>>
read(id)

Retrieve a blog post by ID.

async read(id: string): Promise<BlogPostResult<BlogPostMetadata>>
update(id, updates)

Update an existing blog post.

async update(id: string, updates: Partial<BlogPostMetadata>): Promise<BlogPostResult<BlogPostMetadata>>
delete(id)

Delete a blog post by ID.

async delete(id: string): Promise<BlogPostResult<void>>
list(filters?)

List blog posts with optional filtering.

async list(filters?: {
  status?: BlogPostStatus;
  tags?: string[];
  categories?: string[];
  author?: { name?: string; email?: string };
}): Promise<BlogPostResult<BlogPostMetadata[]>>
getPublishUrl(slug)

Get the publish URL for a blog post.

getPublishUrl(slug: string): string
validate(metadata)

Validate blog post metadata against schema.

validate(metadata: unknown): BlogPostResult<BlogPostMetadata>

Types

BlogPostMetadata

interface BlogPostMetadata {
  content: string;
  createdAt: Date;
  updatedAt: Date;
  title: string;
  slug: string;
  excerpt?: string;
  seo: BlogPostSEO;
  author: BlogPostAuthor;
  tags: string[];
  categories: string[];
  status: BlogPostStatus;
  publishedAt?: Date;
  scheduledFor?: Date;
  featuredImage?: string;
  readingTime?: number;
  wordCount: number;
}

BlogPostSEO

interface BlogPostSEO {
  metaTitle: string;
  metaDescription: string;
  keywords: string[];
  ogImage?: string;
  ogType: 'article';
  canonicalUrl?: string;
}

BlogPostAuthor

interface BlogPostAuthor {
  name: string;
  email?: string;
  avatar?: string;
  bio?: string;
}

BlogPostStatus

type BlogPostStatus = 'draft' | 'published' | 'scheduled' | 'archived';

Utility Functions

calculateWordCount(content)

Calculate word count from HTML content.

function calculateWordCount(content: string): number

calculateReadingTime(wordCount, wordsPerMinute?)

Calculate reading time in minutes.

function calculateReadingTime(wordCount: number, wordsPerMinute?: number): number

validateSEOCompleteness(seo)

Validate that SEO metadata is complete for publishing.

function validateSEOCompleteness(seo: BlogPostSEO): {
  isComplete: boolean;
  errors: string[];
}

generateSlug(title)

Generate URL-safe slug from title.

function generateSlug(title: string): string

Configuration

Database Schema

The package automatically creates the following PostgreSQL schema:

CREATE TABLE blog_posts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  title VARCHAR(200) NOT NULL,
  slug VARCHAR(200) NOT NULL UNIQUE,
  content TEXT NOT NULL,
  excerpt VARCHAR(300),

  -- SEO metadata
  meta_title VARCHAR(60),
  meta_description VARCHAR(160),
  keywords TEXT[],
  og_image TEXT,
  canonical_url TEXT,

  -- Author information
  author_name VARCHAR(100) NOT NULL,
  author_email VARCHAR(100),
  author_avatar TEXT,
  author_bio VARCHAR(500),

  -- Taxonomy
  tags TEXT[] DEFAULT '{}',
  categories TEXT[] DEFAULT '{}',

  -- Publishing workflow
  status VARCHAR(20) DEFAULT 'draft',
  published_at TIMESTAMPTZ,
  scheduled_for TIMESTAMPTZ,

  -- Additional metadata
  featured_image TEXT,
  reading_time INTEGER,
  word_count INTEGER DEFAULT 0,

  -- Timestamps
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_blog_posts_slug ON blog_posts(slug);
CREATE INDEX idx_blog_posts_status ON blog_posts(status);
CREATE INDEX idx_blog_posts_published_at ON blog_posts(published_at);
CREATE INDEX idx_blog_posts_tags ON blog_posts USING GIN(tags);
CREATE INDEX idx_blog_posts_categories ON blog_posts USING GIN(categories);

TipTap Editor Configuration

The content type includes TipTap WYSIWYG editor with the following extensions:

  • starter-kit - Basic formatting (bold, italic, headings, lists, etc.)
  • link - Hyperlink support
  • image - Image embedding
  • code-block-lowlight - Syntax-highlighted code blocks
  • table - Table support with rows, cells, and headers

Integration Status

Logger Integration

Status: Planned

Logger integration will be added in a future release for:

  • Audit logging of blog post lifecycle events (create, update, publish, delete)
  • Database operation logging
  • Error tracking and debugging
  • Performance monitoring

NeverHub Integration

Status: Planned

NeverHub integration will be added in a future release for:

  • Event publishing for blog post lifecycle (created, updated, published, deleted)
  • Service discovery for author lookup and media management
  • Real-time collaboration features
  • Webhook notifications for publishing events

Graceful Degradation

The package is designed to work standalone without any external integrations. Logger and NeverHub integrations will be optional enhancements that provide additional functionality when available.

Examples

Complete Blog Publishing Workflow

import { BlogPostContentType, generateSlug } from '@bernierllc/content-type-blog-post';

const blogPost = new BlogPostContentType({ dbClient, baseUrl: 'https://blog.example.com' });

// 1. Create draft
const draft = await blogPost.create({
  title: 'My First Blog Post',
  slug: generateSlug('My First Blog Post'),
  content: '<h1>Hello World</h1><p>This is my first post...</p>',
  author: { name: 'Jane Smith', email: '[email protected]' },
  status: 'draft'
});

// 2. Add SEO metadata
await blogPost.update(draft.data.id, {
  seo: {
    metaTitle: 'My First Blog Post - Example Blog',
    metaDescription: 'An introduction to blogging on our new platform.',
    keywords: ['blogging', 'first post', 'introduction']
  }
});

// 3. Schedule for publishing
await blogPost.update(draft.data.id, {
  status: 'scheduled',
  scheduledFor: new Date('2025-12-01T09:00:00Z')
});

// 4. Publish immediately
await blogPost.update(draft.data.id, {
  status: 'published',
  publishedAt: new Date()
});

// 5. Get publish URL
const url = blogPost.getPublishUrl(draft.data.slug);
console.log(`Published at: ${url}`);

SEO Best Practices

import { validateSEOCompleteness } from '@bernierllc/content-type-blog-post';

// Validate SEO before publishing
const seoValidation = validateSEOCompleteness({
  metaTitle: 'Complete Guide to TypeScript',
  metaDescription: 'Learn TypeScript from basics to advanced concepts.',
  keywords: ['typescript', 'javascript'],
  ogImage: 'https://example.com/og-image.jpg'
});

if (!seoValidation.isComplete) {
  console.error('SEO incomplete:', seoValidation.errors);
  // ['Meta title should be between 30-60 characters', ...]
}

Reading Time Calculation

import { calculateWordCount, calculateReadingTime } from '@bernierllc/content-type-blog-post';

const content = '<p>Your blog post content here...</p>';
const wordCount = calculateWordCount(content);
const readingTime = calculateReadingTime(wordCount); // default: 200 words/minute

await blogPost.create({
  title: 'Article Title',
  content,
  wordCount,
  readingTime,
  // ... other fields
});

Error Handling

All methods return a BlogPostResult type for consistent error handling:

interface BlogPostResult<T> {
  success: boolean;
  data?: T;
  error?: string;
}

Example usage:

const result = await blogPost.create({ /* metadata */ });

if (result.success) {
  console.log('Created:', result.data);
} else {
  console.error('Failed:', result.error);
}

Testing

The package includes comprehensive test coverage (91%+ coverage):

# Run tests
npm test

# Run tests with coverage
npm run test:coverage

# Run tests in watch mode
npm run test:watch

License

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.

See Also