従来のWordPressテーマ開発では、ヘッダーやフッターのマークアップを各テンプレートファイルに個別に記述する方法が一般的でした。そのため、ナビゲーションメニューやフッター要素を更新するたびに、該当するマークアップを含むすべてのテンプレートファイルを探し出し、複数箇所を修正する必要があります。こうした運用はメンテナンスの負担を増やすだけでなく、サイト全体で不整合が生じるリスクも高めます。
Radicleは、Acornのコンポーネントベースアーキテクチャを通じて、LaravelのBladeテンプレートエンジンをWordPressに導入します。マークアップをテンプレートごとに分散させるのではなく、再利用可能なコンポーネントとして一度定義し、テーマ全体から呼び出して使用します。UI要素を更新する際も、複数のテンプレートを修正する必要はなく、該当するコンポーネントファイルを1つ変更するだけで済みます。
WordPressテンプレート開発にコンポーネントベースアーキテクチャが必要な理由
WordPressでは、header.phpやfooter.phpといったテンプレートファイルをテーマディレクトリに配置し、各ページテンプレートからget_header()やget_footer()を呼び出す仕組みが一般的です。これはシンプルなサイトでは十分に機能しますが、規模が大きくなり構造が複雑になると、管理の難しさが顕在化します。
たとえば、カスタム投稿タイプ、ランディングページ、マーケティング用テンプレートなどを含むサイトでは、それぞれのテンプレートに同じナビゲーションのマークアップ、フッター構造、サイドバー要素を記述することになります。その結果、新しいメニュー項目の追加やフッター内のお問い合わせフォームの修正を行うたびに、複数のテンプレートファイルを個別に確認・更新する必要が生じます。
Radicleでは、Bladeテンプレートを resources/views/ 配下に整理し、レイアウト、コンポーネント、ブロックごとにディレクトリを分けて管理します。
components:見出しやボタンなど、再利用可能な自己完結型のUIコンポーネントを格納layouts:ページ全体の構造を定義するレイアウトテンプレートを配置blocks:WordPressのサイトエディターと連携するブロックテンプレートを保存
この構成により、各UI要素の「単一の情報源(Single Source of Truth)」が確立されます。たとえば x-heading コンポーネントでは、見出しのマークアップとスタイルを一箇所で定義します。テンプレート側でこのコンポーネントを呼び出すと、Bladeはその単一の定義を参照します。そのため、コンポーネントを更新すれば、サイト全体に反映されているすべてのインスタンスが自動的に更新されます。
重複コードを排除するプライマリレイアウトの構築方法
ナビゲーションやフッターのマークアップを各テンプレートに繰り返し記述する代わりに、テンプレート継承を活用してサイト全体の構造(サイトシェル)を定義する親レイアウトを作成できます。
レイアウトは、resources/views/components/ に配置するBladeコンポーネントファイルから始まります。layout.blade.php では、ページコンテンツを包み込むHTMLの基本構造を定義します。ここには、doctype 宣言、メタタグやアセット参照を含む <head> セクション、ナビゲーション構造、フッター要素などが含まれます。このレイアウトで重要な役割を果たすのが $slot 変数です。Bladeでは、$slot がコンテンツの挿入ポイントとして機能し、各ページ固有の内容がここに差し込まれます。
<html>
<head>
<title>{{ $title ?? 'My Site' }}</title>
</head>
<body>
<nav>
</nav>
<main>
{{ $slot }}
</main>
<footer>
</footer>
</body>
</html>
子テンプレートは、Bladeのコンポーネント構文を使用してこのレイアウトを拡張します。各ページテンプレートでは、<x-layout> コンポーネントタグでコンテンツ全体をラップします。Bladeはレイアウトコンポーネントをレンダリングする際に、$slot が配置された位置へ子テンプレートのコンテンツを自動的に挿入します。これにより、共通レイアウトを保ちながらページごとの内容を差し込むことができます。
<x-layout>
<h1>Page Title</h1>
<p>Page content goes here.</p>
</x-layout>
名前付きスロットを使用すると、ページタイトルなどの動的コンテンツを挿入するための追加の差し込みポイントを定義できます。デフォルトのスロットだけでなく、特定の用途に応じて名前付きスロットを明示的に定義することが可能です。name属性を指定した<x-slot>コンポーネントを使うことで、あらかじめ定義したスロットの位置に対応するコンテンツを渡すことができます。
<x-layout>
<x-slot name="title">
Custom Page Title
</x-slot>
<h1>Page Heading</h1>
<p>Page content.</p>
</x-layout>
レイアウトコンポーネント側では、スロット名に対応する変数を通じて名前付きスロットの内容にアクセスします。これにより、1つの子テンプレートからレイアウト内の複数の位置へコンテンツを差し込むことが可能になります。
一貫したデザインを実現する再利用可能なUIコンポーネントの作成
Bladeコンポーネントを使用すると、共通のUI要素のマークアップやスタイルを一元管理できます。
各テンプレートでTailwindのクラスを使ってボタンのマークアップを個別に記述する代わりに、マークアップをカプセル化したボタン用コンポーネントを作成します。このコンポーネントは、基本となるスタイルを統一しつつ、カスタマイズ用のprops(プロパティ)を受け取れるように設計します。
x-heading コンポーネントを、他のタイポグラフィ関連コンポーネントと組み合わせて使用するのは、その好例です。このコンポーネントは、出力するHTML要素(h1、h2、h3 など)を指定するlevelプロップと、視覚的なサイズを制御する sizeプロップを受け取ります。内部では、これらのプロップを対応するTailwindクラスにマッピングすることで、実装の詳細をコンポーネント内に閉じ込めています。
<x-heading level="h1" size="3xl">
Main Page Title
</x-heading>
<x-heading level="h2" size="2xl">
Section Heading
</x-heading>
resources/views/components/heading.blade.phpでは、Bladeの@propsディレクティブを使用して、受け取るプロップとそのデフォルト値を定義します。あわせて、マークアップやスタイルに関するロジックもこのファイル内にまとめます。コンポーネントは、渡されたプロップの値に応じてクラスを組み立て、適切なHTML要素を出力します。
x-linkコンポーネントも同様のパターンに従い、異なるリンクスタイルを切り替えるためのバリアントプロップをサポートします。バリアントを指定することで、通常のリンク表示、ボタン風スタイル、装飾なしのシンプルな表示などを切り替えられます。
x-buttonのようなUIコンポーネントも同じ考え方で設計できます。ボタンコンポーネントは、サイズやバリアントといったプロップを受け取り、用途に応じたスタイルを適用します。さらに、x-modalコンポーネントを通じてAlpine.jsのようなフレームワークと組み合わせれば、テンプレート内にJavaScriptを直接書き散らすことなく、モーダルなどのインタラクションを実装できます。
<x-button variant="primary" @click="showModal = true">
Open Modal
</x-button>
<x-modal x-show="showModal">
<p>Modal content</p>
</x-modal>
プロップは、文字列やブール値などの単純なデータを渡す場合に適しています。一方、マークアップを含むような複雑なコンテンツには、スロットの使用が適しています。たとえば、modalコンポーネントでは、HTMLをプロップとして渡すのではなく、モーダル内に表示するコンテンツ全体をデフォルトスロットで受け取ります。これにより、柔軟性を保ちながらコンポーネントの責務を明確に分離できます。
ビューコンポーザーでWordPressのデータをBladeテンプレートに接続する
ビューコンポーザーを使用すると、テンプレートをレンダリングする前に複数のソースからデータを集約できます。テンプレート内で投稿を直接クエリするのではなく、データの取得や変換を担当するcomposerクラスを作成し、テンプレート側には整形済みのデータだけを渡します。これにより、テンプレートは表示ロジックに集中でき、データ取得の処理を分離できます。
RadicleのPostモデルには、WordPressの投稿データを扱いやすくするための便利なメソッドが用意されています。
title():投稿タイトルを返す-
content():フィルタリング後の投稿本文を取得 -
excerpt():指定したワード数に基づいて抜粋を生成 -
permalink():投稿のURLを返す
特集画像については、hasThumbnail()で画像の有無を確認し、thumbnail()で指定したサイズの画像HTMLを取得します。
$post = Post::find(123);
echo $post->title();
echo $post->excerpt(30);
if ($post->hasThumbnail()) {
echo $post->thumbnail('large');
}
ビューコンポーザーのクラスはapp/View/Composers/に配置され、Roots\Acorn\View\Composerを継承します。このクラスでは、適用対象となるビューを、テンプレート名の配列を受け取る保護された静的プロパティ$viewsによって定義します。たとえばフロントページ用のコンポーザーでは、front-page.blade.phpテンプレートを対象にするため、'front-page'を指定します。
namespace AppViewComposers;
use AppModelsPost;
use RootsAcornViewComposer;
class FrontPage extends Composer
{
protected static $views = ['front-page'];
public function with()
{
return [
'recentPosts' => Post::recent(6)->get(),
'totalPosts' => Post::published()->count(),
];
}
}
with()メソッドは、キーがテンプレート内で使用される変数名となる配列を返します。値には、Eloquentコレクション、単一のモデル、あるいは任意のデータ構造を指定できます。いずれの場合も、このメソッドはテンプレートがレンダリングされる前に実行され、必要なデータを事前に準備します。
front-page.blade.phpテンプレートでは、これらの変数に直接アクセスできます。Bladeの@foreachディレクティブを使って投稿をループすると、各反復処理の中でPostモデルのメソッドを呼び出すことができます。
<div class="posts-grid">
@foreach ($recentPosts as $post)
<article>
@if ($post->hasThumbnail())
<img src="{{ $post->thumbnail('medium') }}" alt="{{ $post->title() }}">
@endif
<h2>
<a href="{{ $post->permalink() }}">{{ $post->title() }}</a>
</h2>
<p>{{ $post->excerpt(20) }}</p>
</article>
@endforeach
</div>
<p>Total posts: {{ $totalPosts }}</p>
このパターンでは、コンポーザーがクエリの実行やデータの整形を担当し、テンプレートはマークアップと表示ロジックに専念します。データ構造を変更したり、新しいクエリを追加したりする場合も、テンプレートファイルを修正することなく、コンポーザー側を更新するだけで対応できます。
BladeレンダリングによるWordPressサイトエディタのビルディングブロック
Radicleでは、Bladeテンプレートを用いたサイトエディタブロックのサーバーサイドレンダリングを採用しています。WordPress管理画面ではJavaScriptのエディタコンポーネントがブロックUIを処理し、フロントエンドの出力はBladeテンプレートが担います。
これにより、ブロックの編集インターフェースは(たとえば)Reactで構築しつつ、本番環境のHTMLはBladeでレンダリングすることができます。
wp acorn make:blockコマンドを実行すると、各ブロックに対して3つのファイルが生成されます。
-
app/Blocks/内のPHPクラス:サーバーサイドのロジックを管理します。 -
resources/js/editor/内のJSXコンポーネント:ブロックエディターのインターフェースを定義します。 -
resources/views/blocks/内のBladeテンプレート:フロントエンドの出力をレンダリングします。
たとえばwp acorn make:block latest-postsを実行すると、LatestPosts.php、latest-posts.block.jsx、latest-posts.blade.phpが生成されます。JSXファイルではブロックの属性やエディターコントロールを定義し、動的ブロックではInspectorControlsを使用してブロックのサイドバーに設定項目を追加します。@wordpress/componentsから各種コントロールをインポートし、それらを組み合わせて設定インターフェースを構築します。
import { InspectorControls } from '@wordpress/block-editor';
import { RangeControl, ToggleControl, RadioControl } from '@wordpress/components';
export const attributes = {
posts: { type: 'number', default: 5 },
displayFeaturedImage: { type: 'boolean', default: false },
postLayout: { type: 'string', default: 'list' },
};
export const edit = ({ attributes, setAttributes }) => {
return (
<>
<InspectorControls>
<RangeControl
label="Number of posts"
value={attributes.posts}
onChange={(value) => setAttributes({ posts: value })}
min={1}
max={10}
/>
<ToggleControl
label="Display featured image"
checked={attributes.displayFeaturedImage}
onChange={(value) => setAttributes({ displayFeaturedImage: value })}
/>
<RadioControl
label="Layout"
selected={attributes.postLayout}
options={[
{ label: 'List', value: 'list' },
{ label: 'Grid', value: 'grid' },
]}
onChange={(value) => setAttributes({ postLayout: value })}
/>
</InspectorControls>
</>
);
};
BlocksServiceProvider.phpのrender_blockフィルタは、ブロックのレンダリング処理をフックし、属性をBladeテンプレートへ渡します。このフィルタは、ブロックのコンテンツおよびブロックデータも受け取ります。処理内ではブロック名を判別し、必要なデータをクエリしたうえで、レンダリング済みのBladeビューを返します。
add_filter('render_block', function ($block_content, $block) {
if ($block['blockName'] === 'radicle/latest-posts') {
$attributes = $block['attrs'] ?? [];
$posts = get_posts([
'numberposts' => $attributes['posts'] ?? 5,
'post_status' => 'publish',
]);
return view('blocks.latest-posts', [
'posts' => $posts,
'displayFeaturedImage' => $attributes['displayFeaturedImage'] ?? false,
'postLayout' => $attributes['postLayout'] ?? 'list',
]);
}
return $block_content;
}, 10, 2);
Bladeテンプレートでは、PHPの配列を用いて条件付きレイアウトを管理できます。たとえば、レイアウト設定をネストされた配列として定義し、レイアウト名を対応するCSSクラスやHTML要素にマッピングします。
@php
$layoutConfig = [
'grid' => [
'container' => 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6',
'element' => 'div',
],
'list' => [
'container' => 'space-y-6',
'element' => 'div',
],
];
$config = $layoutConfig[$postLayout];
@endphp
<div class="{{ $config['container'] }}">
@foreach ($posts as $post)
<article>
@if ($displayFeaturedImage && has_post_thumbnail($post))
{{ get_the_post_thumbnail($post, 'medium') }}
@endif
<h3>{{ $post->post_title }}</h3>
</article>
@endforeach
</div>
このパターンを採用することで、テンプレートロジックを重複させることなく、エディター側の設定に応じてフロントエンドの出力を柔軟に調整できるブロックを構築できます。役割分担も明確で、JavaScriptがユーザーインターフェースを担当し、PHPがデータの取得やクエリを処理し、Bladeがマークアップ構造の生成を担います。
KinstaのキャッシュがBladeのコンパイル済みビューのパフォーマンスを向上させる仕組み
Bladeのコンパイルでは、.blade.phpテンプレートがプレーンなPHPコードへ変換されます。テンプレートとBlade構文の解析が完了すると、コンパイル済みファイルはstorage/framework/views/に保存されます。この処理はページが読み込まれるたびに実行されるわけではなく、テンプレートが変更されたときにのみ再コンパイルされます。それ以外のリクエストではコンパイル処理はスキップされ、キャッシュされたPHPファイルがそのまま実行されます。
このプロセスでは、Bladeディレクティブが対応するPHPコードに変換されます。たとえば、@foreachディレクティブはforeachループへ、{{ $variable }}構文はエスケープ処理付きのecho文へと変換されます。
Kinstaのサーバーレベルキャッシュ(エッジキャッシュおよびRedisオブジェクトキャッシュ)は、Bladeのコンパイルキャッシュと組み合わせることで、複数層のパフォーマンス最適化を実現します。コンパイル済みビューはその中間層に位置し、上位のキャッシュレイヤーと下位の実行最適化の双方から恩恵を受けます。
デプロイ時には、テンプレートの変更をステージング環境や本番環境へ反映する際に、ビューキャッシュをクリアする必要があります。Bladeは開発中にテンプレートの変更を自動検出しますが、本番デプロイではphp artisan view:clearまたはwp acorn view:clearを実行し、ビューキャッシュを明示的に削除することが推奨されます。
最新のWordPressテーマ開発を支えるRadicleとKinsta
RadicleのLaravelに着想を得た構造と、Kinstaのマネージドインフラストラクチャを組み合わせることで、WordPressプロジェクトをスケーラブルに運用するための強固な基盤を構築できます。複数のクライアントサイトを管理するエージェンシーであれば、プロジェクト間で共通のコンポーネントパターンを再利用できます。開発者にとっても、WordPressとの互換性を保ちながら、慣れ親しんだLaravelの規約に沿って開発できる点は大きなメリットです。
次のステップは、現在の環境によって異なります。新規プロジェクトの場合は、Radicleをインストールし、最初のBladeコンポーネントを作成するところから始めましょう。既存テーマを移行する場合は、繰り返し使用しているマークアップパターンを洗い出し、セクション単位でコンポーネント化していきます。特にナビゲーション、ヘッダー、フッターなど、効果がすぐに現れやすい領域から着手するのがおすすめです。
最新の開発ワークフローに対応したマネージドWordPress環境をお探しであれば、Kinstaをお試しください。RadicleやAcornなどのツールとスムーズに連携できる機能を提供しています。