How I Integrated Headless CMS in my NextJS App
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:
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!
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: