UNPKG

@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
/* 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; }