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:

  1. Content Storage and Management: It focuses solely on creating, managing, and storing content.
  2. API-Driven: Content is delivered via APIs (RESTful or GraphQL) to any front-end or device.
  3. Front-End Agnostic: You can use any technology to build the front-end (e.g., React, Angular, Vue.js, or native mobile apps).
  4. 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

  1. Install MongoDB support
> npm i @payloadcms/db-mongodb@latest
  1. 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

  1. Install Postgres support
> npm i @payloadcms/db-postgres@latest
  1. 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

  1. Add NEXT_PUBLIC_SERVER_URL or VERCEL_PROJECT_PRODUCTION_URL to your .env file.
  2. 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`],
  },
}
  1. 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

Reference URL

AWS S3

Reference URL

Azure Blob Storage

Reference URL

Google Cloud Storage

Reference URL


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.

Previous
NVM