Files
QwenClaw-with-Auth/skills/payloadcms-cms/SKILL.md

16 KiB

PayloadCMS Skill for QwenClaw

Overview

This skill provides PayloadCMS expertise to QwenClaw, enabling it to build, configure, and extend Payload CMS projects. Payload is an open-source, fullstack Next.js framework that gives you instant backend superpowers.

Source: https://github.com/payloadcms/payload


What is PayloadCMS?

Payload is a Next.js native CMS that can be installed directly in your existing /app folder. It provides:

  • Full TypeScript backend and admin panel instantly
  • Server components to extend Payload UI
  • Direct database queries in server components (no REST/GraphQL needed)
  • Automatic TypeScript types for your data

Key Features

  • Next.js Native - Runs inside your /app folder
  • TypeScript First - Automatic type generation
  • Authentication - Built-in auth out of the box
  • Versions & Drafts - Content versioning support
  • Localization - Multi-language content
  • Block-Based Layout - Visual page builder
  • Customizable Admin - React-based admin panel
  • Lexical Editor - Modern rich text editor
  • Access Control - Granular permissions
  • Hooks - Document and field-level hooks
  • High Performance - Optimized API
  • Security - HTTP-only cookies, CSRF protection

Installation

Create New Project

# Basic installation
pnpx create-payload-app@latest

# With website template (recommended)
pnpx create-payload-app@latest -t website

# From example
npx create-payload-app --example example_name

Project Structure

my-payload-app/
├── app/
│   ├── (payload)/
│   │   ├── admin/
│   │   ├── api/
│   │   └── layout.tsx
│   └── (frontend)/
│       ├── page.tsx
│       └── layout.tsx
├── collections/
│   ├── Users.ts
│   └── Posts.ts
├── payload.config.ts
├── payload-types.ts
└── package.json

Collections & Schemas

Basic Collection

// collections/Posts.ts
import type { CollectionConfig } from 'payload';

export const Posts: CollectionConfig = {
  slug: 'posts',
  admin: {
    useAsTitle: 'title',
  },
  access: {
    read: () => true,
  },
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
    },
    {
      name: 'content',
      type: 'richText',
      required: true,
    },
    {
      name: 'publishedAt',
      type: 'date',
      admin: {
        date: {
          pickerAppearance: 'dayAndTime',
        },
      },
    },
    {
      name: 'status',
      type: 'select',
      options: [
        { label: 'Draft', value: 'draft' },
        { label: 'Published', value: 'published' },
      ],
      defaultValue: 'draft',
    },
  ],
};

Collection with Relationships

// collections/Authors.ts
export const Authors: CollectionConfig = {
  slug: 'authors',
  admin: {
    useAsTitle: 'name',
  },
  fields: [
    {
      name: 'name',
      type: 'text',
      required: true,
    },
    {
      name: 'email',
      type: 'email',
      required: true,
    },
    {
      name: 'avatar',
      type: 'upload',
      relationTo: 'media',
    },
  ],
};

// In Posts collection
{
  name: 'author',
  type: 'relationship',
  relationTo: 'authors',
}

Blocks Field (Page Builder)

{
  name: 'layout',
  type: 'blocks',
  blocks: [
    {
      slug: 'hero',
      fields: [
        {
          name: 'title',
          type: 'text',
        },
        {
          name: 'subtitle',
          type: 'text',
        },
        {
          name: 'backgroundImage',
          type: 'upload',
          relationTo: 'media',
        },
      ],
    },
    {
      slug: 'content',
      fields: [
        {
          name: 'content',
          type: 'richText',
        },
      ],
    },
    {
      slug: 'cta',
      fields: [
        {
          name: 'title',
          type: 'text',
        },
        {
          name: 'buttonText',
          type: 'text',
        },
        {
          name: 'buttonUrl',
          type: 'text',
        },
      ],
    },
  ],
}

Access Control

Field-Level Access

{
  name: 'isFeatured',
  type: 'checkbox',
  access: {
    read: () => true,
    update: ({ req }) => req.user?.role === 'admin',
  },
}

Collection Access

access: {
  read: () => true,
  create: ({ req }) => req.user?.role === 'admin',
  update: ({ req }) => req.user?.role === 'admin',
  delete: ({ req }) => req.user?.role === 'admin',
}

Hooks

Before Change Hook

{
  name: 'slug',
  type: 'text',
  hooks: {
    beforeChange: [
      ({ value, data }) => {
        if (!value && data.title) {
          return data.title
            .toLowerCase()
            .replace(/[^a-z0-9]+/g, '-')
            .replace(/(^-|-$)/g, '');
        }
        return value;
      },
    ],
  },
}

After Change Hook

hooks: {
  afterChange: [
    async ({ doc, req, operation }) => {
      if (operation === 'create') {
        // Send welcome email
        await sendWelcomeEmail(doc.email);
      }
      return doc;
    },
  ],
}

