
Content Platforms
- 47 installs
- 19 repo stars
- Updated January 20, 2026
- miles990/claude-software-skills
Helps with marketing & seo tasks.
About
content-platforms is a Claude Code skill for marketing & seo. It helps solo builders move faster with AI-assisted development.
- content-platforms
- Marketing & SEO
- AI-coding skill
Content Platforms by the numbers
- 47 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,345 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/miles990/claude-software-skills --skill content-platformsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 47 |
|---|---|
| repo stars | ★ 19 |
| Last updated | January 20, 2026 |
| Repository | miles990/claude-software-skills ↗ |
What it does
Helps with marketing & seo tasks.
Files
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
/**
* CMS Schema Template
* Usage: Core types for content management systems
*/
// ===========================================
// Content Types
// ===========================================
export interface Content {
id: string;
type: ContentType;
title: string;
slug: string;
status: ContentStatus;
visibility: ContentVisibility;
author: Author;
content: ContentBody;
metadata: ContentMetadata;
seo: SEOMetadata;
scheduling: SchedulingConfig;
revisions: Revision[];
createdAt: Date;
updatedAt: Date;
publishedAt?: Date;
}
export type ContentType =
| 'article'
| 'page'
| 'blog_post'
| 'news'
| 'product'
| 'landing_page'
| 'documentation'
| 'faq';
export type ContentStatus =
| 'draft'
| 'pending_review'
| 'approved'
| 'published'
| 'archived'
| 'deleted';
export type ContentVisibility = 'public' | 'private' | 'password_protected' | 'members_only';
// ===========================================
// Content Body
// ===========================================
export interface ContentBody {
format: 'html' | 'markdown' | 'blocks' | 'rich_text';
raw: string;
rendered?: string;
blocks?: ContentBlock[];
excerpt?: string;
wordCount?: number;
readingTime?: number; // minutes
}
export interface ContentBlock {
id: string;
type: BlockType;
data: Record<string, unknown>;
children?: ContentBlock[];
}
export type BlockType =
| 'paragraph'
| 'heading'
| 'image'
| 'video'
| 'embed'
| 'code'
| 'quote'
| 'list'
| 'table'
| 'divider'
| 'callout'
| 'accordion'
| 'gallery'
| 'cta';
// ===========================================
// Media & Assets
// ===========================================
export interface Media {
id: string;
type: MediaType;
filename: string;
originalFilename: string;
mimeType: string;
size: number; // bytes
url: string;
thumbnails?: Record<string, string>;
dimensions?: { width: number; height: number };
duration?: number; // seconds for audio/video
alt?: string;
caption?: string;
metadata: MediaMetadata;
uploadedBy: string;
uploadedAt: Date;
}
export type MediaType = 'image' | 'video' | 'audio' | 'document' | 'archive';
export interface MediaMetadata {
folderId?: string;
tags: string[];
exif?: Record<string, unknown>;
blurhash?: string;
dominantColor?: string;
}
// ===========================================
// Taxonomy
// ===========================================
export interface Category {
id: string;
name: string;
slug: string;
description?: string;
parentId?: string;
order: number;
metadata?: Record<string, unknown>;
}
export interface Tag {
id: string;
name: string;
slug: string;
description?: string;
count: number; // number of contents using this tag
}
export interface Taxonomy {
categories: Category[];
tags: Tag[];
customTaxonomies?: CustomTaxonomy[];
}
export interface CustomTaxonomy {
id: string;
name: string;
slug: string;
hierarchical: boolean;
terms: TaxonomyTerm[];
}
export interface TaxonomyTerm {
id: string;
name: string;
slug: string;
parentId?: string;
}
// ===========================================
// Author & Users
// ===========================================
export interface Author {
id: string;
name: string;
slug: string;
email: string;
avatar?: string;
bio?: string;
socialLinks?: SocialLinks;
role: AuthorRole;
}
export type AuthorRole = 'admin' | 'editor' | 'author' | 'contributor' | 'subscriber';
export interface SocialLinks {
twitter?: string;
linkedin?: string;
github?: string;
website?: string;
}
// ===========================================
// SEO & Metadata
// ===========================================
export interface ContentMetadata {
categoryIds: string[];
tagIds: string[];
featuredImage?: Media;
customFields: Record<string, unknown>;
relatedContentIds: string[];
language: string;
translations?: Record<string, string>; // language -> contentId
}
export interface SEOMetadata {
metaTitle?: string;
metaDescription?: string;
canonicalUrl?: string;
ogImage?: string;
ogTitle?: string;
ogDescription?: string;
twitterCard?: 'summary' | 'summary_large_image';
noIndex?: boolean;
noFollow?: boolean;
schema?: Record<string, unknown>; // JSON-LD structured data
}
// ===========================================
// Scheduling & Workflow
// ===========================================
export interface SchedulingConfig {
publishAt?: Date;
unpublishAt?: Date;
timezone: string;
}
export interface Revision {
id: string;
version: number;
content: ContentBody;
authorId: string;
createdAt: Date;
comment?: string;
changes?: ContentDiff[];
}
export interface ContentDiff {
field: string;
before: unknown;
after: unknown;
}
// ===========================================
// Comments & Engagement
// ===========================================
export interface Comment {
id: string;
contentId: string;
parentId?: string;
author: CommentAuthor;
body: string;
status: CommentStatus;
createdAt: Date;
updatedAt?: Date;
likes: number;
replies?: Comment[];
}
export interface CommentAuthor {
id?: string;
name: string;
email: string;
website?: string;
isRegistered: boolean;
}
export type CommentStatus = 'pending' | 'approved' | 'spam' | 'deleted';
// ===========================================
// CMS Configuration
// ===========================================
export interface CMSConfig {
siteName: string;
siteUrl: string;
defaultLanguage: string;
supportedLanguages: string[];
mediaUpload: MediaUploadConfig;
contentTypes: ContentTypeConfig[];
workflows: WorkflowConfig[];
}
export interface MediaUploadConfig {
maxFileSize: number; // bytes
allowedMimeTypes: string[];
storagePath: string;
cdnUrl?: string;
imageOptimization: {
enabled: boolean;
quality: number;
formats: ('webp' | 'avif')[];
};
}
export interface ContentTypeConfig {
type: ContentType;
label: string;
fields: FieldDefinition[];
templates: string[];
defaultTemplate: string;
}
export interface FieldDefinition {
name: string;
type: 'text' | 'richtext' | 'number' | 'date' | 'media' | 'relation' | 'select' | 'boolean';
label: string;
required: boolean;
defaultValue?: unknown;
validation?: Record<string, unknown>;
}
export interface WorkflowConfig {
name: string;
stages: WorkflowStage[];
transitions: WorkflowTransition[];
}
export interface WorkflowStage {
id: string;
name: string;
status: ContentStatus;
permissions: string[];
}
export interface WorkflowTransition {
from: string;
to: string;
conditions?: Record<string, unknown>;
actions?: string[];
}
Content Platforms Templates
Schema templates for content management systems.
Files
| Template | Purpose |
|---|---|
cms-schema.ts | Core CMS type definitions |
Usage
# Copy template
cp templates/cms-schema.ts ./src/types/cms.ts
# Import types
import type { Content, Media, Category } from './types/cms';Schema Overview
Content Structure
Content
├── id, slug, title
├── type (article, page, blog_post, ...)
├── status (draft, published, ...)
├── visibility (public, private, ...)
├── content (body, format, blocks)
├── metadata (categories, tags, customFields)
├── seo (meta tags, OG, schema)
├── scheduling (publish/unpublish dates)
└── revisions[]Key Types
| Type | Description |
|---|---|
Content | Main content entity |
ContentBody | Content format and data |
ContentBlock | Block-based content |
Media | Media/asset management |
Category | Hierarchical taxonomy |
Tag | Flat taxonomy |
Author | Content creator |
SEOMetadata | Search optimization |
Revision | Version history |
Comment | User engagement |
Content Types
type ContentType =
| 'article' // News articles
| 'page' // Static pages
| 'blog_post' // Blog entries
| 'news' // News items
| 'product' // Product pages
| 'landing_page' // Marketing pages
| 'documentation' // Docs
| 'faq'; // FAQ entriesContent Status
draft → pending_review → approved → published → archived
↓
deletedBlock-Based Content
const blocks: ContentBlock[] = [
{ id: '1', type: 'heading', data: { level: 1, text: 'Title' } },
{ id: '2', type: 'paragraph', data: { text: 'Content...' } },
{ id: '3', type: 'image', data: { src: '/img.jpg', alt: 'Image' } },
{ id: '4', type: 'code', data: { language: 'ts', code: '...' } },
];Media Management
const media: Media = {
id: 'media_123',
type: 'image',
filename: 'hero.webp',
mimeType: 'image/webp',
size: 102400,
url: 'https://cdn.example.com/hero.webp',
thumbnails: {
small: '...thumbnail-300.webp',
medium: '...thumbnail-600.webp',
},
dimensions: { width: 1920, height: 1080 },
};SEO Configuration
const seo: SEOMetadata = {
metaTitle: 'Page Title | Site Name',
metaDescription: 'Description under 160 chars',
canonicalUrl: 'https://example.com/page',
ogImage: 'https://example.com/og.jpg',
schema: {
'@context': 'https://schema.org',
'@type': 'Article',
// ...
},
};Workflow Example
const workflow: WorkflowConfig = {
name: 'Editorial',
stages: [
{ id: 'draft', name: 'Draft', status: 'draft', permissions: ['author'] },
{ id: 'review', name: 'Review', status: 'pending_review', permissions: ['editor'] },
{ id: 'publish', name: 'Published', status: 'published', permissions: ['admin'] },
],
transitions: [
{ from: 'draft', to: 'review' },
{ from: 'review', to: 'draft' },
{ from: 'review', to: 'publish' },
],
};Customization
Add Custom Content Type
// Extend ContentType
type ContentType =
| 'article'
// ... existing types
| 'recipe'; // Custom type
// Add field definitions
const recipeFields: FieldDefinition[] = [
{ name: 'ingredients', type: 'richtext', label: 'Ingredients', required: true },
{ name: 'cookTime', type: 'number', label: 'Cook Time (min)', required: true },
];Add Custom Field
interface ContentMetadata {
// ... existing fields
customFields: {
featured: boolean;
priority: number;
externalId?: string;
};
}Related skills
Marketing & SEOcontent