791 lines
16 KiB
Markdown
791 lines
16 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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)
|
|
|
|
```typescript
|
|
{
|
|
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
|
|
|
|
```typescript
|
|
{
|
|
name: 'isFeatured',
|
|
type: 'checkbox',
|
|
access: {
|
|
read: () => true,
|
|
update: ({ req }) => req.user?.role === 'admin',
|
|
},
|
|
}
|
|
```
|
|
|
|
### Collection Access
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
{
|
|
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
|
|
|
|
```typescript
|
|
hooks: {
|
|
afterChange: [
|
|
async ({ doc, req, operation }) => {
|
|
if (operation === 'create') {
|
|
// Send welcome email
|
|
await sendWelcomeEmail(doc.email);
|
|
}
|
|
return doc;
|
|
},
|
|
],
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Localization
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
const post = await payload.findByID({
|
|
collection: 'posts',
|
|
id: postId,
|
|
depth: 2, // Populate relationships
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## VPS Landing Page with PayloadCMS
|
|
|
|
### Collections for VPS Site
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
```typescript
|
|
// Always import generated types
|
|
import type { Post, User } from '@payload-types';
|
|
```
|
|
|
|
### 2. Depth for Relationships
|
|
```typescript
|
|
// Use depth to populate relationships
|
|
const post = await payload.findByID({
|
|
collection: 'posts',
|
|
id: postId,
|
|
depth: 2,
|
|
});
|
|
```
|
|
|
|
### 3. Access Control
|
|
```typescript
|
|
// Always define access control
|
|
access: {
|
|
read: () => true,
|
|
create: ({ req }) => !!req.user,
|
|
update: ({ req }) => !!req.user,
|
|
}
|
|
```
|
|
|
|
### 4. Validation
|
|
```typescript
|
|
// 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
|
|
- **Website**: https://payloadcms.com/
|
|
- **GitHub**: https://github.com/payloadcms/payload
|
|
- **Documentation**: https://payloadcms.com/docs
|
|
- **Examples**: https://github.com/payloadcms/payload/tree/main/examples
|
|
|
|
### 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
|
|
|
|
```yaml
|
|
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!** 🚀
|