T-CREATOR

WordPress 技術ロードマップ 2025:ブロック × ヘッドレス二刀流の最前線

WordPress 技術ロードマップ 2025:ブロック × ヘッドレス二刀流の最前線

WordPress が技術的な転換点を迎える 2025 年。従来のテーマ開発からブロックエディター中心の開発へ、そして静的サイトからヘッドレス構成への大きな流れが加速しています。この変化の波に乗り遅れないために、開発者が今押さえるべき技術スタックと実装手法をまとめました。

特に注目すべきは、ブロックエディターとヘッドレス CMS の「二刀流」アプローチです。これらの技術を組み合わせることで、コンテンツ管理の利便性とパフォーマンスを両立できるでしょう。

背景

WordPress 6.x 系の進化とブロックエディター完全移行

WordPress は 2021 年のバージョン 5.9 で Full Site Editing(FSE)を正式リリースして以来、ブロックエディターの機能強化を続けています。2024 年現在、WordPress 6.4 では以下の重要な進歩が見られました。

mermaidflowchart TB
  classic[クラシックテーマ] -->|移行| block[ブロックテーマ]
  classic -->|段階的移行| hybrid[ハイブリッド型]
  hybrid -->|完全移行| block

  block -->|活用| fse[Full Site Editing]
  fse -->|実現| custom[カスタムブロック開発]
  fse -->|実現| template[テンプレート編集]
  fse -->|実現| global[グローバルスタイル]

2025 年に向けて予想される主な変化は以下の通りです。

  • クラシックエディターの段階的廃止: WordPress 7.0 では完全にブロックエディターに統合される予定
  • テーマ.json の標準化: すべてのテーマでテーマ.json による設定管理が必須となる
  • ブロックパターンの充実: 再利用可能なブロック組み合わせが大幅に拡充される

ヘッドレス CMS 市場の急成長

ヘッドレス CMS 市場は 2024 年から 2025 年にかけて年間 30%以上の成長を続けています。WordPress もこの流れに対応し、REST API と GraphQL API の両方をサポートしています。

mermaidflowchart LR
  cms[WordPress CMS] -->|REST API| app1[React App]
  cms -->|GraphQL| app2[Next.js App]
  cms -->|REST API| app3[Vue.js App]
  cms -->|GraphQL| app4[Gatsby App]

  app1 -->|配信| user1[Web ユーザー]
  app2 -->|配信| user2[モバイルユーザー]
  app3 -->|配信| user3[PWA ユーザー]
  app4 -->|配信| user4[静的サイトユーザー]

ヘッドレス化の主なメリットは以下の点です。

  • パフォーマンス向上: 静的サイト生成によるページ読み込み速度の大幅改善
  • スケーラビリティ: CDN 活用による全世界への高速配信
  • セキュリティ強化: フロントエンドとバックエンドの分離による攻撃面の削減

モダン JavaScript フレームワークとの統合需要

企業サイトや大規模メディアでは、WordPress を CMS として活用しつつ、フロントエンドを React、Vue.js、Next.js などのモダンフレームワークで構築する需要が急増しています。

主要なフレームワークと WordPress の組み合わせ事例です。

フレームワーク活用シーン主なメリット
Next.js企業サイト、ブログSSR/SSG 対応、SEO 最適化
Nuxt.jsVue.js 愛用者向けVue.js エコシステム活用
Gatsby静的サイト重視超高速な静的サイト生成
ReactSPA 重視豊富なライブラリ、コンポーネント

課題

従来のテーマ開発からブロック開発への移行課題

従来の PHP ベースのテーマ開発に慣れた開発者にとって、ブロック開発への移行は大きな課題となっています。

mermaidstateDiagram-v2
  [*] --> classic_theme: 従来のテーマ開発
  classic_theme --> learning: 学習期間
  learning --> block_basics: ブロック基礎習得
  block_basics --> js_skills: JavaScript スキル習得
  js_skills --> react_knowledge: React 知識習得
  react_knowledge --> block_development: ブロック開発実践
  block_development --> [*]

  classic_theme --> resistance: 移行への抵抗
  resistance --> forced_migration: 強制移行
  forced_migration --> learning

主な移行課題は以下の通りです。

技術スタックの変化

従来の PHP 中心の開発から、JavaScript、React、TypeScript を含む技術スタックへの移行が必要です。

javascript// 従来のPHPテーマ開発
<?php
function my_theme_setup() {
    add_theme_support('post-thumbnails');
    add_theme_support('custom-logo');
}
add_action('after_setup_theme', 'my_theme_setup');
?>
javascript// ブロック開発(JavaScript/React)
import { registerBlockType } from '@wordpress/blocks';
import {
  InnerBlocks,
  useBlockProps,
} from '@wordpress/block-editor';

registerBlockType('my-theme/custom-block', {
  title: 'カスタムブロック',
  category: 'common',
  edit: ({ attributes, setAttributes }) => {
    const blockProps = useBlockProps();
    return (
      <div {...blockProps}>
        <InnerBlocks />
      </div>
    );
  },
  save: ({ attributes }) => {
    const blockProps = useBlockProps.save();
    return (
      <div {...blockProps}>
        <InnerBlocks.Content />
      </div>
    );
  },
});

