JS
Headless CMS
What is Headless CMS?
A headless CMS (Content Management System) is a backend-only content management system that provides content storage and delivery without a front-end or presentation layer. The term “headless” refers to the fact that the “head” (the front-end) is separated from the “body” (the backend where content is managed).
Key Features of a Headless CMS:
- Content Storage and Management: It focuses solely on creating, managing, and storing content.
- API-Driven: Content is delivered via APIs (RESTful or GraphQL) to any front-end or device.
- Front-End Agnostic: You can use any technology to build the front-end (e.g., React, Angular, Vue.js, or native mobile apps).
- Scalability and Flexibility: Content can be reused and distributed across multiple channels, such as websites, apps, IoT devices, and more.
Create Project
> npx create-payload-app@latest -t website
# or
> npx create-payload-app@latest -t blank
Environment Variables
- DATABASE_URI: The connection string for your database.
- VERCEL_PROJECT_PRODUCTION_URL: domain without protocol (e.g., example.com).
Examples:
DATABASE_URI=mongodb://localhost:27017/my-database
#DATABASE_URI=postgresql://127.0.0.1:5432/payloadcms
PAYLOAD_SECRET=your-secret-key
DB Connection
MongoDB Reference URL
- Install MongoDB support
> npm i @payloadcms/db-mongodb@latest
- Configure the Mongoose adapter
import {mongooseAdapter} from '@payloadcms/db-mongodb'
export default buildConfig({
// Configure the Mongoose adapter here
db: mongooseAdapter({
// Mongoose-specific arguments go here.
// URL is required.
url: process.env.DATABASE_URI,
}),
})
Postgres Reference URL
- Install Postgres support
> npm i @payloadcms/db-postgres@latest
- Configure the Postgres adapter
import {postgresAdapter} from '@payloadcms/db-postgres'
export default buildConfig({
// Configure the PostgreSQL adapter here
db: postgresAdapter({
pool: {
connectionString: process.env.DATABASE_URI || '',
},
}),
})
Tailwind CSS Styling
Follow the instruction from Tailwind CSS documentation to install Tailwind CSS in your project.
> pnpm add tailwindcss@latest postcss@latest autoprefixer@latest
> pnpx tailwindcss init -p
Update tailwind.config.js
file:
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./src/**/*.{js,ts,jsx,tsx,mdx}'
],
theme: {
extend: {},
},
plugins: [],
}
Leave the postcss.config.js file as default.
Create a folder in src
called styles
and add a global.css
file:
@tailwind base;
@tailwind components;
@tailwind utilities;
Root Next.js Routing
Follow the Next.js migration instruction and create a file called layout.tsx
in the src
folder:
import '@/styles/global.css'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Payload Blank Example',
description: 'A blank example using Payload CMS'
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang={'en'}>
<body>
<main>{children}</main>
</body>
</html>
)
}
Nginx Reverse Proxy
server {
listen 443 ssl;
server_name your_domain.com www.your_domain.com;
# Increase the buffer size for large requests
client_max_body_size 5M;
# Root directory (optional for serving static files)
root /data/www/your_payload_project;
index index.html;
location /_next/static {
alias /data/www/your_payload_project/.next/static;
add_header Cache-Control "public, max-age=3600, immutable";
}
location / {
try_files $uri.html $uri/index.html # only serve html files from this dir
@public
@nextjs;
add_header Cache-Control "public, max-age=3600";
}
location @public {
add_header Cache-Control "public, max-age=3600";
}
# Proxy pass to PM2-managed Next.js
location @nextjs {
# reverse proxy for next server
proxy_pass http://localhost:3000; #Remember to change the port if you are using a different port
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
#proxy_set_header X-Forwarded-Proto $scheme;
#proxy_set_header X-Real-IP $remote_addr;
# Handle compression
#proxy_set_header Accept-Encoding gzip;
}
ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
# Error pages (optional)
error_page 404 /404.html;
# Logging
access_log /data/log/nginx/your_domain_access.log;
error_log /data/log/nginx/your_domain_error.log;
}
server {
listen 80;
listen [::]:80;
server_name your_domain.com www.your_domain.com;
if ($host = www.your_domain.com) {
return 301 https://$host$request_uri;
}
if ($host = your_domain.com) {
return 301 https://$host$request_uri;
}
return 404;
}
Your First Collection
Create a Collection
Find the collections folder in your project src/collections
and create a new file Symbol.ts
with the following content:
import type { CollectionConfig } from 'payload'
import { anyone } from '../access/anyone'
import { authenticated } from '../access/authenticated'
import { slugField } from '@/fields/slug'
export const Symbol: CollectionConfig = {
slug: 'symbols',
access: {
create: authenticated,
delete: authenticated,
read: anyone,
update: authenticated,
},
admin: {
useAsTitle: 'name',
defaultColumns: ['name', 'kicad', 'designator'],
},
fields: [
{
name: 'name',
type: 'text',
required: true,
},
{
name: 'description',
type: 'textarea',
label: 'Description',
},
{
name: 'symbol',
type: 'text',
label: 'Symbol',
},
...slugField(),
],
}
Next, find the payload.config.ts
file in the src
of your project and import the collection you just created:
import { Symbol } from './collections/Symbol'
export default buildConfig({
...,
collections: [Pages, Posts, Media, Categories, Users, Symbol],
...,
})
Reload your dashboard and you should see a new Pages collection. You can now create, edit, and delete pages.
Migration
Migration
Payload provides a migration system that allows you to manage changes to your database schema over time. This is useful for making changes to your database schema in a controlled manner, and for ensuring that all instances of your application are running the same version of the database schema.
Migration Files
Payload stores all created migrations in a folder that you can specify. By default, migrations are stored in ./src/migrations
.
A migration file has two exports - an up function, which is called when a migration is executed, and a down function that will be called if for some reason the migration fails to complete successfully. The up function should contain all changes that you attempt to make within the migration, and the down should ideally revert any changes you make.
import {MigrateUpArgs, MigrateDownArgs} from '@payloadcms/your-db-adapter'
export async function up({payload, req}: MigrateUpArgs): Promise<void> {
// Perform changes to your database here.
// You have access to `payload` as an argument, and
// everything is done in TypeScript.
}
export async function down({payload, req}: MigrateDownArgs): Promise<void> {
// Do whatever you need to revert changes if the `up` function fails
}
Email Configuration
NodeMailer
> pnpm add @payloadcms/email-nodemailer@latest
Modify your .env
file to include the following:
SMTP_HOST=smtp.your_email_host.com
SMTP_USER=your_email@your_domain.com
SMTP_PASSWORD=your_email_password
Add the following to your payload.config.ts
file:
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
export default buildConfig({
...,
email: nodemailerAdapter({
defaultFromAddress: 'info@gerbergpt.com',
defaultFromName: 'GerberGPT',
// Nodemailer transportOptions
transportOptions: {
host: process.env.SMTP_HOST,
port: 587,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
},
}),
...,
})
Sitemap
Install
> pnpm add next-sitemap@latest
Configuration
- Add
NEXT_PUBLIC_SERVER_URL
orVERCEL_PROJECT_PRODUCTION_URL
to your.env
file. - Create a
next-sitemap.config.cjs
file in the root of your project:
const SITE_URL =
process.env.NEXT_PUBLIC_SERVER_URL ||
process.env.VERCEL_PROJECT_PRODUCTION_URL ||
'https://example.com'
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: SITE_URL,
generateRobotsTxt: true,
exclude: ['/posts-sitemap.xml', '/pages-sitemap.xml', '/*', '/posts/*'],
robotsTxtOptions: {
policies: [
{
userAgent: '*',
disallow: '/admin/*',
},
],
additionalSitemaps: [`${SITE_URL}/pages-sitemap.xml`, `${SITE_URL}/posts-sitemap.xml`],
},
}
- Add the following to your
package.json
file:
{
"scripts": {
"postbuild": "next-sitemap --config next-sitemap.config.cjs"
}
}
Commands
Migrate
> npm run payload migrate
Create
> npm run payload migrate:create optional-name-here
Status
> npm run payload migrate:status
> npm run payload migrate:create initial
Login
http://localhost:3000/admin
File Storage
Vercel Blob Storage
AWS S3
Azure Blob Storage
Google Cloud Storage
Preview
Deploying to Vercel
Troubleshooting
TypeError: (0 , s.isRedirectError) is not a function
AWS S3 load image error /_next/image 500 Internal Server Error
sh: 1: cross-env: Permission denied
chmod +x ./node_modules/.bin/cross-env
[ Server ] Error: Exceeded max identifier length for table or enum name of 63 characters.