14 Kasım 2025
Neobrutalism'yi Payload CMS ile entegre etme
UI kütüphaneleri geliştirmenizi belirgin biçimde hızlandırabilir. Bu yazıda bir sonraki React projeniz için en iyi 5 tanesini ele alıyoruz.
Dov Azencot
@DovAzencotPayload CMS'i içerik arka ucu, Neobrutalism'yi de ön uç bileşen kitiniz olarak bağlamak ve ardından Next.js'te temiz, neobrütalist bir blog listesi yayınlamak için pratik, baştan sona bir kılavuz.
Yığınınız Next.js + TailwindCSS üzerine kuruluysa sonuç mükemmel olur. Payload Next-nativ, Neobrutalism ise Tailwind öncelikli; bu yüzden ikisi kusursuzca kenetlenir.
Ne inşa edeceksiniz
- Posts koleksiyonuna sahip bir Payload CMS uygulaması
- Payload'ın REST API'sinden gönderileri çeken bir Next.js ön ucu
- Neobrutalism bileşenlerini (Cards, Badges, Buttons) kullanarak responsive bir liste render eden bir
/blogsayfası
Ön koşullar
- Node.js 20.9+ (Payload, Node 20.9 ya da daha yenisini gerektirir)
- Ön uç için Next.js 15+ (Payload Next-nativ)
- Uyumlu herhangi bir veritabanı: SQLite (dev), Postgres ya da MongoDB
İpucu: Yerel geliştirme için SQLite en hızlı yoldur. Dağıtım sırasında Postgres/Mongo'ya geçin.
Bölüm 1 — Payload CMS'i kurma
Payload'ı mevcut bir Next.js uygulamasına ekleyebilir ya da yeni bir tane oluşturabilirsiniz. Burada, API'sinin temiz ve taşınabilir kalması için ayrı bir Payload projesi oluşturacağız.
1) Yeni bir Payload uygulaması oluşturma
Bu, Payload'ı ve Yönetim Arayüzü'nü başlatır (genellikle http://localhost:3000/admin adresinde).
2) Bir Posts koleksiyonu tanımlama
src/collections/Posts.ts dosyasını oluşturun:
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;Onu ana yapılandırmanız olan src/payload.config.ts'e ekleyin:
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
],
});Henüz bir Media ya da Users koleksiyonunuz yoksa,
create-payload-app'i bir blog şablonuyla çalıştırın ya da yüklemeler ve kimlik doğrulama için daha sonra basit koleksiyonlar ekleyin.
3) Birkaç gönderi ekleme
Yönetim Arayüzü → Posts → Create New yolunu izleyin ve geçmiş publishedAt tarihleriyle birkaç yayımlanmış gönderi ekleyin.
4) REST API'nin çalıştığını doğrulama
Payload, varsayılan olarak /api/<collection> adresinde bir REST API sunar.
Şu adrese gidin:
http://localhost:3000/api/posts?limit=10&sort=-publishedAt&where[status][equals]=published
Gönderilerinizi içeren bir JSON görmeniz gerekir.
Dağıtım yaparken
PAYLOAD_PUBLIC_SERVER_URL'i ve veritabanı ortam değişkenlerinizi ayarlayın. Payload'ı Next.js'ten ayrı barındırıyorsanız, ön ucunuzun kaynağı için CORS'u etkinleştirin.
Bölüm 2 — Next.js + Neobrutalism ön ucunu kurma
Payload REST API'sini tüketen ve Neobrutalism bileşenlerini kullanarak bir blog listesi render eden bir Next.js uygulaması inşa edeceğiz.
1) Next.js uygulamasını oluşturma
2) TailwindCSS'i kurma (Tailwind şablonunu seçmediyseniz)
pnpm i -D tailwindcss postcss autoprefixer
npx tailwindcss init -ptailwind.config.ts içindeki content alanına ./components/**/*.{ts,tsx} ve ./app/**/*.{ts,tsx} ekleyin. Kurulum aracı gerektiriyorsa Neobrutalism'nin yollarını da dahil edin.
3) Neobrutalism'yi kurma
Resmi kurulum aracını (CLI) ya da manuel kurulumu kullanın. Örnek (pnpm):
pnpm add retrouiArdından bileşenleri ihtiyaç duyduğunuz yerde içe aktarın. (Neobrutalism bileşenleri kopyalamak için bir CLI sunuyorsa, onu burada çalıştırın ve yönergeleri izleyin.)
4) Payload'a bağlanma (env + fetch yardımcısı)
.env.local dosyasını oluşturun:
NEXT_PUBLIC_PAYLOAD_BASE_URL=http://localhost:3000
Küçük bir fetch yardımcısı olan lib/payload.ts dosyasını ekleyin:
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) Blog listesi sayfasını Neobrutalism ile inşa etme
app/blog/page.tsx dosyasını oluşturun (App Router):
import Image from 'next/image';
import Link from 'next/link';
import { getPublishedPosts, type Post } from '@/lib/payload';
// Example Neobrutalism components — adjust paths/names per your install
import { Card } from 'neobrutalism/card';
import { Button } from 'neobrutalism/button';
import { Badge } from 'neobrutalism/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 Neobrutalism.
</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>
);
}Neobrutalism paketiniz farklı içe aktarma adları/yolları sunuyorsa,
import { Card } from 'neobrutalism/card'satırlarını buna göre ayarlayın. Ayrıca başka herhangi bir Neobrutalism bileşeniyle de değiştirebilirsiniz (Tabs, Inputs vb.).
6) Gönderi ayrıntı sayfası (isteğe bağlı)
app/blog/[slug]/page.tsx dosyasını oluşturun:
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>
);
}Zengin metin için tercih ettiğiniz Payload Renderer'ı (ör. @payloadcms/richtext-lexical) kullanın ve buna göre render edin.
Bölüm 3 — Cross-origin ve dağıtım notları
Next.js uygulamanız Payload'dan farklı bir kaynakta çalışıyorsa:
- Payload'da CORS'u etkinleştirin:
payload.config.tsiçindecors: ["https://your-next-app.com", "http://localhost:3000"]ayarını yapın. - Görsel URL'lerinin çözülmesi için
serverURL'i (vePAYLOAD_PUBLIC_SERVER_URL'i) doğru ayarlayın. - Taslakları koruyun: yukarıda genel
read'e izin verdik. Özel içerik için bunu, kimlik doğrulamayı/rolleri denetleyen biraccess.readişleviyle değiştirin.
Dağıtım seçenekleri:
- Kendi kendine barındırma (bir VPS üzerinde Node uygulaması) ya da modern bir DX için Vercel/Cloudflare'e dağıtım.
- Hızlı demolar için SQLite, üretim için Postgres/Mongo kullanın.
Bölüm 4 — İyileştirmeler
- Arama ve filtreler: Payload'ın sorgu parametrelerini (ör.
where[title][like]) kullanın ve filtrelemek için bir Neobrutalism Input + Tabs bağlayın. - Sayfalama: REST API
totalDocs,limit,pagedöndürür; bir Neobrutalism Button ile „Daha fazla yükle“ oluşturun. - Görseller:
coverImage'i ayrı bir Media koleksiyonuna taşıyın ve Payload'ın yükleme bağdaştırıcısını kullanın. - Önizleme modu: Next.js'te, taslak kimlik doğrulama başlıklarıyla veri çeken bir taslak önizleme rotası kurun.
- GraphQL: Payload GraphQL de sunar; tipli sorguları tercih ediyorsanız kullanın.
Sorun giderme
- CORS hataları:
corsyapılandırmasını denetleyin veserverURL'in ayarlı olduğundan emin olun. - Görseller görünmüyor: Yüklemeler için döndürülen
url'i ve Next.jsnext.config.js'in Payload alan adınaimages.domainsaltında izin verdiğini doğrulayın. - Hiçbir şey render edilmiyor:
lib/payload.tsiçindeki fetch URL'sini inceleyin. Kayıtların var olduğundan vestatus'unpublishedolduğundan emin olun.
Özet
Artık şunlara sahipsiniz:
- Gönderileri REST üzerinden sunan bir Payload CMS uygulaması
- Neobrutalism ile stillendirilmiş bir Next.js sitesi
- Gönderileri listeleyen ve her birine bağlantı veren bir
/blogsayfası
Buradan itibaren yazarlar, kategoriler ve etiketler ekleyin; daha fazla Neobrutalism bileşeniyle stillendirin; ve yayına alın.