開発ツールチェーンの習得

ブロック開発では、webpack、Babel、npm/yarn などのモダンなツールチェーンの理解が必須となります。

ヘッドレス導入時のパフォーマンスと SEO 課題

ヘッドレス構成は多くのメリットをもたらしますが、同時に新たな課題も生み出します。

パフォーマンス課題

ヘッドレス構成では、API 呼び出しによるデータ取得が必要になり、適切な最適化を行わないとかえってパフォーマンスが悪化する場合があります。

mermaidsequenceDiagram
  participant user as ユーザー
  participant frontend as フロントエンド
  participant wp as WordPress API
  participant db as データベース

  user->>frontend: ページアクセス
  frontend->>wp: データ要求
  wp->>db: クエリ実行
  db-->>wp: データ返却
  wp-->>frontend: JSON データ
  frontend-->>user: レンダリング完了

  Note over user,db: 複数のAPI呼び出しによる<br/>遅延の可能性

SEO 課題

静的サイト生成を適用しない場合、JavaScript でのレンダリングにより SEO に悪影響を与える可能性があります。

主な SEO 課題は以下の通りです。

  • 初期ページロードの遅延: JavaScript 実行完了まで内容が表示されない
  • メタタグの動的設定: ページごとのメタ情報設定の複雑化
  • 構造化データの実装: JSON-LD などの実装がより複雑になる

開発者のスキルセット変革の必要性

WordPress 開発者に求められるスキルセットが大きく変化しています。

従来の必要スキル

  • PHP
  • MySQL
  • HTML/CSS
  • jQuery
  • WordPress API

2025 年に向けて必要なスキル

  • JavaScript (ES6+)
  • React.js
  • TypeScript
  • Node.js
  • GraphQL
  • JAMstack
  • CI/CD
  • モダン CSS(CSS Grid、Flexbox)

この技術的ギャップを埋めるために、計画的な学習とスキルアップが重要になります。

解決策:ブロック × ヘッドレス二刀流

WordPress の未来は、ブロックエディターとヘッドレス構成を組み合わせた「二刀流」アプローチにあります。この手法により、コンテンツ管理の利便性とパフォーマンスの両方を実現できます。

Full Site Editing(FSE)の活用戦略

Full Site Editing は 2025 年の WordPress 開発の中核となる技術です。効果的な活用方法を段階的に実装していきましょう。

theme.json による設定の統一

FSE の基盤となる theme.json の設定から始めます。

json{
  "version": 2,
  "settings": {
    "color": {
      "custom": true,
      "palette": [
        {
          "name": "プライマリ",
          "slug": "primary",
          "color": "#1e3a8a"
        },
        {
          "name": "セカンダリ",
          "slug": "secondary",
          "color": "#f59e0b"
        }
      ]
    },
    "typography": {
      "fontSizes": [
        {
          "name": "小",
          "slug": "small",
          "size": "14px"
        },
        {
          "name": "中",
          "slug": "medium",
          "size": "18px"
        },
        {
          "name": "大",
          "slug": "large",
          "size": "24px"
        }
      ]
    }
  }
}

theme.json により、サイト全体のデザインシステムを統一的に管理できます。これにより、ブロックエディターでの編集時にも一貫したデザインを保てます。

カスタムブロックの開発

プロジェクト固有の機能はカスタムブロックとして実装します。

javascript// block.jsonによるブロック定義
{
    "apiVersion": 2,
    "name": "my-theme/hero-section",
    "title": "ヒーローセクション",
    "category": "design",
    "icon": "cover-image",
    "description": "ヒーロー画像とテキストを表示するブロック",
    "supports": {
        "align": ["wide", "full"],
        "color": {
            "background": true,
            "text": true
        }
    },
    "attributes": {
        "title": {
            "type": "string",
            "default": ""
        },
        "subtitle": {
            "type": "string",
            "default": ""
        },
        "backgroundImage": {
            "type": "object",
            "default": {}
        }
    }
}
javascript// edit.jsでの編集画面実装
import { __ } from '@wordpress/i18n';
import {
  PanelBody,
  TextControl,
} from '@wordpress/components';
import {
  InspectorControls,
  MediaUpload,
  MediaUploadCheck,
  useBlockProps,
} from '@wordpress/block-editor';

export default function Edit({
  attributes,
  setAttributes,
}) {
  const { title, subtitle, backgroundImage } = attributes;
  const blockProps = useBlockProps();

  return (
    <>
      <InspectorControls>
        <PanelBody title={__('ヒーロー設定', 'textdomain')}>
          <TextControl
            label={__('タイトル', 'textdomain')}
            value={title}
            onChange={(value) =>
              setAttributes({ title: value })
            }
          />
          <TextControl
            label={__('サブタイトル', 'textdomain')}
            value={subtitle}
            onChange={(value) =>
              setAttributes({ subtitle: value })
            }
          />
          <MediaUploadCheck>
            <MediaUpload
              onSelect={(media) =>
                setAttributes({ backgroundImage: media })
              }
              value={backgroundImage.id}
              render={({ open }) => (
                <button onClick={open}>
                  {__('背景画像を選択', 'textdomain')}
                </button>
              )}
            />
          </MediaUploadCheck>
        </PanelBody>
      </InspectorControls>
      <div {...blockProps}>
        <div
          className='hero-section'
          style={{
            backgroundImage: backgroundImage.url
              ? `url(${backgroundImage.url})`
              : 'none',
          }}
        >
          <h1>
            {title || __('タイトルを入力', 'textdomain')}
          </h1>
          <p>
            {subtitle ||
              __('サブタイトルを入力', 'textdomain')}
          </p>
        </div>
      </div>
    </>
  );
}

