
- Installs
- 55
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/miles990/claude-software-skills --skill content-platforms- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads content-platforms automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
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
FAQ
What does the content-platforms skill do?
CMS, blogging platforms, and content management patterns
How do I install content-platforms?
Run npx skills add https://github.com/miles990/claude-software-skills --skill content-platforms in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/content-platforms/ (personal) or .claude/skills/content-platforms/ (project).
Which AI agents can use content-platforms?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is content-platforms safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains content-platforms?
It is published by miles990 in miles990/claude-software-skills. It has 55 recorded installs.
More from miles990/claude-software-skills
code-quality
miles990/claude-software-skills
Clean code principles, SOLID, and code review practices
devops-cicd
miles990/claude-software-skills
CI/CD pipelines, infrastructure as code, and deployment strategies
game-development
miles990/claude-software-skills
Game development patterns, architectures, and best practices
project-management
miles990/claude-software-skills
Open the skill to read its SKILL.md instructions.
e-commerce
miles990/claude-software-skills
Open the skill to read its SKILL.md instructions.
saas-platforms
miles990/claude-software-skills
Open the skill to read its SKILL.md instructions.
Popular other skills
hyperframes-cli
heygen-com/hyperframes
The HyperFrames CLI by job - scaffold, check, look at frames, edit the timeline, preview, render locally or in the cloud, publish, project history - plus reading its JSON and recovering from build or render failures.
triage
mattpocock/skills
Move issues and external PRs through a state machine of triage roles, categorise, verify, grill if needed, and write agent-ready briefs.
ask-matt
mattpocock/skills
Ask which skill or flow fits your situation. A router over the skills in this repo.
to-questionnaire
mattpocock/skills
Turn a decision you can't fully answer into a questionnaire for someone else to fill in.
loop-me
mattpocock/skills
Grill me about specs for the workflows I want to build, within this workspace.