Localization

const config: Config = {
  localization: {
    locales: [
      { code: 'en', label: 'English' },
      { code: 'es', label: 'Spanish' },
      { code: 'fr', label: 'French' },
    ],
    defaultLocale: 'en',
  },
  // ...
};

Using Payload in Server Components

Query Collection

import { getPayload } from 'payload';
import config from '@payload-config';

export default async function PostsPage() {
  const payload = await getPayload({ config });
  
  const posts = await payload.find({
    collection: 'posts',
    where: {
      status: { equals: 'published' },
    },
    sort: '-publishedAt',
    limit: 10,
  });

  return (
    <div>
      {posts.docs.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </article>
      ))}
    </div>
  );
}

Query by ID

const post = await payload.findByID({
  collection: 'posts',
  id: postId,
  depth: 2, // Populate relationships
});

VPS Landing Page with PayloadCMS

Collections for VPS Site

// collections/VpsPlans.ts
export const VpsPlans: CollectionConfig = {
  slug: 'vps-plans',
  admin: {
    useAsTitle: 'name',
  },
  access: {
    read: () => true,
  },
  fields: [
    {
      name: 'name',
      type: 'text',
      required: true,
    },
    {
      name: 'price',
      type: 'number',
      required: true,
    },
    {
      name: 'billingPeriod',
      type: 'select',
      options: ['monthly', 'yearly'],
      defaultValue: 'monthly',
    },
    {
      name: 'vcpuCores',
      type: 'number',
      required: true,
    },
    {
      name: 'ram',
      type: 'number',
      label: 'RAM (GB)',
      required: true,
    },
    {
      name: 'storage',
      type: 'number',
      label: 'Storage (GB)',
      required: true,
    },
    {
      name: 'bandwidth',
      type: 'text',
      label: 'Bandwidth',
      required: true,
    },
    {
      name: 'features',
      type: 'array',
      fields: [
        {
          name: 'feature',
          type: 'text',
        },
      ],
    },
    {
      name: 'isPopular',
      type: 'checkbox',
      defaultValue: false,
    },
    {
      name: 'ctaText',
      type: 'text',
      defaultValue: 'Get Started',
    },
  ],
};

// collections/Features.ts
export const Features: CollectionConfig = {
  slug: 'features',
  admin: {
    useAsTitle: 'title',
  },
  access: {
    read: () => true,
  },
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
    },
    {
      name: 'description',
      type: 'richText',
      required: true,
    },
    {
      name: 'icon',
      type: 'text',
      label: 'Icon Name (Bootstrap Icons)',
    },
    {
      name: 'order',
      type: 'number',
      admin: {
        position: 'sidebar',
      },
    },
  ],
};

// collections/Testimonials.ts
export const Testimonials: CollectionConfig = {
  slug: 'testimonials',
  admin: {
    useAsTitle: 'authorName',
  },
  access: {
    read: () => true,
  },
  fields: [
    {
      name: 'authorName',
      type: 'text',
      required: true,
    },
    {
      name: 'authorTitle',
      type: 'text',
    },
    {
      name: 'company',
      type: 'text',
    },
    {
      name: 'quote',
      type: 'richText',
      required: true,
    },
    {
      name: 'avatar',
      type: 'upload',
      relationTo: 'media',
    },
    {
      name: 'rating',
      type: 'number',
      min: 1,
      max: 5,
    },
  ],
};

Frontend Page Component

// app/(frontend)/page.tsx
import { getPayload } from 'payload';
import config from '@payload-config';
import VpsPricing from './components/VpsPricing';
import FeaturesGrid from './components/FeaturesGrid';

export default async function HomePage() {
  const payload = await getPayload({ config });

  const plans = await payload.find({
    collection: 'vps-plans',
    sort: 'price',
  });

  const features = await payload.find({
    collection: 'features',
    sort: 'order',
  });

  return (
    <main>
      <HeroSection />
      <FeaturesGrid features={features.docs} />
      <VpsPricing plans={plans.docs} />
      <CtaSection />
    </main>
  );
}

Pricing Component

// app/(frontend)/components/VpsPricing.tsx
import type { VpsPlan } from '@payload-types';

interface VpsPricingProps {
  plans: VpsPlan[];
}