このようなカスタムブロックにより、サイト固有の機能をブロックエディター内で簡単に管理できるようになります。

REST API と GraphQL の使い分け

WordPress のヘッドレス化では、REST API と GraphQL の特性を理解して適切に使い分けることが重要です。

mermaidflowchart LR
  wp[WordPress] -->|REST API| simple[シンプルなデータ取得]
  wp -->|GraphQL| complex[複雑なデータ取得]

  simple -->|活用例| blog[ブログ投稿一覧]
  simple -->|活用例| page[固定ページ]

  complex -->|活用例| relation[関連データ一括取得]
  complex -->|活用例| custom[カスタマイズ要求]

REST API の活用場面

REST API は以下のような場面で効果的です。

javascript// 投稿一覧の取得
async function fetchPosts() {
  try {
    const response = await fetch(
      '/wp-json/wp/v2/posts?per_page=10'
    );
    const posts = await response.json();
    return posts;
  } catch (error) {
    console.error('投稿の取得に失敗しました:', error);
    return [];
  }
}

// 特定の投稿の取得
async function fetchPost(postId) {
  try {
    const response = await fetch(
      `/wp-json/wp/v2/posts/${postId}`
    );
    const post = await response.json();
    return post;
  } catch (error) {
    console.error('投稿の取得に失敗しました:', error);
    return null;
  }
}

REST API は学習コストが低く、シンプルなデータ取得に適しています。

GraphQL の活用場面

GraphQL は複雑なデータ構造やリレーションを含む場合に威力を発揮します。

javascript// GraphQLクエリの例
const GET_POST_WITH_CATEGORIES = `
  query GetPostWithCategories($id: ID!) {
    post(id: $id) {
      id
      title
      content
      excerpt
      author {
        name
        avatar
      }
      categories {
        nodes {
          name
          slug
        }
      }
      tags {
        nodes {
          name
          slug
        }
      }
      featuredImage {
        node {
          sourceUrl
          altText
        }
      }
    }
  }
`;

// GraphQLクエリの実行
async function fetchPostWithCategories(postId) {
  try {
    const response = await fetch('/graphql', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        query: GET_POST_WITH_CATEGORIES,
        variables: { id: postId },
      }),
    });

    const { data } = await response.json();
    return data.post;
  } catch (error) {
    console.error('GraphQLクエリが失敗しました:', error);
    return null;
  }
}

GraphQL では、必要なデータを 1 回のリクエストで効率的に取得できます。

使い分けの指針

項目REST APIGraphQL
学習コスト中〜高
データ効率標準
キャッシュ簡単複雑
適用場面シンプルな CRUD複雑なデータ取得

Next.js/Nuxt.js との最適な連携手法

モダンな JavaScript フレームワークと WordPress の連携により、高性能なサイトを構築できます。

Next.js との連携パターン

Next.js 13 以降の App Router を活用した実装例です。

javascript// app/posts/page.tsx - 投稿一覧ページ
import { Metadata } from 'next';

interface Post {
  id: number;
  title: { rendered: string };
  excerpt: { rendered: string };
  slug: string;
}

async function getPosts(): Promise<Post[]> {
  const res = await fetch(
    `${process.env.WORDPRESS_API_URL}/wp-json/wp/v2/posts`,
    {
      next: { revalidate: 60 }, // 60秒間キャッシュ
    }
  );

  if (!res.ok) {
    throw new Error('Failed to fetch posts');
  }

  return res.json();
}

export const metadata: Metadata = {
  title: 'ブログ投稿一覧',
  description: '最新のブログ投稿をご覧ください',
};

export default async function PostsPage() {
  const posts = await getPosts();

  return (
    <div className='posts-container'>
      <h1>ブログ投稿</h1>
      <div className='posts-grid'>
        {posts.map((post) => (
          <article key={post.id} className='post-card'>
            <h2>
              <a href={`/posts/${post.slug}`}>
                {post.title.rendered}
              </a>
            </h2>
            <div
              dangerouslySetInnerHTML={{
                __html: post.excerpt.rendered,
              }}
            />
          </article>
        ))}
      </div>
    </div>
  );
}
javascript// app/posts/[slug]/page.tsx - 個別投稿ページ
import { Metadata } from 'next';
import { notFound } from 'next/navigation';

interface Post {
  id: number;
  title: { rendered: string };
  content: { rendered: string };
  slug: string;
}

