--- title: Integrate RetroUI with Payload CMS coverImage: https://pub-5f7cbdfd9ffa4c838e386788f395f0c4.r2.dev/blogs/retroui-with-payload-cms/retroui-with-payload-cms-cover.png description: Using UI libraries can significantly speed up your development. In this article, we will explore the top 5 for your next React project. tags: - react - ui library - top 5 author: name: Arif Hossain avatar: https://pub-5f7cbdfd9ffa4c838e386788f395f0c4.r2.dev/arif.jpg x: ariflogs status: draft publishedAt: 14 Nov, 2025 --- A practical, end‑to‑end guide to wire up **Payload CMS** as your content backend and **RetroUI** as your front‑end component kit—then ship a clean, neo‑brutalist **Blog List** in Next.js. > Works great if your stack is **Next.js + TailwindCSS**. Payload is Next‑native and RetroUI is Tailwind‑first, so they snap together nicely. --- ## What you’ll build * A Payload CMS app with a **Posts** collection * A Next.js front‑end that fetches posts from Payload’s REST API * A `/blog` page that renders a responsive list using RetroUI components (Cards, Badges, Buttons) --- ## Prerequisites * **Node.js 20.9+** (Payload requires Node 20.9 or newer) * **Next.js 15+** for the front‑end (Payload is Next‑native) * Any compatible DB: SQLite (dev), Postgres, or MongoDB > Tip: For local development, SQLite is the quickest path. Switch to Postgres/Mongo when deploying. --- ## Part 1 — Set up Payload CMS You can add Payload to an existing Next.js app or scaffold a new one. We’ll scaffold a dedicated Payload project so its API stays clean and portable. ### 1) Create a new Payload app ```bash npx create-payload-app # Follow the prompts: choose a template (blank or blog), set DB (SQLite for dev), etc. cd pnpm dev # or npm run dev / yarn dev ``` This starts Payload and its Admin UI (usually at `http://localhost:3000/admin`). ### 2) Define a Posts collection Create `src/collections/Posts.ts`: ```ts import { CollectionConfig } from 'payload/types'; const Posts: CollectionConfig = { slug: 'posts', admin: { useAsTitle: 'title', defaultColumns: ['title', 'publishedAt', 'status'], }, access: { read: () => true, // public read access for blog }, fields: [ { name: 'title', type: 'text', required: true, }, { name: 'slug', type: 'text', required: true, unique: true, }, { name: 'excerpt', type: 'textarea', }, { name: 'coverImage', type: 'upload', relationTo: 'media', }, { name: 'status', type: 'select', options: [ { label: 'Draft', value: 'draft' }, { label: 'Published', value: 'published' }, ], defaultValue: 'draft', required: true, }, { name: 'publishedAt', type: 'date', admin: { position: 'sidebar' }, }, { name: 'content', type: 'richText', }, ], }; export default Posts; ``` Add it to your main config `src/payload.config.ts`: ```ts import { buildConfig } from 'payload/config'; import Posts from './collections/Posts'; export default buildConfig({ serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL, admin: { user: 'users' }, collections: [ Posts, // Media and Users collections if you need them ], }); ``` > If you don’t have a **Media** or **Users** collection yet, run `create-payload-app` with a blog template or add simple collections for uploads and auth later. ### 3) Seed a few posts Open the Admin UI → **Posts** → **Create New** and add a couple of published posts with past `publishedAt` dates. ### 4) Confirm the REST API works Payload exposes a REST API by default at `/api/`. Visit: ``` http://localhost:3000/api/posts?limit=10&sort=-publishedAt&where[status][equals]=published ``` You should see JSON with your posts. > When you deploy, set `PAYLOAD_PUBLIC_SERVER_URL` and your DB env vars. If you’re hosting Payload separately from Next.js, enable **CORS** for your front‑end origin. --- ## Part 2 — Set up Next.js + RetroUI front‑end We’ll build a Next.js app that consumes the Payload REST API and renders a Blog List using **RetroUI** components. ### 1) Create the Next.js app ```bash npx create-next-app@latest retroui-payload-blog cd retroui-payload-blog ``` ### 2) Install TailwindCSS (if you didn’t choose the Tailwind template) ```bash pnpm i -D tailwindcss postcss autoprefixer npx tailwindcss init -p ``` Add `./components/**/*.{ts,tsx}` and `./app/**/*.{ts,tsx}` to `tailwind.config.ts` content. Also include RetroUI’s paths if required by the installer. ### 3) Install RetroUI Use the official installer (CLI) or manual install. Example (pnpm): ```bash pnpm add retroui ``` Then import components where needed. (If RetroUI ships a CLI to copy components, run it here and follow prompts.) ### 4) Connect to Payload (env + fetch helper) Create `.env.local`: ``` NEXT_PUBLIC_PAYLOAD_BASE_URL=http://localhost:3000 ``` Add a tiny fetch utility `lib/payload.ts`: ```ts export type Post = { id: string; title: string; slug: string; excerpt?: string; coverImage?: { url: string; filename: string } | string | null; status: 'draft' | 'published'; publishedAt?: string | null; }; export async function getPublishedPosts(limit = 12) { const base = process.env.NEXT_PUBLIC_PAYLOAD_BASE_URL; const url = new URL('/api/posts', base); url.searchParams.set('limit', String(limit)); url.searchParams.set('sort', '-publishedAt'); url.searchParams.set('where[status][equals]', 'published'); const res = await fetch(url.toString(), { next: { revalidate: 60 } }); if (!res.ok) throw new Error(`Failed to fetch posts: ${res.status}`); const data = await res.json(); return data.docs as Post[]; } ``` ### 5) Build the Blog List page with RetroUI Create `app/blog/page.tsx` (App Router): ```tsx import Image from 'next/image'; import Link from 'next/link'; import { getPublishedPosts, type Post } from '@/lib/payload'; // Example RetroUI components — adjust paths/names per your install import { Card } from 'retroui/card'; import { Button } from 'retroui/button'; import { Badge } from 'retroui/badge'; export const revalidate = 60; // ISR cadence export default async function BlogListPage() { const posts = await getPublishedPosts(12); return (

