2026年5月28日

2026年5月28日

WordPressをHeadless CMSとしてNext.jsフロントエンドと連携する方法

はじめに

Headless WordPressとは、WordPressをコンテンツ管理のバックエンドとしてのみ使い、フロントエンドの表示はNext.jsなどのモダンフレームワークに任せるアーキテクチャです。表示速度・セキュリティ・開発体験が大幅に向上し、WordPressの管理画面はそのまま使えるため、編集者にとっても移行コストが低い手法です。

症状・原因

  • WordPressのフロントエンドがPHPテンプレートに縛られ、モダンなUI実装が難しい
  • ページ表示速度が遅く、Core Web Vitalsのスコアが低い
  • 開発チームがReact/Next.jsに精通しているがWordPressの管理機能を活かしたい

解決手順

ステップ1:WordPress側でCORSとREST APIカスタムフィールドを設定する

<?php
// REST APIにCORSヘッダーを追加してNext.jsからのアクセスを許可する
add_action( 'rest_api_init', 'my_add_cors_headers', 15 );

function my_add_cors_headers() {
    // フロントエンドのオリジンを指定する(本番URLに合わせる)
    $allowed_origins = [
        'https://yoursite.vercel.app',
        'http://localhost:3000', // 開発環境
    ];

    $origin = $_SERVER['HTTP_ORIGIN'] ?? '';

    if ( in_array( $origin, $allowed_origins, true ) ) {
        header( "Access-Control-Allow-Origin: {$origin}" );
        header( 'Access-Control-Allow-Methods: GET, POST, OPTIONS' );
        header( 'Access-Control-Allow-Credentials: true' );
        header( 'Access-Control-Allow-Headers: Authorization, Content-Type' );
    }

    if ( $_SERVER['REQUEST_METHOD'] === 'OPTIONS' ) {
        // プリフライトリクエストには200を返して終了する
        status_header( 200 );
        exit;
    }
}

// カスタムフィールドをREST APIレスポンスに追加する
add_action( 'rest_api_init', 'my_register_rest_fields' );

function my_register_rest_fields() {
    // 「投稿」に「アイキャッチURL」フィールドをREST APIで公開する
    register_rest_field( 'post', 'featured_image_url', [
        'get_callback' => function( $post ) {
            $thumb_id = get_post_thumbnail_id( $post['id'] );
            return $thumb_id
                ? wp_get_attachment_image_url( $thumb_id, 'large' )
                : null;
        },
        'schema' => [
            'type'        => 'string',
            'description' => 'アイキャッチ画像のURL',
        ],
    ] );

    // カスタムフィールド「author_profile」も公開する
    register_rest_field( 'post', 'author_profile', [
        'get_callback' => fn( $post ) => get_post_meta( $post['id'], 'author_profile', true ),
    ] );
}

ステップ2:Next.jsのgetStaticPropsでWordPress REST APIを呼び出す

// pages/blog/[slug].jsx
// WordPress REST APIから記事データを取得して静的生成する

const WP_API_BASE = process.env.NEXT_PUBLIC_WP_API_URL; // 例: https://cms.yoursite.com/wp-json/wp/v2

// 静的パスを生成する(全記事のslugを取得)
export async function getStaticPaths() {
    const res = await fetch(`${WP_API_BASE}/posts?per_page=100&_fields=slug`);
    const posts = await res.json();

    return {
        // 全記事のパスを事前生成する
        paths: posts.map((post) => ({ params: { slug: post.slug } })),
        // 未生成のパスはオンデマンドで生成する(ISRと組み合わせ)
        fallback: 'blocking',
    };
}

// 各ページのデータを取得する
export async function getStaticProps({ params }) {
    const res = await fetch(
        `${WP_API_BASE}/posts?slug=${params.slug}&_embed=1`
    );
    const posts = await res.json();

    // 記事が見つからない場合は404ページを返す
    if (!posts.length) {
        return { notFound: true };
    }

    const post = posts[0];

    return {
        props: {
            post: {
                id: post.id,
                title: post.title.rendered,
                content: post.content.rendered,
                date: post.date,
                // 独自フィールド(ステップ1で追加したもの)
                featuredImageUrl: post.featured_image_url ?? null,
                authorProfile: post.author_profile ?? '',
                // カテゴリ情報(_embedで取得)
                categories: post._embedded?.['wp:term']?.[0] ?? [],
            },
        },
        // ISR: 60秒ごとにバックグラウンドで再生成する
        revalidate: 60,
    };
}

export default function BlogPost({ post }) {
    return (
        <article>
            <h1 dangerouslySetInnerHTML={{ __html: post.title }} />
            <div dangerouslySetInnerHTML={{ __html: post.content }} />
        </article>
    );
}

ステップ3:Incremental Static Regeneration(ISR)を設定する