async function getPost(slug: string): Promise<Post | null> {
  const res = await fetch(
    `${process.env.WORDPRESS_API_URL}/wp-json/wp/v2/posts?slug=${slug}`,
    { next: { revalidate: 3600 } } // 1時間キャッシュ
  );

  if (!res.ok) {
    return null;
  }

  const posts = await res.json();
  return posts.length > 0 ? posts[0] : null;
}

export async function generateMetadata({
  params,
}: {
  params: { slug: string },
}): Promise<Metadata> {
  const post = await getPost(params.slug);

  if (!post) {
    return {};
  }

  return {
    title: post.title.rendered,
    description: '投稿の詳細をご覧ください',
  };
}

export default async function PostPage({
  params,
}: {
  params: { slug: string },
}) {
  const post = await getPost(params.slug);

  if (!post) {
    notFound();
  }

  return (
    <article className='post-detail'>
      <h1>{post.title.rendered}</h1>
      <div
        className='post-content'
        dangerouslySetInnerHTML={{
          __html: post.content.rendered,
        }}
      />
    </article>
  );
}

Nuxt.js との連携パターン

Nuxt.js 3 でのサーバーサイドレンダリング実装例です。

javascript// pages/posts/index.vue - 投稿一覧ページ
<template>
  <div class="posts-container">
    <h1>ブログ投稿</h1>
    <div class="posts-grid">
      <article
        v-for="post in posts"
        :key="post.id"
        class="post-card"
      >
        <h2>
          <NuxtLink :to="`/posts/${post.slug}`">
            {{ post.title.rendered }}
          </NuxtLink>
        </h2>
        <div v-html="post.excerpt.rendered" />
      </article>
    </div>
  </div>
</template>

<script setup>
const { data: posts } = await $fetch('/api/posts');

useHead({
  title: 'ブログ投稿一覧',
  meta: [
    {
      name: 'description',
      content: '最新のブログ投稿をご覧ください'
    }
  ]
});
</script>
javascript// server/api/posts.ts - APIルート
export default defineEventHandler(async (event) => {
  try {
    const posts = await $fetch(
      `${process.env.WORDPRESS_API_URL}/wp-json/wp/v2/posts`
    );
    return posts;
  } catch (error) {
    throw createError({
      statusCode: 500,
      statusMessage: '投稿の取得に失敗しました',
    });
  }
});

この構成により、WordPress を CMS として活用しつつ、モダンなフロントエンド技術でユーザー体験を向上させることができます。

具体例

実際のプロジェクトで活用できる実装パターンを、段階的に詳しく解説いたします。

ブロックテーマの実装パターン

モダンなブロックテーマの開発手順を段階別に説明します。

プロジェクト構造の設計

まず、適切なディレクトリ構造を作成しましょう。

cssmy-block-theme/
├── style.css
├── index.php
├── theme.json
├── functions.php
├── templates/
│   ├── index.html
│   ├── single.html
│   ├── page.html
│   └── archive.html
├── parts/
│   ├── header.html
│   ├── footer.html
│   └── navigation.html
├── patterns/
│   ├── hero-section.php
│   └── feature-grid.php
├── assets/
│   ├── css/
│   ├── js/
│   └── images/
└── src/
    ├── blocks/
    │   ├── custom-hero/
    │   └── feature-card/
    └── js/
        └── theme.js

テーマの基本設定

php<?php
// functions.php - テーマの初期化
function my_block_theme_setup() {
    // テーマサポートの追加
    add_theme_support('wp-block-styles');
    add_theme_support('align-wide');
    add_theme_support('editor-styles');
    add_theme_support('responsive-embeds');

    // エディタースタイルの読み込み
    add_editor_style('assets/css/editor-style.css');

    // ブロックパターンのカテゴリ追加
    register_block_pattern_category(
        'my-theme-patterns',
        array('label' => __('マイテーマパターン', 'my-block-theme'))
    );
}
add_action('after_setup_theme', 'my_block_theme_setup');

// スタイルとスクリプトの読み込み
function my_block_theme_enqueue_assets() {
    wp_enqueue_style(
        'my-block-theme-style',
        get_stylesheet_uri(),
        array(),
        wp_get_theme()->get('Version')
    );

    wp_enqueue_script(
        'my-block-theme-script',
        get_template_directory_uri() . '/assets/js/theme.js',
        array(),
        wp_get_theme()->get('Version'),
        true
    );
}
add_action('wp_enqueue_scripts', 'my_block_theme_enqueue_assets');
?>

ブロックパターンの作成

再利用可能なブロックパターンを作成します。

php<?php
// patterns/hero-section.php
?>
<?php
/**
 * Title: ヒーローセクション
 * Slug: my-theme/hero-section
 * Categories: my-theme-patterns
 * Description: 背景画像付きのヒーローセクション
 */
?>

