タグ: Next.js

  • WordPress REST APIでヘッドレスCMS構築:Next.jsフロントエンドと連携する実践テクニック

    WordPressをヘッドレスCMSとして使い、フロントエンドをNext.jsで構築するアーキテクチャが注目されています。コンテンツ管理はWordPressの使いやすい管理画面で、表示はモダンなReactアプリで、という両方の良いところを取る手法です。

    WordPress REST APIの基本

    WordPress 4.7以降、REST APIが標準搭載されています。/wp-json/wp/v2/ 以下のエンドポイントで投稿・ページ・カテゴリなどのデータをJSON形式で取得できます。

    # 投稿一覧を取得
    curl https://your-site.com/wp-json/wp/v2/posts
    
    # 特定の投稿を取得
    curl https://your-site.com/wp-json/wp/v2/posts/123
    
    # カテゴリ一覧
    curl https://your-site.com/wp-json/wp/v2/categories
    
    # 検索
    curl "https://your-site.com/wp-json/wp/v2/posts?search=React"
    
    # ページネーション
    curl "https://your-site.com/wp-json/wp/v2/posts?per_page=10&page=2"

    Next.jsプロジェクトのセットアップ

    npx create-next-app@latest wp-frontend --typescript --app
    cd wp-frontend

    WordPress APIクライアント

    // lib/wordpress.ts
    const API_URL = process.env.WORDPRESS_API_URL || "https://your-site.com/wp-json/wp/v2";
    
    export interface WPPost {
      id: number;
      slug: string;
      title: { rendered: string };
      content: { rendered: string };
      excerpt: { rendered: string };
      date: string;
      categories: number[];
      _embedded?: {
        "wp:featuredmedia"?: Array<{ source_url: string }>;
      };
    }
    
    export async function getPosts(page = 1, perPage = 10): Promise<WPPost[]> {
      const res = await fetch(
        `${API_URL}/posts?_embed&per_page=${perPage}&page=${page}`,
        { next: { revalidate: 3600 } } // ISR: 1時間キャッシュ
      );
      if (!res.ok) throw new Error("Failed to fetch posts");
      return res.json();
    }
    
    export async function getPostBySlug(slug: string): Promise<WPPost | null> {
      const res = await fetch(
        `${API_URL}/posts?_embed&slug=${slug}`,
        { next: { revalidate: 3600 } }
      );
      const posts = await res.json();
      return posts[0] || null;
    }
    
    export async function getCategories() {
      const res = await fetch(`${API_URL}/categories`);
      return res.json();
    }

    投稿一覧ページ

    // app/page.tsx
    import { getPosts } from "@/lib/wordpress";
    import Link from "next/link";
    
    export default async function Home() {
      const posts = await getPosts();
    
      return (
        <main>
          <h1>ブログ</h1>
          {posts.map((post) => (
            <article key={post.id}>
              <Link href={`/posts/${post.slug}`}>
                <h2 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
              </Link>
              <div dangerouslySetInnerHTML={{ __html: post.excerpt.rendered }} />
              <time>{new Date(post.date).toLocaleDateString("ja-JP")}</time>
            </article>
          ))}
        </main>
      );
    }

    投稿詳細ページ

    // app/posts/[slug]/page.tsx
    import { getPostBySlug, getPosts } from "@/lib/wordpress";
    import { notFound } from "next/navigation";
    
    export async function generateStaticParams() {
      const posts = await getPosts(1, 100);
      return posts.map((post) => ({ slug: post.slug }));
    }
    
    export default async function PostPage({ params }: { params: { slug: string } }) {
      const post = await getPostBySlug(params.slug);
      if (!post) notFound();
    
      return (
        <article>
          <h1 dangerouslySetInnerHTML={{ __html: post.title.rendered }} />
          <time>{new Date(post.date).toLocaleDateString("ja-JP")}</time>
          <div dangerouslySetInnerHTML={{ __html: post.content.rendered }} />
        </article>
      );
    }

    カスタムエンドポイントの追加

    functions.phpでカスタムREST APIエンドポイントを追加できます。人気記事ランキングや関連記事など、標準APIにない機能を実装する場合に使います。

    // functions.php
    add_action("rest_api_init", function() {
        register_rest_route("custom/v1", "/popular", [
            "methods" => "GET",
            "callback" => function() {
                $posts = get_posts([
                    "meta_key" => "post_views",
                    "orderby" => "meta_value_num",
                    "order" => "DESC",
                    "numberposts" => 5,
                ]);
                return array_map(function($p) {
                    return [
                        "id" => $p->ID,
                        "title" => $p->post_title,
                        "slug" => $p->post_name,
                        "views" => get_post_meta($p->ID, "post_views", true),
                    ];
                }, $posts);
            },
            "permission_callback" => "__return_true",
        ]);
    });

    CORS設定

    ヘッドレス構成ではフロントエンドとWordPressが別ドメインになるため、CORS設定が必要です。

    // functions.php
    add_action("rest_api_init", function() {
        remove_filter("rest_pre_serve_request", "rest_send_cors_headers");
        add_filter("rest_pre_serve_request", function($value) {
            header("Access-Control-Allow-Origin: https://your-frontend.com");
            header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
            header("Access-Control-Allow-Headers: Content-Type, Authorization");
            return $value;
        });
    });

    まとめ

    WordPressのREST APIとNext.jsを組み合わせることで、編集者にはWordPressの使いやすいUIを、ユーザーにはReactベースの高速な表示体験を提供できます。ISRを使えばビルド不要で最新コンテンツが反映される、最強のブログ基盤が構築できます。

  • Next.js 14 App RouterでSSR・SSG・ISRを使い分ける実践ガイド

    Next.js 14のApp Routerでは、レンダリング方式の選択が柔軟になりました。SSR(サーバーサイドレンダリング)、SSG(静的サイト生成)、ISR(増分静的再生成)をページ単位・コンポーネント単位で使い分ける方法を解説します。

    App Routerの基本

    Next.js 13以降のApp Routerでは、デフォルトですべてのコンポーネントがServer Componentになります。これにより、クライアントに送信されるJavaScriptの量が大幅に削減されます。

    // app/page.tsx - デフォルトでServer Component
    export default async function HomePage() {
      // サーバー側で実行される
      const data = await fetch("https://api.example.com/posts");
      const posts = await data.json();
    
      return (
        <main>
          <h1>最新記事</h1>
          {posts.map(post => (
            <article key={post.id}>
              <h2>{post.title}</h2>
              <p>{post.excerpt}</p>
            </article>
          ))}
        </main>
      );
    }

    SSG(静的生成)

    ビルド時にHTMLを生成する方式です。ブログ記事やドキュメントなど、更新頻度が低いコンテンツに最適です。

    // app/blog/[slug]/page.tsx
    export async function generateStaticParams() {
      const posts = await fetch("https://api.example.com/posts").then(r => r.json());
      return posts.map(post => ({ slug: post.slug }));
    }
    
    export default async function BlogPost({ params }) {
      const post = await fetch(
        `https://api.example.com/posts/${params.slug}`,
        { cache: "force-cache" } // SSG: ビルド時にキャッシュ
      ).then(r => r.json());
    
      return <article><h1>{post.title}</h1><div>{post.content}</div></article>;
    }

    SSR(サーバーサイドレンダリング)

    リクエストごとにサーバーでHTMLを生成します。ユーザーごとに異なるコンテンツを表示する場合に使います。

    // app/dashboard/page.tsx
    export default async function Dashboard() {
      const data = await fetch("https://api.example.com/user/dashboard", {
        cache: "no-store", // SSR: キャッシュしない
        headers: { Authorization: `Bearer ${getToken()}` }
      }).then(r => r.json());
    
      return <div>ようこそ、{data.user.name}さん</div>;
    }

    ISR(増分静的再生成)

    SSGとSSRの良いとこ取り。静的に生成されたページを一定時間後にバックグラウンドで再生成します。

    // app/products/page.tsx
    export default async function Products() {
      const products = await fetch("https://api.example.com/products", {
        next: { revalidate: 3600 } // ISR: 1時間ごとに再生成
      }).then(r => r.json());
    
      return (
        <div>
          {products.map(p => <div key={p.id}>{p.name} - ¥{p.price}</div>)}
        </div>
      );
    }

    使い分けの判断基準

    どのレンダリング方式を使うかは、コンテンツの特性で決めます。更新頻度が低い(ブログ・ドキュメント)→ SSG、リアルタイム性が必要(ダッシュボード)→ SSR、適度に更新される(商品一覧・ニュース)→ ISR、が基本的な選び方です。

    まとめ

    App Routerでは、fetchのオプションを変えるだけでレンダリング方式を切り替えられます。ページの特性に合わせて最適な方式を選ぶことで、パフォーマンスとユーザー体験を両立できます。

IP: 取得中...
216.73.216.31216.73.216.31