Back to resources

Tech Exploration: Building a blog platform with Payload CMS

Welcome to Tech Exploration, where Ketryon tests cutting-edge tools to power modern solutions. In this edition, we dive into building apps with Payload CMS.

Tech Exploration: Building a blog platform with Payload CMS

Image credit: Photo by @altumcode on Unsplash

Payload CMSNext.jsTypeScriptContent Management
By Kenny TranPublished on 4/5/2025Last updated on 4/5/2025

Introduction

At Ketryon, we’re passionate about tools that empower businesses with tailored, scalable solutions. We explored Payload CMS, a headless CMS and application framework that blends developer control with user-friendly content management. Unlike rigid platforms like WordPress, Payload lets us define content in code, integrating seamlessly with Next.js for websites, apps, and tools. We built a minimal blog platform to test its ecosystem, showcasing its potential for startups and Swedish enterprises seeking flexible, GDPR-compliant content solutions.

What Is Payload CMS?

Payload CMS is an open-source, TypeScript-first headless CMS and application framework built with Node.js, React, and MongoDB or Postgres. Unlike WordPress, which relies on pre-built templates, Payload lets developers define content schemas in code, creating REST and GraphQL APIs and a customizable admin UI. It integrates natively with Next.js, running in the same project for a unified stack. Think of Payload as a backend builder: it’s like assembling a custom database and API with Lego bricks, tailored to your app’s needs, all within a JavaScript/TypeScript workflow. Its MIT license keeps it free for any project.

Key features include:

  • Payload Config: A TypeScript file to define content, authentication, and UI.
  • Collections and Globals: Collections store multiple items (e.g., blog posts), while globals hold single settings (e.g., site title).
  • APIs: Auto-generated REST, GraphQL, and Local APIs for fast data access.
  • Admin UI: A React-based dashboard for editors, customizable with code.
  • Authentication: Built-in user login and roles, secured with JWT.
  • Database Support: Works with MongoDB, Postgres, and more via adapters.

Why It’s Relevant

Payload CMS bridges developer flexibility and content editor usability, unlike traditional CMS platforms that limit customization. Its code-first approach saves time and costs for businesses and developers.

  • For Businesses: Payload’s pre-built APIs and authentication cut backend coding time, reducing costs for a blog or app from weeks to days. For Swedish firms, its role-based access and schema control ensure GDPR-compliant data handling, perfect for e-commerce or SaaS. Companies like Microsoft and ASICS use Payload for scalable content solutions.
  • For Developers: Payload’s TypeScript-first design and Next.js integration fit our JavaScript expertise. Its Local API fetches data instantly within Next.js, speeding up page loads compared to external API calls. X posts call it “a dev’s dream” for its clean data flow.
  • Industry Trend: Payload’s 3.0 release in 2024 boosted its popularity, with thousands of projects adopting it for websites and tools. Its Next.js focus and community growth make it a top choice for modern apps.

Our Test Drive

To dive into Payload CMS’s ecosystem, we built a minimal blog platform using Payload, Next.js, and TypeScript. Our goal was to create a web app where users create posts, manage categories, and access content via a public API, exploring key aspects: schema definition, admin UI customization, API integration, authentication, and deployment. The platform offers a flexible solution for content-driven businesses like blogs or startup websites.

Project Setup

We initialized a Next.js project with Payload

npx create-next-app@latest blog-platform --typescript
cd blog-platform
npm i payload @payloadcms/next @payloadcms/richtext-lexical @payloadcms/db-mongodb mongoose

Note: create-next-app includes Next.js and React dependencies. We added Payload, its Next.js integration, Lexical rich-text editor, and MongoDB adapter. We copied Payload’s template files (payload.config.ts, payload-types.ts) into /src/app/(payload) and connected to a MongoDB Atlas instance. The payload.config.ts set up Payload:

import { buildConfig } from "payload";
import { mongooseAdapter } from "@payloadcms/db-mongodb";
import { lexicalEditor } from "@payloadcms/richtext-lexical";
import { Posts } from "./collections/Posts";
import { Categories } from "./collections/Categories";
import { Users } from "./collections/Users";

export default buildConfig({
  collections: [Posts, Categories, Users],
  editor: lexicalEditor(),
  db: mongooseAdapter({ url: process.env.MONGODB_URI }),
  typescript: { outputFile: "./payload-types.ts" },
});

Running npm run dev launched Next.js and Payload’s admin UI at /admin, letting us test content creation.

Schema Definition

Schemas define the app’s data structure, like a blueprint for content. We created two collections (Posts, Categories) and a Users collection for authentication. The Posts collection (collections/Posts.ts) defined fields for blog posts:

import { CollectionConfig } from "payload";