<!-- wp:cover {"url":"<?php echo esc_url(get_template_directory_uri()); ?>/assets/images/hero-bg.jpg","id":123,"dimRatio":40,"align":"full","className":"hero-section"} -->
<div class="wp-block-cover alignfull hero-section">
    <span aria-hidden="true" class="wp-block-cover__background has-background-dim-40 has-background-dim"></span>
    <img class="wp-block-cover__image-background wp-image-123" alt="" src="<?php echo esc_url(get_template_directory_uri()); ?>/assets/images/hero-bg.jpg" data-object-fit="cover"/>

    <div class="wp-block-cover__inner-container">
        <!-- wp:group {"align":"wide","layout":{"type":"constrained"}} -->
        <div class="wp-block-group alignwide">
            <!-- wp:heading {"textAlign":"center","level":1,"fontSize":"huge"} -->
            <h1 class="wp-block-heading has-text-align-center has-huge-font-size">
                あなたのビジネスを次のレベルへ
            </h1>
            <!-- /wp:heading -->

            <!-- wp:paragraph {"align":"center","fontSize":"large"} -->
            <p class="has-text-align-center has-large-font-size">
                革新的なソリューションで成長を加速させましょう
            </p>
            <!-- /wp:paragraph -->

            <!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
            <div class="wp-block-buttons">
                <!-- wp:button {"backgroundColor":"primary","className":"is-style-fill"} -->
                <div class="wp-block-button is-style-fill">
                    <a class="wp-block-button__link has-primary-background-color has-background wp-element-button">
                        今すぐ始める
                    </a>
                </div>
                <!-- /wp:button -->
            </div>
            <!-- /wp:buttons -->
        </div>
        <!-- /wp:group -->
    </div>
</div>
<!-- /wp:cover -->

カスタムブロックの開発

プロジェクト専用の機能をカスタムブロックとして実装します。

javascript// src/blocks/feature-card/block.json
{
    "apiVersion": 2,
    "name": "my-theme/feature-card",
    "title": "機能カード",
    "category": "design",
    "icon": "id-alt",
    "description": "アイコン、タイトル、説明文を表示するカード",
    "supports": {
        "align": ["left", "center", "right"],
        "color": {
            "background": true,
            "text": true
        }
    },
    "attributes": {
        "title": {
            "type": "string",
            "default": ""
        },
        "description": {
            "type": "string",
            "default": ""
        },
        "icon": {
            "type": "string",
            "default": "star-filled"
        }
    },
    "editorScript": "file:./index.js",
    "editorStyle": "file:./editor.css",
    "style": "file:./style.css"
}
javascript// src/blocks/feature-card/index.js
import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import {
  PanelBody,
  TextControl,
  SelectControl,
} from '@wordpress/components';
import {
  InspectorControls,
  useBlockProps,
} from '@wordpress/block-editor';
import { Icon } from '@wordpress/icons';

const ICON_OPTIONS = [
  { label: '星', value: 'star-filled' },
  { label: 'チェック', value: 'yes-alt' },
  { label: '電球', value: 'lightbulb' },
  { label: 'ハート', value: 'heart' },
  { label: '設定', value: 'admin-settings' },
];

registerBlockType('my-theme/feature-card', {
  edit: ({ attributes, setAttributes }) => {
    const { title, description, icon } = attributes;
    const blockProps = useBlockProps({
      className: 'feature-card',
    });

    return (
      <>
        <InspectorControls>
          <PanelBody
            title={__('機能カード設定', 'my-block-theme')}
          >
            <TextControl
              label={__('タイトル', 'my-block-theme')}
              value={title}
              onChange={(value) =>
                setAttributes({ title: value })
              }
            />
            <TextControl
              label={__('説明文', 'my-block-theme')}
              value={description}
              onChange={(value) =>
                setAttributes({ description: value })
              }
            />
            <SelectControl
              label={__('アイコン', 'my-block-theme')}
              value={icon}
              options={ICON_OPTIONS}
              onChange={(value) =>
                setAttributes({ icon: value })
              }
            />
          </PanelBody>
        </InspectorControls>

        <div {...blockProps}>
          <div className='feature-card__icon'>
            <Icon icon={icon} size={48} />
          </div>
          <h3 className='feature-card__title'>
            {title ||
              __('タイトルを入力', 'my-block-theme')}
          </h3>
          <p className='feature-card__description'>
            {description ||
              __('説明文を入力', 'my-block-theme')}
          </p>
        </div>
      </>
    );
  },

  save: ({ attributes }) => {
    const { title, description, icon } = attributes;
    const blockProps = useBlockProps.save({
      className: 'feature-card',
    });

    return (
      <div {...blockProps}>
        <div className='feature-card__icon'>
          <Icon icon={icon} size={48} />
        </div>
        <h3 className='feature-card__title'>{title}</h3>
        <p className='feature-card__description'>
          {description}
        </p>
      </div>
    );
  },
});

スタイリングの実装

css/* src/blocks/feature-card/style.css */
.feature-card {
  padding: 2rem;
  border: 1px solid var(--wp--preset--color--primary);
  border-radius: 8px;
  text-align: center;
  background-color: var(--wp--preset--color--white);
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.feature-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}

.feature-card__icon {
  margin-bottom: 1rem;
  color: var(--wp--preset--color--primary);
}

.feature-card__title {
  margin: 0 0 1rem 0;
  font-size: 1.25rem;
  font-weight: 600;
  color: var(--wp--preset--color--contrast);
}

.feature-card__description {
  margin: 0;
  color: var(--wp--preset--color--contrast-2);
  line-height: 1.6;
}

