content-platforms

Building content management systems, blogging platforms, and rich media applications.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "content-platforms" with this command: npx skills add miles990/claude-software-skills/miles990-claude-software-skills-content-platforms

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 &#x26;&#x26; 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 &#x26;&#x26; { 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 &#x26;&#x26; { name: 'twitter:image', content: meta.ogImage },
].filter(Boolean),
link: [
  meta.canonical &#x26;&#x26; { rel: 'canonical', href: meta.canonical },
].filter(Boolean),

}; }

Related Skills

  • [[frontend]] - Content rendering

  • [[database]] - Content storage

  • [[cloud-platforms]] - Media hosting

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

saas-platforms

No summary provided by upstream source.

Repository SourceNeeds Review
General

architecture-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

frontend

No summary provided by upstream source.

Repository SourceNeeds Review