@bernierllc/content-type-blog-post
Version:
Blog post content type with rich TipTap editor, SEO metadata, database storage, and web publishing
131 lines (110 loc) • 3.71 kB
text/typescript
/*
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.
*/
import { z } from 'zod';
/**
* Blog post SEO metadata schema
* Note: Allows empty strings for draft posts. Use validateSEOCompleteness() to enforce completeness before publishing.
*/
export const BlogPostSEOSchema = z.object({
metaTitle: z.string().max(60, 'Meta title should be 60 characters or less'),
metaDescription: z.string().max(160, 'Meta description should be 160 characters or less'),
keywords: z.array(z.string()).max(10, 'Maximum 10 keywords recommended'),
ogImage: z.string().url().optional(),
ogType: z.literal('article').default('article'),
canonicalUrl: z.string().url().optional(),
});
export type BlogPostSEO = z.infer<typeof BlogPostSEOSchema>;
/**
* Blog post author metadata schema
*/
export const BlogPostAuthorSchema = z.object({
name: z.string().min(1),
email: z.string().email().optional(),
avatar: z.string().url().optional(),
bio: z.string().max(500).optional(),
});
export type BlogPostAuthor = z.infer<typeof BlogPostAuthorSchema>;
/**
* Blog post workflow status
*/
export const BlogPostStatusSchema = z.enum(['draft', 'published', 'scheduled', 'archived']);
export type BlogPostStatus = z.infer<typeof BlogPostStatusSchema>;
/**
* Blog post metadata schema (extends text content metadata)
*/
export const BlogPostMetadataSchema = z.object({
// Inherited from TextContentMetadata
content: z.string().min(1, 'Content is required'),
createdAt: z.date(),
updatedAt: z.date(),
// Blog-specific fields
title: z.string().min(1).max(200),
slug: z.string().min(1).regex(/^[a-z0-9-]+$/, 'Slug must be lowercase letters, numbers, and hyphens'),
excerpt: z.string().max(300).optional(),
// SEO metadata
seo: BlogPostSEOSchema,
// Author information
author: BlogPostAuthorSchema,
// Taxonomy
tags: z.array(z.string()).default([]),
categories: z.array(z.string()).default([]),
// Publishing workflow
status: BlogPostStatusSchema.default('draft'),
publishedAt: z.date().optional(),
scheduledFor: z.date().optional(),
// Additional metadata
featuredImage: z.string().url().optional(),
readingTime: z.number().positive().optional(), // minutes
wordCount: z.number().nonnegative().default(0),
});
export type BlogPostMetadata = z.infer<typeof BlogPostMetadataSchema>;
/**
* TipTap editor configuration for blog posts
*/
export interface TipTapEditorConfig {
type: 'tiptap-wysiwyg';
extensions: string[]; // List of enabled extensions
placeholder?: string;
editable?: boolean;
autofocus?: boolean | 'start' | 'end';
}
/**
* Database storage configuration
*/
export interface DatabaseStorageConfig {
type: 'database';
table: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
client?: any; // PostgreSQL/Supabase client instance
}
/**
* Web publishing configuration
*/
export interface WebPublishingConfig {
type: 'web';
urlPattern: string; // e.g., '/blog/{slug}' or '/blog/{year}/{month}/{slug}'
baseUrl?: string;
}
/**
* Blog post content result
*/
export interface BlogPostResult<T = unknown> {
success: boolean;
data?: T;
error?: string;
}
/**
* Blog post content type configuration
*/
export interface BlogPostContentTypeConfig {
id?: string;
name?: string;
editor?: TipTapEditorConfig;
metadata?: z.ZodType<BlogPostMetadata>;
storage?: DatabaseStorageConfig;
publishing?: WebPublishingConfig;
}