/* レスポンシブ対応 */
@media (max-width: 768px) {
  .feature-card {
    padding: 1.5rem;
  }

  .feature-card__title {
    font-size: 1.125rem;
  }
}

ヘッドレス WordPress + Next.js 構成の実装

WordPress REST API と Next.js を組み合わせた実装例を詳しく解説します。

プロジェクト初期化

bash# Next.jsプロジェクトの作成
yarn create next-app@latest my-wordpress-frontend --typescript --tailwind --eslint

# 必要な依存関係の追加
cd my-wordpress-frontend
yarn add @types/node dotenv

環境設定

bash# .env.local - 環境変数の設定
WORDPRESS_API_URL=https://your-wordpress-site.com
NEXT_PUBLIC_SITE_URL=https://your-frontend-site.com

API クライアントの実装

javascript// lib/wordpress.ts - WordPress API クライアント
interface WordPressPost {
    id: number;
    date: string;
    slug: string;
    title: { rendered: string };
    content: { rendered: string };
    excerpt: { rendered: string };
    author: number;
    featured_media: number;
    categories: number[];
    tags: number[];
}

interface WordPressPage {
    id: number;
    slug: string;
    title: { rendered: string };
    content: { rendered: string };
}

class WordPressAPI {
    private baseURL: string;

    constructor() {
        this.baseURL = process.env.WORDPRESS_API_URL || '';
    }

    private async fetchAPI(endpoint: string, options: RequestInit = {}) {
        const url = `${this.baseURL}/wp-json/wp/v2/${endpoint}`;

        const defaultOptions: RequestInit = {
            headers: {
                'Content-Type': 'application/json',
            },
            ...options,
        };

        try {
            const response = await fetch(url, defaultOptions);

            if (!response.ok) {
                throw new Error(`API Error: ${response.status} ${response.statusText}`);
            }

            return await response.json();
        } catch (error) {
            console.error('WordPress API Error:', error);
            throw error;
        }
    }

    // 投稿一覧の取得
    async getPosts(params: {
        per_page?: number;
        page?: number;
        categories?: string;
        search?: string;
    } = {}): Promise<WordPressPost[]> {
        const queryParams = new URLSearchParams();

        Object.entries(params).forEach(([key, value]) => {
            if (value !== undefined) {
                queryParams.append(key, value.toString());
            }
        });

        return this.fetchAPI(`posts?${queryParams.toString()}`);
    }

    // 特定投稿の取得
    async getPost(slug: string): Promise<WordPressPost | null> {
        try {
            const posts = await this.fetchAPI(`posts?slug=${slug}`);
            return posts.length > 0 ? posts[0] : null;
        } catch (error) {
            return null;
        }
    }

    // 固定ページの取得
    async getPage(slug: string): Promise<WordPressPage | null> {
        try {
            const pages = await this.fetchAPI(`pages?slug=${slug}`);
            return pages.length > 0 ? pages[0] : null;
        } catch (error) {
            return null;
        }
    }

    // カテゴリー一覧の取得
    async getCategories() {
        return this.fetchAPI('categories');
    }

    // タグ一覧の取得
    async getTags() {
        return this.fetchAPI('tags');
    }
}

export const wordpressAPI = new WordPressAPI();

投稿一覧ページの実装

javascript// app/blog/page.tsx - ブログ一覧ページ
import { Metadata } from 'next';
import Link from 'next/link';
import { wordpressAPI } from '@/lib/wordpress';

export const metadata: Metadata = {
  title: 'ブログ | My WordPress Site',
  description: '最新のブログ投稿をお読みください',
};

async function BlogPage() {
  let posts;

  try {
    posts = await wordpressAPI.getPosts({ per_page: 12 });
  } catch (error) {
    console.error('投稿の取得に失敗しました:', error);
    posts = [];
  }

  if (posts.length === 0) {
    return (
      <div className='container mx-auto px-4 py-8'>
        <h1 className='text-3xl font-bold mb-8'>ブログ</h1>
        <p className='text-gray-600'>
          現在、表示できる投稿がありません。
        </p>
      </div>
    );
  }

  return (
    <div className='container mx-auto px-4 py-8'>
      <h1 className='text-3xl font-bold mb-8'>ブログ</h1>

      <div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6'>
        {posts.map((post) => (
          <article
            key={post.id}
            className='bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow'
          >
            <div className='p-6'>
              <h2 className='text-xl font-semibold mb-3'>
                <Link
                  href={`/blog/${post.slug}`}
                  className='text-gray-900 hover:text-blue-600 transition-colors'
                >
                  {post.title.rendered}
                </Link>
              </h2>

              <div
                className='text-gray-600 mb-4 line-clamp-3'
                dangerouslySetInnerHTML={{
                  __html: post.excerpt.rendered,
                }}
              />

              <div className='text-sm text-gray-500'>
                {new Date(post.date).toLocaleDateString(
                  'ja-JP'
                )}
              </div>
            </div>
          </article>
        ))}
      </div>
    </div>
  );
}

export default BlogPage;

個別投稿ページの実装

javascript// app/blog/[slug]/page.tsx - 個別投稿ページ
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { wordpressAPI } from '@/lib/wordpress';

interface PageProps {
  params: { slug: string };
}

