MCPInstall

content-platforms

CMS, blogging platforms, and content management patterns

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
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads content-platforms automatically when relevant.

  3. 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

View all →

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

DevOps & Cloud606 installs

game-development

miles990/claude-software-skills

Game development patterns, architectures, and best practices

Media & Creative411 installs

project-management

miles990/claude-software-skills

Open the skill to read its SKILL.md instructions.

389 installs

e-commerce

miles990/claude-software-skills

Open the skill to read its SKILL.md instructions.

376 installs

saas-platforms

miles990/claude-software-skills

Open the skill to read its SKILL.md instructions.

362 installs

Popular other skills

View category →

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.

1.3M installs

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.

1M installs

ask-matt

mattpocock/skills

Ask which skill or flow fits your situation. A router over the skills in this repo.

778.4K installs

to-questionnaire

mattpocock/skills

Turn a decision you can't fully answer into a questionnaire for someone else to fill in.

508.9K installs

wait-what

mattpocock/skills

Stop. That last message did not land: re-pitch it.

459.7K installs

loop-me

mattpocock/skills

Grill me about specs for the workflows I want to build, within this workspace.

447.9K installs