How I Integrated Headless CMS in my NextJS App

Judy@webdecoded
4 min readJan 8, 2025

--

While building my portfolio website, I decided to include a blog section. Since I already had an existing Next.js app, I wanted the blog functionality to be integrated within it rather than using a separate platform. This is somewhat ironic since you’re reading this on Medium, but I plan to explore SEO opportunities for my self-hosted blog:)

For the headless CMS, I chose Payload CMS because it’s open-source and offers all the features I needed.

Step 1: Installing Dependencies

npm i payload @payloadcms/next @payloadcms/richtext-lexical sharp graphql --legacy-peer-deps

This is the list of packages you’d need to run the Payload in your application, and you can install them with other package managers like pnpm.

Step 2: Picking a Database

Next, we need to install a database adapter package. I chose Postgres as my database, which requires this installation command:

npm i @payloadcms/db-postgres --legacy-peer-deps

If you already have a hosted database, you can use its URI to connect to Payload. If not, you can easily create and host one on Vercel. From the dashboard, add a new store:

Vercel Dashboard

The system will generate a Database URI in different formats. Select the psql tab and copy the value in quotes, which begins with "postgres://…"

After copying this value, I created an .env file in my repository’s root directory with the following variables:

DATABASE_URI="postgres://...." // <- paste your database URI here
PAYLOAD_SECRET=""

For PAYLOAD_SECRET , I generated it using a command that I ran in my terminal:

openssl rand -base64 32

This command outputs the value that can be pasted in the .env file.

Step 3: Organizing Files

It’s best to separate app’s other logic from that of payload and this is proposed structure from payload templates:

app/
├─ (payload)/
├── // Payload files
├─ (app)/
├── // Your app files

So for this step, I moved all the files I had in /app folder in the newly created folder /(landing-page) . You can name the folder containing non-CMS related logic anything you want — since it’s placed inside parentheses, it won’t appear in your routes.
As for the (payload) folder, I copied the files from Payload Blank Template while converting them to JavaScript as I am not using TypeScript in the portfolio project.

Step 4: Updating NextJS Config

For configuration, I modified the root next.config.mjs file to incorporate the Payload plugin:

import { withPayload } from "@payloadcms/next/withPayload";
/** @type {import('next').NextConfig} */
const nextConfig = {
// Your Next.js config here
experimental: {
reactCompiler: false,
},
};
// Make sure you wrap your `nextConfig`
// with the `withPayload` plugin
export default withPayload(nextConfig);

If yours doesn’t have a .mjs file extension that you can add

“type”: “module” to your package.json

Step 5: Creating Payload Config

Collections provide a way to organize CMS data. I created a separate folder for collections in my app directory and added a new file to define the Posts collection:

// /app/collections/Posts.js
export const Posts = {
slug: "posts",
fields: [
{
name: "title",
type: "text",
required: true,
},
{
name: "content",
type: "richText",
},
{
name: "includedInBlog",
type: "checkbox",
defaultValue: true,
},
],
};

In the root of my project, I created a new file payload.config.js and added configurations that set up the rich text editor, use my environment variables, and create a Posts Collection — the data type I’ll use to store my blog posts.

import sharp from "sharp";
import { lexicalEditor } from "@payloadcms/richtext-lexical";
import { postgresAdapter } from "@payloadcms/db-postgres";
import { buildConfig } from "payload";
import { Posts } from "./src/app/collections/Posts"; // or ./app/collections/Posts if you are not using `src` folder
export default buildConfig({
// If you'd like to use Rich Text, pass your editor here
editor: lexicalEditor(),
// Define and configure your collections in this array
collections: [Posts],
// Your Payload secret - should be a complex and secure string, unguessable
secret: process.env.PAYLOAD_SECRET || "",
// Whichever Database Adapter you're using should go here
// Mongoose is shown as an example, but you can also use Postgres
db: postgresAdapter({
pool: {
connectionString: process.env.DATABASE_URI || "",
},
}),
// If you want to resize images, crop, set focal point, etc.
// make sure to install it and pass it to the config.
// This is optional - if you don't need to do these things,
// you don't need it!
sharp,
});

and modified my jsonconfig.json file to include payload configuration:

{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"],
"@payload-config": [
"./payload.config.js"
]
}
}
}

Step 6: Login

At this stage, I can create a new user by navigating to http://localhost:3000/admin and start creating Posts!

Admin Dashboard

If you’d like to check out source code, here is the repository:
https://github.com/webdecoded-tutorials/nextjs-blog-portfolio

Video tutorial(also includes building the portfolio):

Follow me on X:

https://x.com/webdecoded_g

--

--

Judy@webdecoded
Judy@webdecoded

Written by Judy@webdecoded

Software Engineer | YouTuber | Web3 Enthusiast

No responses yet