export async function generateMetadata({
  params,
}: PageProps): Promise<Metadata> {
  const post = await wordpressAPI.getPost(params.slug);

  if (!post) {
    return {};
  }

  return {
    title: `${post.title.rendered} | My WordPress Site`,
    description: post.excerpt.rendered
      .replace(/<[^>]*>/g, '')
      .substring(0, 160),
  };
}

async function BlogPostPage({ params }: PageProps) {
  const post = await wordpressAPI.getPost(params.slug);

  if (!post) {
    notFound();
  }

  return (
    <div className='container mx-auto px-4 py-8'>
      <article className='max-w-4xl mx-auto'>
        <header className='mb-8'>
          <h1 className='text-4xl font-bold mb-4'>
            {post.title.rendered}
          </h1>

          <div className='text-gray-600 mb-6'>
            投稿日:{' '}
            {new Date(post.date).toLocaleDateString(
              'ja-JP'
            )}
          </div>
        </header>

        <div
          className='prose prose-lg max-w-none'
          dangerouslySetInnerHTML={{
            __html: post.content.rendered,
          }}
        />
      </article>
    </div>
  );
}

// 静的パス生成(オプション)
export async function generateStaticParams() {
  try {
    const posts = await wordpressAPI.getPosts({
      per_page: 100,
    });

    return posts.map((post) => ({
      slug: post.slug,
    }));
  } catch (error) {
    console.error(
      '静的パス生成中にエラーが発生しました:',
      error
    );
    return [];
  }
}

export default BlogPostPage;

キャッシュと ISR の設定

javascript// next.config.js - Next.js設定
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    domains: ['your-wordpress-site.com'],
  },

  // ISR (Incremental Static Regeneration) の設定
  experimental: {
    isrMemoryCacheSize: 0, // デフォルトは50MBまで
  },

  // リライト設定(WordPress サイトマップなど)
  async rewrites() {
    return [
      {
        source: '/sitemap.xml',
        destination: `${process.env.WORDPRESS_API_URL}/sitemap.xml`,
      },
      {
        source: '/robots.txt',
        destination: `${process.env.WORDPRESS_API_URL}/robots.txt`,
      },
    ];
  },
};

module.exports = nextConfig;

デュアル運用(管理画面+フロントエンド分離)の事例

WordPress 管理画面とヘッドレスフロントエンドを組み合わせる実践的な運用パターンを解説します。

mermaidflowchart TB
  editor[コンテンツ編集者] -->|記事作成| wp[WordPress 管理画面]
  wp -->|Webhook| deploy[自動デプロイ]
  wp -->|REST API| frontend[Next.js フロントエンド]

  deploy -->|ビルド| vercel[Vercel]
  vercel -->|配信| cdn[CDN]
  cdn -->|表示| user[エンドユーザー]

  wp -->|プレビュー| preview[プレビュー環境]
  preview -->|確認| editor

WordPress 側の設定

php<?php
// functions.php - Webhook とCORS設定
// CORS設定
function add_cors_http_header() {
    header("Access-Control-Allow-Origin: https://your-frontend-site.com");
    header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
    header("Access-Control-Allow-Headers: Content-Type, Authorization");
}
add_action('init', 'add_cors_http_header');

// 投稿保存時にWebhookを送信
function trigger_frontend_rebuild($post_id) {
    // 自動保存や下書きは無視
    if (wp_is_post_autosave($post_id) || wp_is_post_revision($post_id)) {
        return;
    }

    $post = get_post($post_id);

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

    // Vercel Deploy Hookに通知
    $webhook_url = get_option('vercel_deploy_hook');

    if ($webhook_url) {
        wp_remote_post($webhook_url, array(
            'method' => 'POST',
            'timeout' => 10,
            'body' => json_encode(array(
                'post_id' => $post_id,
                'post_slug' => $post->post_name,
                'action' => 'rebuild'
            )),
            'headers' => array(
                'Content-Type' => 'application/json'
            )
        ));
    }
}
add_action('save_post', 'trigger_frontend_rebuild');

// 管理画面にWebhook URL設定を追加
function add_webhook_settings() {
    add_settings_section(
        'webhook_settings',
        'フロントエンド連携設定',
        null,
        'general'
    );

    add_settings_field(
        'vercel_deploy_hook',
        'Vercel Deploy Hook URL',
        'webhook_url_input',
        'general',
        'webhook_settings'
    );

    register_setting('general', 'vercel_deploy_hook');
}
add_action('admin_init', 'add_webhook_settings');

function webhook_url_input() {
    $value = get_option('vercel_deploy_hook', '');
    echo '<input type="url" name="vercel_deploy_hook" value="' . esc_attr($value) . '" class="regular-text" />';
    echo '<p class="description">Vercel プロジェクトのDeploy Hook URLを入力してください</p>';
}
?>

プレビュー機能の実装

javascript// lib/preview.ts - プレビュー機能
export async function getPreviewPost(
  id: string,
  authToken: string
) {
  const response = await fetch(
    `${process.env.WORDPRESS_API_URL}/wp-json/wp/v2/posts/${id}?context=edit`,
    {
      headers: {
        Authorization: `Bearer ${authToken}`,
      },
    }
  );

  if (!response.ok) {
    throw new Error('プレビューの取得に失敗しました');
  }

  return response.json();
}