export const Posts: CollectionConfig = {
  slug: "posts",
  admin: { defaultColumns: ["title", "category", "createdAt"] },
  fields: [
    { name: "title", type: "text", required: true },
    {
      name: "content",
      type: "richText",
      required: true,
      editor: lexicalEditor(),
    },
    {
      name: "category",
      type: "relationship",
      relationTo: "categories",
      required: true,
    },
    { name: "createdAt", type: "date", admin: { readOnly: true } },
  ],
};

The Categories collection was simpler:

import { CollectionConfig } from "payload";

export const Categories: CollectionConfig = {
  slug: "categories",
  fields: [{ name: "name", type: "text", required: true }],
};

Payload generated TypeScript interfaces in payload-types.ts, ensuring type-safe queries and rendering.

Admin UI Customization

The admin UI lets editors manage content without coding. We customized it to enhance the post creation experience, adding a title preview field (collections/Posts.ts):

import { Field } from "payload";

const TitlePreview: Field = {
  name: "titlePreview",
  type: "ui",
  admin: {
    components: {
      Field: ({ value }: { value?: string }) => (
        <div style={{ fontSize: "1.2em", color: "#333" }}>
          Preview: {value || "No title entered"}
        </div>
      ),
    },
  },
};

export const Posts: CollectionConfig = {
  slug: "posts",
  admin: { defaultColumns: ["title", "category", "createdAt"] },
  fields: [
    { name: "title", type: "text", required: true },
    TitlePreview,
    {
      name: "content",
      type: "richText",
      required: true,
      editor: lexicalEditor(),
    },
    {
      name: "category",
      type: "relationship",
      relationTo: "categories",
      required: true,
    },
    { name: "createdAt", type: "date", admin: { readOnly: true } },
  ],
};

The Lexical editor provided rich-text editing, storing content as JSON for flexibility. Our custom field improved the editor’s workflow with a live preview.

API Integration

Payload’s Local API fetches data within Next.js, ideal for fast server-side rendering. In app/page.tsx, we displayed posts:

import { getPayload } from "@payloadcms/next";
import { Post } from "../../payload-types";
import { renderRichText } from "@payloadcms/richtext-lexical";

export default async function Home() {
  const payload = await getPayload();
  const { docs: posts } = await payload.find({
    collection: "posts",
    sort: "-createdAt",
    depth: 1, // Populate category
  });

  return (
    <div style={{ padding: "20px" }}>
      <h1>Blog Platform</h1>
      {posts.map((post: Post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{(post.category as { name: string })?.name}</p>
          <div>{renderRichText(post.content)}</div>
        </article>
      ))}
    </div>
  );
}

We used @payloadcms/richtext-lexical to render the JSON content from the Lexical editor. The depth: 1 option populated category.name. We also tested the REST API (/api/posts) for external access, confirming its versatility.

Authentication

We enabled user authentication for admin and editor roles (collections/Users.ts):

import { CollectionConfig } from "payload";

export const Users: CollectionConfig = {
  slug: "users",
  auth: true,
  fields: [
    { name: "name", type: "text", required: true },
    {
      name: "role",
      type: "select",
      options: ["admin", "editor"],
      required: true,
    },
  ],
};

Payload’s JWT-based login and role-based access worked out of the box. We customized the login page (app/(payload)/admin/page.tsx):

import { Login } from "@payloadcms/next";

export default function AdminLogin() {
  return (
    <div style={{ padding: "40px", textAlign: "center" }}>
      <h1>Ketryon Blog Admin</h1>
      <Login />
    </div>
  );
}

This secured the admin UI, limiting editors to posts and categories.

Learnings

Building the blog platform taught us valuable lessons about Payload CMS’s ecosystem:

  • Schema Definition: Defining collections in TypeScript was intuitive, with payload-types.ts catching errors early, saving us debugging time compared to JSON-based CMS platforms.
  • Admin UI: Customizing the UI with React components, like our title preview, was straightforward and boosted editor productivity, though planning custom fields upfront avoided rework.
  • APIs: The Local API’s speed transformed our Next.js rendering, cutting page load times versus external REST calls. Testing REST and GraphQL APIs showed their flexibility for public-facing apps.
  • Authentication: Payload’s JWT authentication setup was plug-and-play, enabling secure admin access in hours, with role-based controls aligning with GDPR needs for Swedish clients.
  • Deployment: Vercel’s integration simplified hosting, and OTA updates let us tweak the admin UI without downtime, ideal for rapid iteration.

References

  1. https://payloadcms.com/docs/getting-started/what-is-payload
  2. https://payloadcms.com/docs/access-control/overview
  3. https://payloadcms.com/docs/local-api/overview