Token导航 LogoToken导航TokenDH.com
研究检索只读github未标认证来源可访问clear审计通过

content-platforms内容平台

Agent Skill

用于辅助文档、README、Markdown、说明文和内容稿件的整理与改写。它适合让 Agent 提炼结构、补齐章节、统一术语、检查链接或把零散材料整理成可读文档。使用时应保留项目已有事实、命令和路径,不要把未确认的信息写成确定结论;涉及对外文案时,还需要控制语气,避免过度营销或夸大能力。

总安装

618

周安装

26

GitHub Stars

12

下载量

216
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

3

许可证

MIT

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:content-platforms(内容平台)
来源仓库:https://github.com/miles990/claude-software-skills
仓库路径:skills/content-platforms
安装命令:
npx skills add https://github.com/miles990/claude-software-skills --skill content-platforms
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。不同来源提供的安装方式可能略有差异;本站展示可直接复制的安装命令,安装前请核对来源页面。

skills.shnpx skills
npx skills add https://github.com/miles990/claude-software-skills --skill content-platforms

简介

构建内容管理系统与富媒体应用的前端架构设计指南。

  • 提供 Headless CMS schema 定义与字段类型标准化示例。
  • 涵盖博客系统与多语言本地化内容模型设计实践。
  • 安装方式:通过 GitHub 仓库安装,命令为 npx skills add https://github.com/miles990/claude-software-skills --skill content-platforms。
  • 注意:侧重技术实现而非运营策略,适合开发者参考使用。

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

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

04

需要参考平台分布和安装热度时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

补充不同宿主或平台的使用分布数据

能力 5

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

Antigravity

29.7%
按下载量换算64

OpenCode

23.07%
按下载量换算50

Gemini CLI

16.76%
按下载量换算36

Claude Code

12.78%
按下载量换算28

windsurf

7.38%
按下载量换算16

Codex

3.43%
按下载量换算7

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

只读

该 Skill 主要提供规则、说明或参考内容,本身偏只读;真正读写文件、联网或执行命令仍取决于宿主 Agent 的任务。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。

来源信息

继续浏览同类 Skills