カテゴリー: WordPress

  • WordPress 6.x ブロックエディタの進化まとめ:サイトエディタ・パターン・スタイルブック完全ガイド

    WordPress 6.x系では、ブロックエディタ(Gutenberg)が大幅に進化しました。サイトエディタの安定化、パターンシステムの刷新、スタイルブックの導入など、サイト構築のワークフローが根本から変わっています。この記事では、WordPress 6.0〜6.7で追加された主要機能を実践的に解説します。

    サイトエディタ(Full Site Editing)の成熟

    WordPress 6.2でサイトエディタがベータから正式版に昇格しました。ヘッダー、フッター、サイドバーを含むサイト全体をブロックエディタで編集できます。

    サイトエディタでは「テンプレート」と「テンプレートパーツ」を直接編集できます。PHPテンプレートファイルを触らずに、ビジュアルエディタ上でレイアウトを組み替えられるのは革命的です。

    テンプレートの種類

    • ページテンプレート: 固定ページ・投稿ごとに異なるレイアウトを適用
    • アーカイブテンプレート: カテゴリ・タグ一覧ページのレイアウト
    • 検索結果テンプレート: 検索結果ページの表示形式
    • 404テンプレート: ページが見つからない場合の表示

    パターンシステムの刷新

    WordPress 6.3で「再利用ブロック」が「パターン」に統合されました。パターンには2種類あります。

    • 同期パターン(旧:再利用ブロック): 一箇所を編集すると全ての使用箇所に反映される
    • 非同期パターン: テンプレートとして挿入。挿入後は独立して編集可能

    テーマ開発者は、patternsディレクトリにPHPファイルを配置するだけでカスタムパターンを登録できます。

    // patterns/hero-section.php
    <?php
    /**
     * Title: ヒーローセクション
     * Slug: mytheme/hero-section
     * Categories: featured
     */
    ?>
    <!-- wp:cover {"dimRatio":50} -->
    <div class="wp-block-cover">
      <div class="wp-block-cover__inner-container">
        <!-- wp:heading {"textAlign":"center","level":1} -->
        <h1 class="has-text-align-center">サイトタイトル</h1>
        <!-- /wp:heading -->
        <!-- wp:paragraph {"align":"center"} -->
        <p class="has-text-align-center">サブタイトルテキスト</p>
        <!-- /wp:paragraph -->
      </div>
    </div>
    <!-- /wp:cover -->

    スタイルブック

    WordPress 6.2で導入されたスタイルブックは、サイト内の全ブロックの見た目を一覧で確認・編集できる機能です。「外観」→「エディター」→「スタイル」からアクセスできます。

    見出し、段落、ボタン、テーブルなど全てのブロックタイプのスタイルを、実際のプレビューを見ながらカスタマイズできます。CSSを一行も書かずに、サイト全体のデザインシステムを構築できるのは非常に強力です。

    theme.jsonの進化

    theme.jsonはブロックテーマの心臓部です。バージョン2(WordPress 6.1〜)では、設定項目が大幅に増えました。

    {
      "$schema": "https://schemas.wp.org/trunk/theme.json",
      "version": 3,
      "settings": {
        "color": {
          "palette": [
            { "slug": "primary", "color": "#1a73e8", "name": "プライマリ" },
            { "slug": "secondary", "color": "#333", "name": "セカンダリ" }
          ],
          "gradients": [],
          "custom": false
        },
        "typography": {
          "fontFamilies": [
            {
              "fontFamily": "-apple-system, BlinkMacSystemFont, sans-serif",
              "slug": "system",
              "name": "システムフォント"
            }
          ],
          "fontSizes": [
            { "slug": "small", "size": "14px", "name": "小" },
            { "slug": "medium", "size": "16px", "name": "中" },
            { "slug": "large", "size": "24px", "name": "大" }
          ]
        },
        "spacing": {
          "units": ["px", "rem", "%"]
        },
        "layout": {
          "contentSize": "800px",
          "wideSize": "1200px"
        }
      },
      "styles": {
        "blocks": {
          "core/heading": {
            "typography": { "fontWeight": "700" }
          }
        }
      }
    }

    WordPress 6.5〜6.7の注目機能

    • フォントライブラリ(6.5): Google Fontsやローカルフォントをアップロードして管理画面から適用
    • ブロックバインディングAPI(6.5): ブロックの属性をカスタムフィールドや外部データに動的にバインド
    • データビュー(6.6): 投稿・ページ一覧がグリッド表示に対応、フィルタ・ソート機能が強化
    • セクションスタイル(6.7): グループブロックにスタイルバリエーションを適用して、セクション単位でデザインを切り替え

    クラシックテーマからの移行

    既存のクラシックテーマ(PHPテンプレートベース)からブロックテーマへの移行は段階的に行えます。まずは「テーマサポート」を追加してブロックエディタの機能を有効にし、徐々にテンプレートをブロックベースに置き換えていくアプローチが現実的です。

    まとめ

    WordPress 6.x系のブロックエディタは、もはや単なる記事エディタではなく、サイト全体を構築するフルサイトエディティングツールへと進化しました。theme.jsonとパターンを活用すれば、コードを最小限に抑えつつ、高品質なテーマを開発できます。

  • 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を使えばビルド不要で最新コンテンツが反映される、最強のブログ基盤が構築できます。

  • WordPress プラグイン開発入門:OOP設計でメンテナブルなプラグインを作る

    WordPressプラグインの開発は、functions.phpにコードを追加する延長線上にありますが、本格的なプラグインはOOP(オブジェクト指向)で設計すべきです。この記事では、保守性が高く拡張しやすいプラグインの作り方を解説します。

    プラグインのディレクトリ構成

    my-awesome-plugin/
    ├── my-awesome-plugin.php    # メインファイル(エントリポイント)
    ├── includes/
    │   ├── class-plugin.php     # プラグインのメインクラス
    │   ├── class-admin.php      # 管理画面の処理
    │   ├── class-api.php        # REST APIエンドポイント
    │   └── class-db.php         # データベース操作
    ├── admin/
    │   ├── views/               # 管理画面テンプレート
    │   ├── css/
    │   └── js/
    ├── public/
    │   ├── css/
    │   └── js/
    ├── languages/               # 翻訳ファイル
    └── readme.txt               # WordPress.org用

    メインファイル

    <?php
    /**
     * Plugin Name: My Awesome Plugin
     * Description: プラグインの説明
     * Version: 1.0.0
     * Author: Your Name
     * Text Domain: my-awesome-plugin
     */
    
    if (!defined("ABSPATH")) exit;
    
    define("MAP_VERSION", "1.0.0");
    define("MAP_PLUGIN_DIR", plugin_dir_path(__FILE__));
    define("MAP_PLUGIN_URL", plugin_dir_url(__FILE__));
    
    require_once MAP_PLUGIN_DIR . "includes/class-plugin.php";
    
    // プラグイン初期化
    function map_init() {
        return MyAwesomePluginPlugin::get_instance();
    }
    add_action("plugins_loaded", "map_init");
    
    // アクティベーション・ディアクティベーション
    register_activation_hook(__FILE__, ["MyAwesomePluginPlugin", "activate"]);
    register_deactivation_hook(__FILE__, ["MyAwesomePluginPlugin", "deactivate"]);

    プラグインのメインクラス(シングルトン)

    <?php
    namespace MyAwesomePlugin;
    
    class Plugin {
        private static $instance = null;
    
        public static function get_instance() {
            if (self::$instance === null) {
                self::$instance = new self();
            }
            return self::$instance;
        }
    
        private function __construct() {
            $this->load_dependencies();
            $this->init_hooks();
        }
    
        private function load_dependencies() {
            require_once MAP_PLUGIN_DIR . "includes/class-admin.php";
            require_once MAP_PLUGIN_DIR . "includes/class-api.php";
            require_once MAP_PLUGIN_DIR . "includes/class-db.php";
        }
    
        private function init_hooks() {
            // 管理画面
            if (is_admin()) {
                new Admin();
            }
            // REST API
            add_action("rest_api_init", [new Api(), "register_routes"]);
            // フロントエンド
            add_action("wp_enqueue_scripts", [$this, "enqueue_public_assets"]);
        }
    
        public function enqueue_public_assets() {
            wp_enqueue_style(
                "map-public",
                MAP_PLUGIN_URL . "public/css/style.css",
                [],
                MAP_VERSION
            );
            wp_enqueue_script(
                "map-public",
                MAP_PLUGIN_URL . "public/js/main.js",
                [],
                MAP_VERSION,
                true
            );
            // JSにデータを渡す
            wp_localize_script("map-public", "mapConfig", [
                "ajaxUrl" => admin_url("admin-ajax.php"),
                "apiUrl" => rest_url("my-plugin/v1/"),
                "nonce" => wp_create_nonce("wp_rest"),
            ]);
        }
    
        public static function activate() {
            Db::create_tables();
            flush_rewrite_rules();
        }
    
        public static function deactivate() {
            flush_rewrite_rules();
        }
    }

    管理画面クラス

    <?php
    namespace MyAwesomePlugin;
    
    class Admin {
        public function __construct() {
            add_action("admin_menu", [$this, "add_menu"]);
            add_action("admin_init", [$this, "register_settings"]);
        }
    
        public function add_menu() {
            add_options_page(
                "My Plugin設定",
                "My Plugin",
                "manage_options",
                "my-awesome-plugin",
                [$this, "render_settings_page"]
            );
        }
    
        public function register_settings() {
            register_setting("map_settings", "map_api_key");
            register_setting("map_settings", "map_enabled");
        }
    
        public function render_settings_page() {
            include MAP_PLUGIN_DIR . "admin/views/settings.php";
        }
    }

    データベースクラス

    <?php
    namespace MyAwesomePlugin;
    
    class Db {
        public static function create_tables() {
            global $wpdb;
            $table = $wpdb->prefix . "map_logs";
            $charset = $wpdb->get_charset_collate();
    
            $sql = "CREATE TABLE IF NOT EXISTS $table (
                id bigint(20) NOT NULL AUTO_INCREMENT,
                user_id bigint(20) DEFAULT NULL,
                action varchar(50) NOT NULL,
                data longtext,
                created_at datetime DEFAULT CURRENT_TIMESTAMP,
                PRIMARY KEY (id),
                KEY user_id (user_id),
                KEY created_at (created_at)
            ) $charset;";
    
            require_once ABSPATH . "wp-admin/includes/upgrade.php";
            dbDelta($sql);
        }
    
        public static function insert_log($action, $data = null) {
            global $wpdb;
            return $wpdb->insert(
                $wpdb->prefix . "map_logs",
                [
                    "user_id" => get_current_user_id(),
                    "action" => $action,
                    "data" => is_array($data) ? json_encode($data) : $data,
                ],
                ["%d", "%s", "%s"]
            );
        }
    }

    セキュリティのベストプラクティス

    • ABSPATH チェック: 全PHPファイルの先頭で if (!defined("ABSPATH")) exit; を記述
    • Nonceの検証: フォーム送信やAjaxリクエストでは必ずnonce検証を行う
    • 権限チェック: current_user_can() で適切な権限を確認
    • サニタイズ: 入力値は sanitize_text_field()、出力は esc_html() で処理
    • SQLインジェクション対策: $wpdb->prepare() を使用

    まとめ

    OOP設計でプラグインを作ることで、コードの見通しが良くなり、チーム開発やメンテナンスが格段に楽になります。最初は少し面倒に感じるかもしれませんが、一度フレームワークを作ってしまえば、新しいプラグインでも使い回せます。WordPress公式ディレクトリへの公開を目指す場合も、この設計であればレビューを通過しやすいでしょう。

IP: 取得中...
216.73.216.31216.73.216.31