コンテンツへスキップ

Neobrutalism と Payload CMS を連携させてブログ一覧を作る実践ガイド

UI ライブラリを使えば開発を大きく高速化できます。本記事では、次の React プロジェクトに役立つトップ 5 を紹介します。

Payload CMS をコンテンツのバックエンドに、Neobrutalism をフロントエンドのコンポーネントキットに据えて、Next.js で洗練されたネオブルータリズムのブログ一覧を出荷するまでを、実践的にひととおり通した end-to-end のガイドです。

スタックが Next.js + TailwindCSS なら相性抜群です。Payload は Next ネイティブ、Neobrutalism は Tailwind ファーストなので、両者はぴたりと噛み合います。


何を作るのか

  • Posts コレクションを備えた Payload CMS アプリ
  • Payload の REST API から投稿を取得する Next.js のフロントエンド
  • Neobrutalism のコンポーネント(Card、Badge、Button)でレスポンシブな一覧を描画する /blog ページ

前提条件

  • Node.js 20.9+(Payload は Node 20.9 以降が必要です)
  • フロントエンド向けの Next.js 15+(Payload は Next ネイティブ)
  • 対応する任意の DB: SQLite(開発用)、Postgres、または MongoDB

ヒント: ローカル開発でいちばん手早いのは SQLite です。デプロイ時に Postgres/Mongo へ切り替えましょう。


Part 1 — Payload CMS をセットアップする

Payload は既存の Next.js アプリに追加することも、新規に scaffold することもできます。ここでは、API をクリーンで持ち運びやすい状態に保つため、専用の Payload プロジェクトを scaffold します。

1) 新しい Payload アプリを作成する

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

これで Payload とその Admin UI が起動します(通常は http://localhost:3000/admin)。

2) Posts コレクションを定義する

src/collections/Posts.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;

メインの設定ファイル src/payload.config.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
  ],
});

まだ MediaUsers コレクションがない場合は、blog テンプレートを指定して create-payload-app を実行するか、アップロードや認証用のシンプルなコレクションを後から追加してください。

3) 投稿をいくつか用意する

Admin UI を開いて PostsCreate New から、過去の publishedAt 日付を持つ公開済み投稿をいくつか追加します。

4) REST API が動くことを確認する

Payload は標準で /api/<collection> に REST API を公開します。

次の URL にアクセスします:

http://localhost:3000/api/posts?limit=10&sort=-publishedAt&where[status][equals]=published

投稿を含む JSON が表示されるはずです。

デプロイ時には PAYLOAD_PUBLIC_SERVER_URL と DB の環境変数を設定します。Payload を Next.js とは別にホストする場合は、フロントエンドのオリジンに対して CORS を有効にしてください。


Part 2 — Next.js + Neobrutalism のフロントエンドをセットアップする

Payload の REST API を利用し、Neobrutalism のコンポーネントでブログ一覧を描画する Next.js アプリを構築します。

1) Next.js アプリを作成する

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 をインストールする(Tailwind テンプレートを選ばなかった場合)

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

tailwind.config.ts の content に ./components/**/*.{ts,tsx}./app/**/*.{ts,tsx} を追加します。インストーラーが求める場合は、Neobrutalism のパスも含めてください。

3) Neobrutalism をインストールする

公式インストーラー(CLI)を使うか、手動でインストールします。例(pnpm):

pnpm add retroui

そのうえで、必要な場所でコンポーネントを import します。(Neobrutalism がコンポーネントをコピーする CLI を提供している場合は、ここで実行してプロンプトに従ってください。)

4) Payload に接続する(env + fetch ヘルパー)

.env.local を作成します:

NEXT_PUBLIC_PAYLOAD_BASE_URL=http://localhost:3000

小さな fetch ユーティリティ lib/payload.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) Neobrutalism でブログ一覧ページを作る

app/blog/page.tsx を作成します(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 パッケージが別の import 名やパスを公開している場合は、import { Card } from 'neobrutalism/card' の行を合わせて調整してください。ほかの Neobrutalism コンポーネント(Tabs、Input など)に差し替えることもできます。

6) 投稿詳細ページ(任意)

app/blog/[slug]/page.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>
  );
}

リッチテキストには、お好みの Payload レンダラー(例: @payloadcms/richtext-lexical)を使い、それに合わせて描画してください。


Part 3 — クロスオリジンとデプロイに関する注意

Next.js アプリが Payload とは別のオリジンで動作する場合:

  • Payload で CORS を有効にします: payload.config.tscors: ["https://your-next-app.com", "http://localhost:3000"] を設定します。
  • 画像 URL が正しく解決されるよう、serverURL(および PAYLOAD_PUBLIC_SERVER_URL)を正確に設定します。
  • 下書きを保護します: 上記では公開の read を許可していました。非公開コンテンツの場合は、認証/ロールをチェックする access.read 関数に置き換えてください。

デプロイの選択肢:

  • セルフホスト(VPS 上の Node アプリ)か、モダンな開発体験のために VercelCloudflare へデプロイします。
  • 手早いデモには SQLite を、本番には Postgres/Mongo を使いましょう。

Part 4 — 拡張

  • 検索とフィルター: Payload のクエリパラメータ(例: where[title][like])を使い、Neobrutalism の Input + Tabs をつないでフィルタリングします。
  • ページネーション: REST API は totalDocslimitpage を返すので、Neobrutalism の Button で「さらに読み込む」を作ります。
  • 画像: coverImage を専用の Media コレクションへ移し、Payload のアップロードアダプターを使います。
  • プレビューモード: 下書き用の認証ヘッダーで取得する下書きプレビュー用のルートを Next.js に用意します。
  • GraphQL: Payload は GraphQL も公開しています。型付きクエリが好みなら活用してください。

トラブルシューティング

  • CORS エラー: cors の設定を確認し、serverURL が設定されているか確かめます。
  • 画像が表示されない: アップロードに対して返される url を確認し、Next.js の next.config.jsimages.domains で Payload のドメインを許可しているか確かめます。
  • 何も描画されない: lib/payload.ts の fetch URL を確認します。レコードが存在し、statuspublished になっているか確かめてください。

まとめ

これで、次のものが揃いました:

  • REST 経由で投稿を公開する Payload CMS アプリ
  • Neobrutalism でスタイリングした Next.js サイト
  • 個々の投稿を一覧表示してリンクする /blog

ここから、著者、カテゴリ、タグを追加し、さらに多くの Neobrutalism コンポーネントでスタイリングして、出荷しましょう。


← 記事一覧へ戻る