Awesome-omni-skill payload
Builds full-stack applications with Payload CMS, the Next.js-native headless CMS. Use when creating content-driven apps with TypeScript, code-first configuration, and full control over your backend.
install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/payload-majiayu000" ~/.claude/skills/diegosouzapw-awesome-omni-skill-payload && rm -rf "$T"
manifest:
skills/development/payload-majiayu000/SKILL.mdsource content
Payload CMS
Open-source, Next.js-native headless CMS with full TypeScript support. Code-first configuration, self-hosted, with REST and GraphQL APIs.
Quick Start
npx create-payload-app@latest my-app cd my-app npm run dev
Opens admin at
http://localhost:3000/admin.
Project Structure
my-app/ app/ # Next.js app directory (payload)/ # Payload admin routes collections/ # Content type definitions payload.config.ts # Main configuration payload-types.ts # Generated types
Configuration
// payload.config.ts import { buildConfig } from 'payload'; import { mongooseAdapter } from '@payloadcms/db-mongodb'; // or: import { postgresAdapter } from '@payloadcms/db-postgres'; import { Posts } from './collections/Posts'; import { Users } from './collections/Users'; import { Media } from './collections/Media'; export default buildConfig({ admin: { user: Users.slug, }, collections: [Users, Posts, Media], db: mongooseAdapter({ url: process.env.MONGODB_URI!, }), typescript: { outputFile: 'payload-types.ts', }, secret: process.env.PAYLOAD_SECRET!, });
Collections (Content Types)
// collections/Posts.ts import { CollectionConfig } from 'payload'; export const Posts: CollectionConfig = { slug: 'posts', admin: { useAsTitle: 'title', defaultColumns: ['title', 'status', 'publishedAt'], }, access: { read: () => true, // Public read create: ({ req }) => !!req.user, // Authenticated only update: ({ req }) => !!req.user, delete: ({ req }) => !!req.user, }, versions: { drafts: true, }, fields: [ { name: 'title', type: 'text', required: true, }, { name: 'slug', type: 'text', unique: true, admin: { position: 'sidebar', }, hooks: { beforeValidate: [ ({ value, data }) => value || data?.title?.toLowerCase().replace(/\s+/g, '-'), ], }, }, { name: 'author', type: 'relationship', relationTo: 'users', }, { name: 'publishedAt', type: 'date', admin: { position: 'sidebar', }, }, { name: 'status', type: 'select', options: [ { label: 'Draft', value: 'draft' }, { label: 'Published', value: 'published' }, ], defaultValue: 'draft', admin: { position: 'sidebar', }, }, { name: 'content', type: 'richText', }, { name: 'featuredImage', type: 'upload', relationTo: 'media', }, { name: 'categories', type: 'relationship', relationTo: 'categories', hasMany: true, }, ], };
Field Types
// Text { name: 'title', type: 'text', required: true } // Textarea { name: 'excerpt', type: 'textarea' } // Rich Text (Lexical) { name: 'content', type: 'richText' } // Number { name: 'price', type: 'number', min: 0 } // Email { name: 'email', type: 'email' } // Date { name: 'publishedAt', type: 'date' } // Checkbox { name: 'featured', type: 'checkbox', defaultValue: false } // Select { name: 'status', type: 'select', options: [ { label: 'Draft', value: 'draft' }, { label: 'Published', value: 'published' }, ], } // Radio { name: 'type', type: 'radio', options: ['video', 'article', 'podcast'], } // Relationship { name: 'author', type: 'relationship', relationTo: 'users', } // Upload { name: 'image', type: 'upload', relationTo: 'media', } // Array (repeatable) { name: 'gallery', type: 'array', fields: [ { name: 'image', type: 'upload', relationTo: 'media' }, { name: 'caption', type: 'text' }, ], } // Group (nested object) { name: 'meta', type: 'group', fields: [ { name: 'title', type: 'text' }, { name: 'description', type: 'textarea' }, ], } // Blocks (flexible content) { name: 'layout', type: 'blocks', blocks: [HeroBlock, ContentBlock, CTABlock], }
Blocks
// blocks/Hero.ts import { Block } from 'payload'; export const HeroBlock: Block = { slug: 'hero', labels: { singular: 'Hero', plural: 'Heroes', }, fields: [ { name: 'heading', type: 'text', required: true }, { name: 'subheading', type: 'text' }, { name: 'image', type: 'upload', relationTo: 'media' }, { name: 'cta', type: 'group', fields: [ { name: 'label', type: 'text' }, { name: 'link', type: 'text' }, ], }, ], };
Access Control
export const Posts: CollectionConfig = { slug: 'posts', access: { // Function-based access read: ({ req }) => { // Published posts are public if (!req.user) { return { status: { equals: 'published' } }; } // Logged in users see all return true; }, create: ({ req }) => !!req.user, update: ({ req }) => { if (!req.user) return false; // Admins can update all if (req.user.role === 'admin') return true; // Authors can only update own posts return { author: { equals: req.user.id }, }; }, delete: ({ req }) => req.user?.role === 'admin', }, };
Hooks
export const Posts: CollectionConfig = { slug: 'posts', hooks: { beforeChange: [ async ({ data, req, operation }) => { if (operation === 'create') { data.author = req.user?.id; } return data; }, ], afterChange: [ async ({ doc, operation }) => { if (operation === 'create') { // Send notification, revalidate cache, etc. await revalidatePath('/posts'); } }, ], beforeRead: [ async ({ doc, req }) => { // Transform document before returning return doc; }, ], }, fields: [/* ... */], };
REST API
Auto-generated at
/api/{collection}.
// Get all posts const response = await fetch('/api/posts'); const { docs, totalDocs, page, limit } = await response.json(); // Get single post const post = await fetch('/api/posts/123').then(r => r.json()); // Query with parameters const params = new URLSearchParams({ where: JSON.stringify({ status: { equals: 'published' }, publishedAt: { less_than: new Date().toISOString() }, }), sort: '-publishedAt', limit: '10', page: '1', depth: '2', }); const filtered = await fetch(`/api/posts?${params}`).then(r => r.json());
Query Operators
// Equals { field: { equals: 'value' } } // Not equals { field: { not_equals: 'value' } } // Greater/less than { field: { greater_than: 100 } } { field: { less_than: 100 } } { field: { greater_than_equal: 100 } } { field: { less_than_equal: 100 } } // Contains (string) { field: { contains: 'text' } } // In array { field: { in: ['a', 'b', 'c'] } } // Not in array { field: { not_in: ['x', 'y'] } } // Exists { field: { exists: true } } // Logical operators { or: [ { status: { equals: 'published' } }, { featured: { equals: true } }, ], } { and: [ { status: { equals: 'published' } }, { category: { equals: 'tech' } }, ], }
GraphQL API
Available at
/api/graphql.
query { Posts( where: { status: { equals: published } } sort: "-publishedAt" limit: 10 ) { docs { id title slug author { name } } totalDocs } }
Local API (Server-Side)
Use in Server Components, API routes, or hooks.
import { getPayload } from 'payload'; import config from '@payload-config'; const payload = await getPayload({ config }); // Find many const posts = await payload.find({ collection: 'posts', where: { status: { equals: 'published' }, }, sort: '-publishedAt', limit: 10, depth: 2, }); // Find one const post = await payload.findByID({ collection: 'posts', id: '123', depth: 2, }); // Create const newPost = await payload.create({ collection: 'posts', data: { title: 'New Post', content: '...', }, }); // Update const updated = await payload.update({ collection: 'posts', id: '123', data: { title: 'Updated Title', }, }); // Delete await payload.delete({ collection: 'posts', id: '123', });
TypeScript
Types are auto-generated to
payload-types.ts.
import { Post, User } from './payload-types'; // Fully typed const posts: Post[] = await payload.find({ collection: 'posts', }); posts.forEach((post: Post) => { console.log(post.title); // TypeScript knows this exists });
Next.js Integration
// app/posts/page.tsx import { getPayload } from 'payload'; import config from '@payload-config'; export default async function PostsPage() { const payload = await getPayload({ config }); const { docs: posts } = await payload.find({ collection: 'posts', where: { status: { equals: 'published' } }, sort: '-publishedAt', }); return ( <ul> {posts.map((post) => ( <li key={post.id}> <a href={`/posts/${post.slug}`}>{post.title}</a> </li> ))} </ul> ); }
// app/posts/[slug]/page.tsx import { getPayload } from 'payload'; import config from '@payload-config'; import { notFound } from 'next/navigation'; export async function generateStaticParams() { const payload = await getPayload({ config }); const { docs } = await payload.find({ collection: 'posts' }); return docs.map((post) => ({ slug: post.slug })); } export default async function PostPage({ params }: { params: { slug: string } }) { const payload = await getPayload({ config }); const { docs } = await payload.find({ collection: 'posts', where: { slug: { equals: params.slug } }, depth: 2, }); if (!docs[0]) notFound(); const post = docs[0]; return ( <article> <h1>{post.title}</h1> {/* Render content */} </article> ); }
Media Collection
// collections/Media.ts import { CollectionConfig } from 'payload'; export const Media: CollectionConfig = { slug: 'media', upload: { staticDir: 'public/media', imageSizes: [ { name: 'thumbnail', width: 400, height: 300, position: 'centre', }, { name: 'card', width: 768, height: 1024, position: 'centre', }, ], mimeTypes: ['image/*'], }, fields: [ { name: 'alt', type: 'text', required: true }, { name: 'caption', type: 'text' }, ], };
Globals (Singletons)
// globals/Settings.ts import { GlobalConfig } from 'payload'; export const Settings: GlobalConfig = { slug: 'settings', access: { read: () => true, }, fields: [ { name: 'siteName', type: 'text', required: true }, { name: 'siteDescription', type: 'textarea' }, { name: 'navigation', type: 'array', fields: [ { name: 'label', type: 'text' }, { name: 'link', type: 'text' }, ], }, ], }; // Usage const settings = await payload.findGlobal({ slug: 'settings' });
Authentication
// Login const user = await payload.login({ collection: 'users', data: { email: 'user@example.com', password: 'password', }, }); // Current user (in hooks/access control) const user = req.user; // Logout await payload.logout({ collection: 'users', });
Deployment
# Build npm run build # Start production npm run start
Deploy to:
- Vercel: One-click with Neon database
- Cloudflare: Workers + D1 + R2
- Self-hosted: Any Node.js host
Best Practices
- Use Local API in Server Components for performance
- Define access control for each collection
- Use hooks for side effects (revalidation, notifications)
- Generate types after schema changes:
npm run generate:types - Use drafts for preview functionality
- Configure image sizes to match your frontend needs