export function isPreviewMode(context: any): boolean {
  return context.preview === true;
}
javascript// pages/api/preview.ts - プレビューAPI
import { NextApiRequest, NextApiResponse } from 'next';

export default async function preview(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { secret, id, slug } = req.query;

  // シークレットキーの確認
  if (secret !== process.env.WORDPRESS_PREVIEW_SECRET) {
    return res
      .status(401)
      .json({ message: 'Invalid token' });
  }

  // 投稿の存在確認
  if (!id || !slug) {
    return res
      .status(400)
      .json({ message: 'Missing id or slug' });
  }

  // プレビューモードを有効化
  res.setPreviewData({
    id: id,
    slug: slug,
  });

  // プレビューページにリダイレクト
  res.redirect(`/blog/${slug}`);
}

段階的な移行戦略

既存の WordPress サイトをヘッドレス構成に移行する際の段階的アプローチです。

フェーズ 1: ハイブリッド運用開始
javascript// middleware.ts - 段階的移行のためのミドルウェア
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const url = request.nextUrl.clone();

  // 移行対象のパスリスト
  const migratedPaths = ['/blog', '/about', '/contact'];

  // まだ移行していないパスはWordPressにプロキシ
  const shouldProxy = !migratedPaths.some((path) =>
    url.pathname.startsWith(path)
  );

  if (shouldProxy) {
    url.href = `${process.env.WORDPRESS_SITE_URL}${url.pathname}${url.search}`;
    return NextResponse.redirect(url);
  }

  return NextResponse.next();
}

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};
フェーズ 2: SEO 対策とリダイレクト
javascript// next.config.js - SEO維持のためのリダイレクト設定
const nextConfig = {
  async redirects() {
    return [
      // 旧URL構造から新URL構造へのリダイレクト
      {
        source: '/category/:slug*',
        destination: '/blog/category/:slug*',
        permanent: true,
      },
      {
        source: '/tag/:slug*',
        destination: '/blog/tag/:slug*',
        permanent: true,
      },
      {
        source: '/:year(\\d{4})/:month(\\d{2})/:slug*',
        destination: '/blog/:slug*',
        permanent: true,
      },
    ];
  },

  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'X-Powered-By',
            value: 'Next.js + WordPress',
          },
        ],
      },
    ];
  },
};

この段階的アプローチにより、SEO の影響を最小限に抑えながら、安全にヘッドレス構成へ移行できます。

まとめ

WordPress 技術ロードマップ 2025 において、ブロックエディターとヘッドレス構成の「二刀流」アプローチが最適解となることをご紹介しました。

重要なポイントの振り返り

2025 年の WordPress 開発では、以下の技術的変革が中心となります。

ブロックエディターの完全活用

  • **Full Site Editing(FSE)**による統一的なサイト管理
  • theme.jsonを活用したデザインシステムの構築
  • カスタムブロック開発によるプロジェクト固有機能の実装
  • ブロックパターンによる効率的なコンテンツ作成

ヘッドレス構成の戦略的導入

  • REST APIGraphQLの適切な使い分け
  • Next.js/Nuxt.jsとの最適な連携パターン
  • 段階的移行戦略による安全な移行プロセス
  • SEO 対策とパフォーマンス最適化の両立

開発者のスキルセット進化

従来の PHP 中心の開発から、JavaScript、React、TypeScript を含むモダンな技術スタックへの移行が必要です。この変化は一朝一夕では実現できませんが、段階的な学習と実践により確実に習得できるでしょう。

推奨される学習パス

WordPress 開発者が 2025 年に向けて身につけるべき技術を優先順位順に整理します。

  1. JavaScript ES6+の習得:モダンな JavaScript 構文の理解
  2. React.js 基礎学習:ブロック開発に必要な基本概念
  3. WordPress Block Development:カスタムブロックの作成手法
  4. REST API 活用:ヘッドレス構成の基礎となる API 操作
  5. Next.js/Nuxt.js 実践:実際のプロジェクトでの応用

技術選択の指針

プロジェクトの規模や要件に応じて、適切な技術選択を行うことが重要です。

  • 小規模サイト:ブロックテーマ + FSE による効率的開発
  • 中規模サイト:部分的ヘッドレス化による段階的最適化
  • 大規模サイト:完全ヘッドレス構成による最大限のパフォーマンス

WordPress 技術の進化は加速し続けており、開発者にとって学習すべき技術が増え続けているのも事実です。しかし、これらの技術を段階的に習得することで、2025 年以降の WordPress 開発において競争力を保ち続けることができるでしょう。

特に「ブロック × ヘッドレス二刀流」のアプローチは、コンテンツ管理の利便性とサイトパフォーマンスを両立する現実的なソリューションとして、多くのプロジェクトで採用されることが予想されます。

今後も WordPress コミュニティの動向を注視しつつ、新しい技術にも柔軟に対応していくことが成功の鍵となるでしょう。

関連リンク

公式ドキュメント

フレームワーク・ツール

学習リソース

コミュニティ・サポート