// next.config.js
// 画像最適化とISRの設定を行う

/** @type {import('next').NextConfig} */
const nextConfig = {
    images: {
        // WordPressのメディアサーバーを許可する
        remotePatterns: [
            {
                protocol: 'https',
                hostname: 'cms.yoursite.com',
                pathname: '/wp-content/uploads/**',
            },
        ],
    },
    // ISRのデフォルト再生成間隔(秒)
    // 個別ページは getStaticProps の revalidate で上書きできる
    experimental: {
        isrFlushToDisk: true, // ISRキャッシュをディスクに保存する
    },
};

module.exports = nextConfig;
// pages/api/revalidate.js
// WordPress Webhookからのリバリデーション要求を処理するAPIエンドポイント

export default async function handler(req, res) {
    // シークレットトークンで正当なリクエストか確認する
    if (req.query.secret !== process.env.REVALIDATION_SECRET) {
        return res.status(401).json({ message: '不正なリクエストです' });
    }

    const { slug, type } = req.query;

    try {
        // 特定のページパスを手動でリバリデートする
        if (type === 'post' && slug) {
            await res.revalidate(`/blog/${slug}`);
        }
        // 投稿一覧ページも合わせてリバリデートする
        await res.revalidate('/blog');

        return res.json({ revalidated: true, slug });
    } catch (err) {
        // リバリデートに失敗した場合はエラーを返す
        return res.status(500).json({ message: 'リバリデート失敗', error: err.message });
    }
}

ステップ4:WordPress投稿時にNext.jsリバリデーションを呼び出す

<?php
// 記事が公開・更新されたときにNext.jsのリバリデーションAPIを叩く
add_action( 'save_post', 'my_trigger_nextjs_revalidation', 10, 2 );

function my_trigger_nextjs_revalidation( $post_id, $post ) {
    // 自動保存とリビジョンはスキップする
    if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
        return;
    }

    // 公開済みの投稿のみ対象にする
    if ( $post->post_status !== 'publish' || $post->post_type !== 'post' ) {
        return;
    }

    $next_url = get_option( 'my_nextjs_url', 'https://yoursite.vercel.app' );
    $secret   = get_option( 'my_revalidation_secret', '' );
    $slug     = $post->post_name;

    // Next.jsのリバリデーションエンドポイントにPOSTリクエストを送る
    $response = wp_remote_get( add_query_arg( [
        'secret' => $secret,
        'slug'   => $slug,
        'type'   => 'post',
    ], "{$next_url}/api/revalidate" ), [
        'timeout' => 10, // 10秒でタイムアウトする
    ] );

    if ( is_wp_error( $response ) ) {
        // エラーログに記録する(デバッグ用)
        error_log( 'Next.js revalidation failed: ' . $response->get_error_message() );
    }
}

ステップ5:next/imageでWordPress画像を最適化する

// components/WpImage.jsx
// WordPress画像をnext/imageで最適化して表示するコンポーネント

import Image from 'next/image';

/**
 * WordPress REST APIから取得した画像URLをnext/imageで表示する
 * @param {string} src - WordPressの画像URL
 * @param {string} alt - alt属性テキスト
 * @param {number} width - 表示幅(px)
 * @param {number} height - 表示高さ(px)
 */
export default function WpImage({ src, alt, width = 800, height = 450, priority = false }) {
    if (!src) return null;

    return (
        <Image
            src={src}
            alt={alt}
            width={width}
            height={height}
            priority={priority} // ファーストビュー画像はpriorityをtrueにする
            // WebP変換はNext.jsが自動で行う
            style={{ width: '100%', height: 'auto' }}
            // WordPressの大サイズ画像を優先して使用する
            sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 800px"
        />
    );
}

// 使用例(BlogPostコンポーネント内)
// <WpImage src={post.featuredImageUrl} alt={post.title} priority={true} />

注意事項

  • 認証: 下書き記事などパスワード保護が必要なコンテンツはJWT認証またはBasic認証を実装してください。
  • プレビュー機能: WordPressの「プレビュー」ボタンはHeadless構成では動作しません。Next.jsのPreview Modeと組み合わせた専用実装が必要です。
  • プラグイン互換性: ページビルダー系プラグイン(Elementor等)が出力するHTMLはHeadless構成では正しく機能しません。Gutenbergのみの使用を推奨します。

まとめ

WordPressをHeadless CMSとして使うことで、Next.jsの静的生成(SSG)とISRにより高速で安全なWebサイトを構築できます。WordPressのWebhookとNext.jsのリバリデーションAPIを組み合わせることで、記事更新時に数秒でフロントエンドに反映される仕組みが実現します。関連記事:WordPressのGraphQL APIをWPGraphQLで実装する方法

お気軽にご相談ください

お見積りへ お問い合わせへ