export default function VpsPricing({ plans }: VpsPricingProps) {
  return (
    <section className="py-20 bg-dark">
      <div className="container">
        <div className="text-center mb-16">
          <h2 className="text-4xl font-bold mb-4">
            Simple, Transparent Pricing
          </h2>
          <p className="text-gray-400 max-w-2xl mx-auto">
            No hidden fees. Pay only for what you use.
          </p>
        </div>

        <div className="grid md:grid-cols-3 gap-8">
          {plans.map((plan) => (
            <div
              key={plan.id}
              className={`p-8 rounded-2xl ${
                plan.isPopular
                  ? 'bg-dark-light border-primary border-2'
                  : 'bg-dark-light border border-gray-800'
              }`}
            >
              {plan.isPopular && (
                <span className="bg-primary text-white px-3 py-1 rounded-full text-sm font-medium">
                  Most Popular
                </span>
              )}

              <h3 className="text-xl font-semibold mt-4 mb-2">
                {plan.name}
              </h3>

              <div className="text-4xl font-bold mb-2">
                ${plan.price}
                <span className="text-sm text-gray-400 font-normal">
                  /{plan.billingPeriod}
                </span>
              </div>

              <ul className="space-y-3 my-6">
                <li className="flex items-center gap-2">
                  <i className="bi bi-check-circle-fill text-primary" />
                  {plan.vcpuCores} vCPU Cores
                </li>
                <li className="flex items-center gap-2">
                  <i className="bi bi-check-circle-fill text-primary" />
                  {plan.ram} GB RAM
                </li>
                <li className="flex items-center gap-2">
                  <i className="bi bi-check-circle-fill text-primary" />
                  {plan.storage} GB NVMe Storage
                </li>
                <li className="flex items-center gap-2">
                  <i className="bi bi-check-circle-fill text-primary" />
                  {plan.bandwidth} Bandwidth
                </li>
                {plan.features?.map((feature, idx) => (
                  <li key={idx} className="flex items-center gap-2">
                    <i className="bi bi-check-circle-fill text-primary" />
                    {feature.feature}
                  </li>
                ))}
              </ul>

              <button
                className={`w-full py-3 rounded-lg font-semibold ${
                  plan.isPopular
                    ? 'bg-primary text-white'
                    : 'border-2 border-gray-600 text-white'
                }`}
              >
                {plan.ctaText}
              </button>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

Payload Config Example

// payload.config.ts
import { buildConfig } from 'payload';
import { lexicalEditor } from '@payloadcms/richtext-lexical';
import { mongooseAdapter } from '@payloadcms/db-mongodb';
import { nodemailerAdapter } from '@payloadcms/email-nodemailer';
import path from 'path';
import { fileURLToPath } from 'url';

import { Users } from './collections/Users';
import { VpsPlans } from './collections/VpsPlans';
import { Features } from './collections/Features';
import { Media } from './collections/Media';

const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);

export default buildConfig({
  admin: {
    user: Users.slug,
    importMap: {
      baseDir: path.resolve(dirname),
    },
  },
  collections: [
    Users,
    VpsPlans,
    Features,
    Media,
  ],
  editor: lexicalEditor(),
  db: mongooseAdapter({
    url: process.env.DATABASE_URI || '',
  }),
  email: nodemailerAdapter({
    defaultFromAddress: 'noreply@example.com',
    defaultFromName: 'CloudVPS',
  }),
  typescript: {
    outputFile: path.resolve(dirname, 'payload-types.ts'),
  },
});

Usage in QwenClaw

Basic Payload Project Creation

Use the payloadcms-cms skill to create a new PayloadCMS project for a VPS hosting landing page with:
- VPS plans collection (name, price, specs, features)
- Features collection (title, description, icon)
- Testimonials collection
- Media library for images

Query Payload Data

Use payloadcms-cms to query all VPS plans sorted by price and display them in a pricing table

Create Collection

Use payloadcms-cms skill to create a new collection for data centers with:
- name (text)
- location (text)
- region (select: US, EU, Asia)
- features (array: DDoS protection, NVMe, 10Gbps)
- latitude/longitude for map

Best Practices

1. Type Safety

// Always import generated types
import type { Post, User } from '@payload-types';

2. Depth for Relationships

// Use depth to populate relationships
const post = await payload.findByID({
  collection: 'posts',
  id: postId,
  depth: 2,
});

3. Access Control

// Always define access control
access: {
  read: () => true,
  create: ({ req }) => !!req.user,
  update: ({ req }) => !!req.user,
}

4. Validation

// Add validation to fields
{
  name: 'email',
  type: 'email',
  required: true,
  validate: (value) => {
    if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      return 'Invalid email format';
    }
    return true;
  },
}

Resources

Official

Templates

  • Website: pnpx create-payload-app@latest -t website
  • E-commerce: pnpx create-payload-app@latest -t ecommerce
  • Blank: pnpx create-payload-app@latest -t blank

Skill Metadata

name: payloadcms-cms
version: 1.0.0
category: development
description: PayloadCMS project creation, configuration, and development
author: PayloadCMS Team (https://github.com/payloadcms)
license: MIT
tags:
  - cms
  - nextjs
  - typescript
  - react
  - backend
  - admin-panel

Skill ready for QwenClaw integration! 🚀