Resources
1Install
npx skillscat add miles990/claude-software-skills/content-platforms Install via the SkillsCat registry.
About this skill
This skill defines patterns and schemas for content management systems and blogging platforms, including headless CMS structures, field types, and media handling. It helps developers design, implement, and integrate structured content workflows, solving the need for flexible content modeling and publishing. It is appropriate for building content‑driven applications, blogs, or any system requiring organized content management.
SKILL.md
Content Platforms
Overview
Building content management systems, blogging platforms, and rich media applications.
Content Models
Headless CMS Schema
// Content types
interface ContentType {
id: string;
name: string;
slug: string;
fields: Field[];
settings: ContentTypeSettings;
}
interface Field {
id: string;
name: string;
type: FieldType;
required: boolean;
localized: boolean;
validation?: FieldValidation;
}
type FieldType =
| 'text'
| 'richText'
| 'number'
| 'boolean'
| 'date'
| 'media'
| 'reference'
| 'array'
| 'json';
// Blog post content type
const blogPostType: ContentType = {
id: 'blogPost',
name: 'Blog Post',
slug: 'blog-posts',
fields: [
{ id: 'title', name: 'Title', type: 'text', required: true, localized: true },
{ id: 'slug', name: 'Slug', type: 'text', required: true, localized: false },
{ id: 'content', name: 'Content', type: 'richText', required: true, localized: true },
{ id: 'excerpt', name: 'Excerpt', type: 'text', required: false, localized: true },
{ id: 'featuredImage', name: 'Featured Image', type: 'media', required: false, localized: false },
{ id: 'author', name: 'Author', type: 'reference', required: true, localized: false },
{ id: 'tags', name: 'Tags', type: 'array', required: false, localized: false },
{ id: 'publishedAt', name: 'Published At', type: 'date', required: false, localized: false },
{ id: 'seo', name: 'SEO', type: 'json', required: false, localized: true },
],
settings: {
previewable: true,
versionable: true,
publishable: true,
},
};
// Prisma schema
/*
model Content {
id String @id @default(cuid())
contentTypeId String
status String @default("draft")
data Json
locale String @default("en")
version Int @default(1)
publishedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([contentTypeId, status])
@@index([contentTypeId, locale])
}
*/Rich Text Editor
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Image from '@tiptap/extension-image';
import Link from '@tiptap/extension-link';
import Placeholder from '@tiptap/extension-placeholder';
function RichTextEditor({
content,
onChange,
}: {
content: string;
onChange: (content: string) => void;
}) {
const editor = useEditor({
extensions: [
StarterKit,
Image.configure({ inline: true }),
Link.configure({ openOnClick: false }),
Placeholder.configure({ placeholder: 'Start writing...' }),
],
content,
onUpdate: ({ editor }) => {
onChange(editor.getHTML());
},
});
if (!editor) return null;
return (
<div className="editor-wrapper">
<MenuBar editor={editor} />
<EditorContent editor={editor} className="prose max-w-none" />
</div>
);
}
function MenuBar({ editor }: { editor: Editor }) {
return (
<div className="menu-bar">
<button
onClick={() => editor.chain().focus().toggleBold().run()}
className={editor.isActive('bold') ? 'active' : ''}
>
Bold
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
className={editor.isActive('italic') ? 'active' : ''}
>
Italic
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
className={editor.isActive('heading', { level: 2 }) ? 'active' : ''}
>
H2
</button>
<button
onClick={() => editor.chain().focus().toggleBulletList().run()}
className={editor.isActive('bulletList') ? 'active' : ''}
>
Bullet List
</button>
<button
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
className={editor.isActive('codeBlock') ? 'active' : ''}
>
Code Block
</button>
<button onClick={() => addImage(editor)}>Image</button>
<button onClick={() => addLink(editor)}>Link</button>
</div>
);
}Media Management
import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import sharp from 'sharp';
const s3 = new S3Client({ region: process.env.AWS_REGION });
interface MediaAsset {
id: string;
filename: string;
mimeType: string;
size: number;
url: string;
thumbnailUrl?: string;
width?: number;
height?: number;
alt?: string;
}
// Upload with image processing
async function uploadMedia(file: Express.Multer.File): Promise<MediaAsset> {
const id = crypto.randomUUID();
const extension = path.extname(file.originalname);
const key = `media/${id}${extension}`;
let processedBuffer = file.buffer;
let width: number | undefined;
let height: number | undefined;
// Process images
if (file.mimetype.startsWith('image/')) {
const image = sharp(file.buffer);
const metadata = await image.metadata();
width = metadata.width;
height = metadata.height;
// Resize if too large
if (width && width > 2000) {
processedBuffer = await image
.resize(2000, null, { withoutEnlargement: true })
.toBuffer();
}
// Generate thumbnail
const thumbnail = await image
.resize(300, 300, { fit: 'cover' })
.webp({ quality: 80 })
.toBuffer();
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: `thumbnails/${id}.webp`,
Body: thumbnail,
ContentType: 'image/webp',
}));
}
// Upload original
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
Body: processedBuffer,
ContentType: file.mimetype,
}));
// Save to database
return prisma.media.create({
data: {
id,
filename: file.originalname,
mimeType: file.mimetype,
size: processedBuffer.length,
url: `${process.env.CDN_URL}/${key}`,
thumbnailUrl: file.mimetype.startsWith('image/')
? `${process.env.CDN_URL}/thumbnails/${id}.webp`
: undefined,
width,
height,
},
});
}
// Image optimization on-the-fly (with caching)
async function getOptimizedImage(
key: string,
options: { width?: number; height?: number; format?: 'webp' | 'avif' | 'jpeg' }
) {
const cacheKey = `optimized/${key}/${JSON.stringify(options)}`;
// Check cache
const cached = await redis.get(cacheKey);
if (cached) {
return Buffer.from(cached, 'base64');
}
// Get original
const original = await s3.send(new GetObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
}));
// Process
let image = sharp(await original.Body?.transformToByteArray());
if (options.width || options.height) {
image = image.resize(options.width, options.height, {
fit: 'inside',
withoutEnlargement: true,
});
}
if (options.format) {
image = image.toFormat(options.format, { quality: 80 });
}
const buffer = await image.toBuffer();
// Cache for 1 hour
await redis.setex(cacheKey, 3600, buffer.toString('base64'));
return buffer;
}Content Versioning
interface ContentVersion {
id: string;
contentId: string;
version: number;
data: Record<string, any>;
createdBy: string;
createdAt: Date;
changeDescription?: string;
}
// Create new version
async function createVersion(
contentId: string,
data: Record<string, any>,
userId: string,
description?: string
) {
const current = await prisma.content.findUnique({
where: { id: contentId },
});
// Save current as version
await prisma.contentVersion.create({
data: {
contentId,
version: current.version,
data: current.data,
createdBy: userId,
changeDescription: description,
},
});
// Update content
return prisma.content.update({
where: { id: contentId },
data: {
data,
version: { increment: 1 },
},
});
}
// Get version history
async function getVersionHistory(contentId: string) {
return prisma.contentVersion.findMany({
where: { contentId },
orderBy: { version: 'desc' },
include: {
createdByUser: { select: { name: true, avatar: true } },
},
});
}
// Restore version
async function restoreVersion(contentId: string, versionNumber: number, userId: string) {
const version = await prisma.contentVersion.findFirst({
where: { contentId, version: versionNumber },
});
if (!version) {
throw new Error('Version not found');
}
return createVersion(contentId, version.data, userId, `Restored from version ${versionNumber}`);
}
// Diff between versions
function diffVersions(oldVersion: ContentVersion, newVersion: ContentVersion) {
// Using deep-diff or similar library
const diff = require('deep-diff');
return diff(oldVersion.data, newVersion.data);
}Publishing Workflow
enum ContentStatus {
DRAFT = 'draft',
IN_REVIEW = 'in_review',
APPROVED = 'approved',
PUBLISHED = 'published',
ARCHIVED = 'archived',
}
// Workflow transitions
const workflowTransitions: Record<ContentStatus, ContentStatus[]> = {
[ContentStatus.DRAFT]: [ContentStatus.IN_REVIEW],
[ContentStatus.IN_REVIEW]: [ContentStatus.DRAFT, ContentStatus.APPROVED],
[ContentStatus.APPROVED]: [ContentStatus.IN_REVIEW, ContentStatus.PUBLISHED],
[ContentStatus.PUBLISHED]: [ContentStatus.ARCHIVED],
[ContentStatus.ARCHIVED]: [ContentStatus.DRAFT],
};
async function transitionContent(
contentId: string,
newStatus: ContentStatus,
userId: string,
comment?: string
) {
const content = await prisma.content.findUnique({ where: { id: contentId } });
const allowedTransitions = workflowTransitions[content.status];
if (!allowedTransitions.includes(newStatus)) {
throw new Error(`Cannot transition from ${content.status} to ${newStatus}`);
}
// Log transition
await prisma.contentWorkflowLog.create({
data: {
contentId,
fromStatus: content.status,
toStatus: newStatus,
userId,
comment,
},
});
// Update content
return prisma.content.update({
where: { id: contentId },
data: {
status: newStatus,
...(newStatus === ContentStatus.PUBLISHED && { publishedAt: new Date() }),
},
});
}
// Schedule publishing
async function schedulePublish(contentId: string, publishAt: Date) {
await prisma.content.update({
where: { id: contentId },
data: {
scheduledPublishAt: publishAt,
status: ContentStatus.APPROVED,
},
});
// Queue job
await queue.add('publish-content', { contentId }, {
delay: publishAt.getTime() - Date.now(),
});
}SEO & Metadata
interface SEOMetadata {
title: string;
description: string;
keywords?: string[];
ogImage?: string;
ogType?: string;
canonical?: string;
noIndex?: boolean;
}
function generateSEOTags(meta: SEOMetadata, url: string) {
return {
title: meta.title,
meta: [
{ name: 'description', content: meta.description },
meta.keywords && { name: 'keywords', content: meta.keywords.join(', ') },
meta.noIndex && { name: 'robots', content: 'noindex, nofollow' },
// Open Graph
{ property: 'og:title', content: meta.title },
{ property: 'og:description', content: meta.description },
{ property: 'og:type', content: meta.ogType || 'article' },
{ property: 'og:url', content: url },
meta.ogImage && { property: 'og:image', content: meta.ogImage },
// Twitter
{ name: 'twitter:card', content: 'summary_large_image' },
{ name: 'twitter:title', content: meta.title },
{ name: 'twitter:description', content: meta.description },
meta.ogImage && { name: 'twitter:image', content: meta.ogImage },
].filter(Boolean),
link: [
meta.canonical && { rel: 'canonical', href: meta.canonical },
].filter(Boolean),
};
}Related Skills
- [[frontend]] - Content rendering
- [[database]] - Content storage
- [[cloud-platforms]] - Media hosting