Files
nanocode/RetroUI/content/blogs/retroui-with-payload-cms.mdx
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

390 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 <your-payload-app>
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/<collection>`.
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 (
<main className="container mx-auto px-4 py-10">
<h1 className="text-4xl font-extrabold mb-6">Blog</h1>
<p className="text-muted-foreground mb-10">
Latest posts from the Payload CMS backend, styled with RetroUI.
</p>
<section className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{posts.map((post) => (
<Card key={post.id} className="p-0 overflow-hidden">
{typeof post.coverImage === 'object' && post.coverImage?.url ? (
<div className="relative h-44 w-full">
<Image
src={post.coverImage.url}
alt={post.title}
fill
className="object-cover"
/>
</div>
) : null}
<div className="p-5 space-y-3">
<div className="flex items-center gap-2">
<Badge variant="outline">Article</Badge>
{post.publishedAt ? (
<span className="text-xs opacity-70">
{new Date(post.publishedAt).toLocaleDateString()}
</span>
) : null}
</div>
<h2 className="text-2xl font-bold leading-tight">
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</h2>
{post.excerpt ? (
<p className="text-sm text-muted-foreground line-clamp-3">
{post.excerpt}
</p>
) : null}
<div className="pt-2">
<Button asChild>
<Link href={`/blog/${post.slug}`}>Read more</Link>
</Button>
</div>
</div>
</Card>
))}
</section>
</main>
);
}
```
> 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 <div className="p-10">Not found</div>;
return (
<article className="container mx-auto px-4 py-10 max-w-3xl">
<Link href="/blog" className="underline">← Back to blog</Link>
<h1 className="text-4xl font-extrabold mt-3">{post.title}</h1>
{post.coverImage?.url ? (
<div className="relative h-80 w-full my-6">
<Image src={post.coverImage.url} alt={post.title} fill className="object-cover" />
</div>
) : null}
{post.content?.root ? (
// If using Payload Lexical richText renderer, render here
<div className="prose prose-neutral dark:prose-invert">
{/* Render your rich text */}
</div>
) : (
post.excerpt ? <p className="mt-4 text-lg">{post.excerpt}</p> : null
)}
</article>
);
}
```
> 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.