Blog

Latest posts from the Payload CMS backend, styled with RetroUI.

{posts.map((post) => ( {typeof post.coverImage === 'object' && post.coverImage?.url ? (
{post.title}
) : null}
Article {post.publishedAt ? ( {new Date(post.publishedAt).toLocaleDateString()} ) : null}

{post.title}

{post.excerpt ? (

{post.excerpt}

) : null}
))}
); } ``` > If your RetroUI package exposes different import names/paths, adjust `import { Card } from 'retroui/card'` lines to match. You can also swap in any other RetroUI components (Tabs, Inputs, etc.). ### 6) Post detail page (optional) Create `app/blog/[slug]/page.tsx`: ```tsx import Image from 'next/image'; import Link from 'next/link'; async function getPost(slug: string) { const base = process.env.NEXT_PUBLIC_PAYLOAD_BASE_URL!; const url = new URL('/api/posts', base); url.searchParams.set('limit', '1'); url.searchParams.set('where[slug][equals]', slug); const res = await fetch(url.toString(), { next: { revalidate: 60 } }); if (!res.ok) throw new Error('Failed to load post'); const data = await res.json(); return data.docs[0]; } export default async function PostPage({ params }: { params: { slug: string } }) { const post = await getPost(params.slug); if (!post) return
Not found
; return (
← Back to blog

{post.title}

{post.coverImage?.url ? (
{post.title}
) : null} {post.content?.root ? ( // If using Payload Lexical richText renderer, render here
{/* Render your rich text */}
) : ( post.excerpt ?

{post.excerpt}

: null )}
); } ``` > For rich text, use your preferred Payload Renderer (e.g., @payloadcms/richtext-lexical) and render accordingly. --- ## Part 3 — Cross‑origin & deployment notes If your Next.js app runs at a different origin than Payload: * Enable **CORS** in Payload: set `cors: ["https://your-next-app.com", "http://localhost:3000"]` in `payload.config.ts`. * Set `serverURL` (and `PAYLOAD_PUBLIC_SERVER_URL`) correctly so image URLs resolve. * Protect drafts: we allowed public `read` above. For private content, replace with an `access.read` function that checks auth/roles. Deployment options: * **Self‑host** (Node app on a VPS) or deploy to **Vercel**/**Cloudflare** for modern DX. * Use **SQLite** for quick demos, **Postgres/Mongo** for prod. --- ## Part 4 — Enhancements * **Search & filters**: Use Payload’s query parameters (e.g., `where[title][like]`) and wire a RetroUI **Input** + **Tabs** to filter. * **Pagination**: the REST API returns `totalDocs`, `limit`, `page`—build “Load more” with a RetroUI Button. * **Images**: move `coverImage` to a dedicated **Media** collection and use Payload’s upload adapter. * **Preview mode**: set up a draft preview route in Next.js that fetches with draft‑auth headers. * **GraphQL**: Payload also exposes GraphQL—use it if you prefer typed queries. --- ## Troubleshooting * **CORS errors**: Check `cors` config and ensure `serverURL` is set. * **Images not showing**: Confirm the `url` returned for uploads and that Next.js `next.config.js` allows the Payload domain under `images.domains`. * **Nothing renders**: Inspect the fetch URL in `lib/payload.ts`. Ensure records exist and `status` is `published`. --- ## Recap You now have: * A Payload CMS app exposing posts over REST * A Next.js site styled with RetroUI * A `/blog` that lists and links to individual posts From here, add authors, categories, and tags; style with more RetroUI components; and ship it.