İçeriğe atla

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.

Payload 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 /blog sayfası

Ö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

pnpm 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
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
yarn 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
bunx --bun 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

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ü → PostsCreate 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

pnpm create next-app@latest retroui-payload-blog
cd retroui-payload-blog
npx create-next-app@latest retroui-payload-blog
cd retroui-payload-blog
yarn create next-app@latest retroui-payload-blog
cd retroui-payload-blog
bunx --bun create-next-app@latest retroui-payload-blog
cd retroui-payload-blog

2) TailwindCSS'i kurma (Tailwind şablonunu seçmediyseniz)

pnpm i -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

tailwind.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 retroui

Ardı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.ts içinde cors: ["https://your-next-app.com", "http://localhost:3000"] ayarını yapın.
  • Görsel URL'lerinin çözülmesi için serverURL'i (ve PAYLOAD_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 bir access.read iş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, page dö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ı: cors yapılandırmasını denetleyin ve serverURL'in ayarlı olduğundan emin olun.
  • Görseller görünmüyor: Yüklemeler için döndürülen url'i ve Next.js next.config.js'in Payload alan adına images.domains altında izin verdiğini doğrulayın.
  • Hiçbir şey render edilmiyor: lib/payload.ts içindeki fetch URL'sini inceleyin. Kayıtların var olduğundan ve status'un published olduğ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 /blog sayfası

Buradan itibaren yazarlar, kategoriler ve etiketler ekleyin; daha fazla Neobrutalism bileşeniyle stillendirin; ve yayına alın.


← Yazılara dönün