2026年6月21日

2026年6月21日

WordPressでInertia.jsを使う方法:仕組みと最小構成のテーマ実装

Inertia.js は、サーバー側のルーティングとデータの用意はそのままに、画面を Vue・React・Svelte のコンポーネントで作り、ページ遷移をページ全体の再読み込みなしで行うためのライブラリです。Laravel と組み合わせて使われることが多く、公式のサーバー側アダプターは Laravel 用です(2026年11月時点)。WordPress 用の公式アダプターはありません。

この記事では、Inertia がサーバーとどうやり取りするかを確認したうえで、WordPress のテーマに最小限のサーバー側処理を書き、Vue 3 で投稿の一覧と個別ページを表示するまでを説明します。前提とするのは、2026年11月時点の公式ドキュメントにある Inertia v3 のプロトコルと、Vite の現行版です。

Inertia.jsの仕組み

Inertia のやり取りは、次の2種類だけです。

リクエスト サーバーが返すもの
最初のアクセス(ブラウザでURLを開いたとき) HTML。ページのデータ(ページオブジェクト)を JSON として埋め込む
2回目以降のページ遷移(X-Inertia: true ヘッダー付き) ページオブジェクトの JSON だけ

ページオブジェクトには、表示するコンポーネント名(component)、コンポーネントに渡すデータ(props)、URL(url)、アセットのバージョン(version)が入ります。ブラウザ側の Inertia は、リンクのクリックを横取りして同じ URL に X-Inertia: true 付きでリクエストし、返ってきた JSON のコンポーネントに差し替えます。

このほか、サーバー側には次の決まりがあります。

  • Inertia のリクエストへの JSON の応答には、X-Inertia: true と Vary: X-Inertia のヘッダーを付ける
  • ブラウザが送ってきたバージョン(X-Inertia-Version)がサーバーの現在のバージョンと違う GET リクエストには、409 Conflict と、移動先を示す X-Inertia-Location ヘッダーを返す。ブラウザはページ全体を読み込み直す
  • Inertia のリクエストに普通の HTML を返すと、ブラウザ側ではエラーとして扱われる

WordPress のテーマでこの決まりを守れば、Inertia のクライアントを使えます。

WordPressで使う方法の選択肢

方法は大きく2つあります。

  1. コミュニティ製のアダプターを使う:boxybird/inertia-wordpress などのプラグインが公開されています。ただし公式のものではなく、更新が止まっているものや、古い版のプロトコルを前提にしたものもあります。使う場合は、Inertia のクライアントの版と合うか、保守されているかを確認してください。
  2. テーマに最小限の処理を書く:上記の決まりは数十行の PHP で書けます。この記事ではこちらを説明します。

なお、目的が「WordPress のサイトでページ遷移を速くしたい」だけであれば、Inertia を入れる必要はありません。WordPress 本体の Interactivity API にもページ遷移の仕組みがあり、既存のテーマやプラグインとの相性を考えるとそちらが合う場合もあります。

テーマの構成

新しいテーマのフォルダー(例: wp-content/themes/my-inertia)に、次のファイルを用意します。

my-inertia/
├── style.css          テーマ情報
├── functions.php      Inertia の処理
├── index.php          ページごとのコンポーネントとデータの指定
├── vite.config.js
├── package.json
├── src/
│   ├── app.js
│   └── Pages/
│       ├── Index.vue
│       ├── Post.vue
│       └── NotFound.vue
└── dist/              ビルド結果(npx vite build で作られる)

style.css には、テーマとして認識させるための見出しを書きます。

/*
Theme Name: My Inertia
*/

サーバー側の処理を書く(functions.php)

functions.php に、ビルドしたファイルの読み込みと、ページを Inertia の形式で返す関数を書きます。

<?php
/**
 * Inertia.js の最小限のサーバー側処理(テーマの functions.php)
 */

function mytheme_inertia_manifest(): array {
	static $manifest = null;
	if ( null === $manifest ) {
		$file     = get_theme_file_path( 'dist/.vite/manifest.json' );
		$manifest = file_exists( $file ) ? (array) json_decode( (string) file_get_contents( $file ), true ) : array();
	}
	return $manifest;
}

function mytheme_inertia_version(): string {
	$file = get_theme_file_path( 'dist/.vite/manifest.json' );
	return file_exists( $file ) ? (string) md5_file( $file ) : '';
}

// ビルドした JavaScript と CSS を読み込む
add_action( 'wp_enqueue_scripts', function () {
	$entry = mytheme_inertia_manifest()['src/app.js'] ?? null;
	if ( ! $entry ) {
		return;
	}
	wp_enqueue_script_module( 'mytheme-app', get_theme_file_uri( 'dist/' . $entry['file'] ), array(), null );
	foreach ( $entry['css'] ?? array() as $i => $css ) {
		wp_enqueue_style( 'mytheme-app-' . $i, get_theme_file_uri( 'dist/' . $css ), array(), null );
	}
} );

// ページを Inertia の形式で返す(HTML か JSON)
function mytheme_inertia_render( string $component, array $props = array() ): void {
	$page = array(
		'component' => $component,
		'props'     => array_merge( array( 'errors' => new stdClass() ), $props ),
		'url'       => wp_unslash( $_SERVER['REQUEST_URI'] ?? '/' ),
		'version'   => mytheme_inertia_version(),
	);

	header( 'Vary: X-Inertia' );

	if ( ! empty( $_SERVER['HTTP_X_INERTIA'] ) ) {
		$client_version = wp_unslash( $_SERVER['HTTP_X_INERTIA_VERSION'] ?? '' );

		// ビルドが更新されていたら、ページ全体を読み込み直させる
		if ( 'GET' === $_SERVER['REQUEST_METHOD'] && $client_version !== $page['version'] ) {
			$home   = wp_parse_url( home_url() );
			$origin = $home['scheme'] . '://' . $home['host'] . ( isset( $home['port'] ) ? ':' . $home['port'] : '' );
			status_header( 409 );
			header( 'X-Inertia-Location: ' . $origin . $page['url'] );
			exit;
		}

		header( 'Content-Type: application/json; charset=utf-8' );
		header( 'X-Inertia: true' );
		echo wp_json_encode( $page );
		exit;
	}
	?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo( 'charset' ); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<script data-page="app" type="application/json"><?php echo wp_json_encode( $page, JSON_HEX_TAG | JSON_HEX_AMP ); ?></script>
<div id="app"></div>
<?php wp_footer(); ?>
</body>
</html>
	<?php
	exit;
}

要点は次のとおりです。

  • アセットのバージョン:Vite が書き出すマニフェストファイルのハッシュをバージョンにしています。ビルドし直すとバージョンが変わり、古い JavaScript を持ったままのブラウザには 409 が返って、ページ全体が読み込み直されます。
  • 最初のアクセス:<script data-page="app" type="application/json"> にページオブジェクトを埋め込み、その後ろに id="app" の要素を置きます。これが Inertia v3 のプロトコルの形です(以前の版では <div id="app"> の data-page 属性に入れていました)。JSON_HEX_TAG を付けて、データ中の </script> で埋め込みが壊れないようにしています。
  • JavaScript の読み込み:Vite の出力は ES モジュールなので、WordPress 6.5 で追加された wp_enqueue_script_module() で読み込みます。
  • $_SERVER の値:WordPress は $_SERVER の値にスラッシュを付け加えるため、wp_unslash() で戻しています。

ページごとにコンポーネントとデータを決める(index.php)

テンプレート階層で最後に使われる index.php で、表示中のページに応じたコンポーネント名とデータを渡します。mytheme_inertia_render() は応答を返したあと処理を終了します。

<?php
/**
 * index.php:表示するページに応じてコンポーネントとデータを決める
 */

if ( is_404() ) {
	mytheme_inertia_render( 'NotFound' );
}

if ( is_singular() ) {
	the_post();
	mytheme_inertia_render( 'Post', array(
		'post' => array(
			'title'   => get_the_title(),
			'date'    => get_the_date(),
			'content' => apply_filters( 'the_content', get_the_content() ),
		),
	) );
}

$posts = array();
while ( have_posts() ) {
	the_post();
	$posts[] = array(
		'id'    => get_the_ID(),
		'title' => get_the_title(),
		'url'   => wp_make_link_relative( get_permalink() ),
	);
}

mytheme_inertia_render( 'Index', array(
	'siteName' => get_bloginfo( 'name' ),
	'posts'    => $posts,
) );

ルーティングは WordPress のままなので、パーマリンク、カテゴリーやタグのアーカイブ、検索結果も、通常どおり WordPress がメインクエリを組み立てます。ここでは一覧系をすべて Index にまとめていますが、is_category() などで分ければ、ページの種類ごとに別のコンポーネントにできます。ページ送り(2ページ目以降)のリンクも必要ならデータに含めます。

フロントエンドを用意する(Vue 3とVite)

テーマのフォルダーで、必要なパッケージを入れます。

npm init -y
npm install vue @inertiajs/vue3
npm install -D vite @vitejs/plugin-vue

vite.config.js を作ります。テーマの PHP からビルド結果を探せるよう、マニフェストを出力する設定にします。

// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  input: 'src/app.js',
  build: {
    outDir: 'dist',
    manifest: true,
  },
});

トップレベルの input は現行の Vite の書き方です。それより前の版(Vite 7 まで)では build.rollupOptions.input に入口のファイルを指定します。使っている版の公式ドキュメントで確認してください。

src/app.js で Inertia のアプリを起動します。

// src/app.js
import 'vite/modulepreload-polyfill';
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';

createInertiaApp({
  resolve: (name) => {
    const pages = import.meta.glob('./Pages/**/*.vue', { eager: true });
    return pages[`./Pages/${name}.vue`];
  },
  setup({ el, App, props, plugin }) {
    createApp({ render: () => h(App, props) })
      .use(plugin)
      .mount(el);
  },
});

{ eager: true } で全ページのコンポーネントを1つのファイルにまとめています。ページごとに分割して読み込む形にすると、分割したファイルの URL をテーマのフォルダーに合わせる設定が別に必要になるため、まずはこの形で動かします。

ページのコンポーネントを作ります。PHP の mytheme_inertia_render() に渡した配列のキーが、そのまま props になります。

<!-- src/Pages/Index.vue -->
<script setup>
import { Link } from '@inertiajs/vue3';

defineProps({
  siteName: String,
  posts: Array,
});
</script>

<template>
  <main>
    <h1>{{ siteName }}</h1>
    <ul>
      <li v-for="post in posts" :key="post.id">
        <Link :href="post.url"><span v-html="post.title"></span></Link>
      </li>
    </ul>
  </main>
</template>
<!-- src/Pages/Post.vue -->
<script setup>
import { Link } from '@inertiajs/vue3';

defineProps({
  post: Object,
});
</script>

<template>
  <article>
    <h1 v-html="post.title"></h1>
    <p>{{ post.date }}</p>
    <div v-html="post.content"></div>
    <Link href="/">記事一覧へ戻る</Link>
  </article>
</template>
<!-- src/Pages/NotFound.vue -->
<template>
  <main>
    <h1>ページが見つかりません</h1>
  </main>
</template>

WordPress の題名は &#8217; のような文字参照を含むことがあり、本文は HTML なので、v-html で表示しています。v-html はエスケープせずに挿入するため、記事を投稿できるのが信頼できるユーザーだけであることを確認してください。Link href="/" は WordPress をドメインの直下に置いている場合の例です。

ビルドして動作を確かめる

npx vite build

dist/.vite/manifest.json と、dist/assets/ に JavaScript・CSS ができます。管理画面の「外観」→「テーマ」でこのテーマを有効にし、サイトを開いて投稿一覧が表示されることを確認します。

続いて、ブラウザの開発者ツールのネットワークタブを開いたまま、一覧の記事のリンクをクリックします。記事ページへのリクエストが JSON で返り(レスポンスヘッダーに X-Inertia: true)、ページ全体が読み込み直されずに表示が切り替われば動いています。

開発中は npx vite build --watch にしておくと、ファイルを保存するたびに書き出し直されます。ビルドし直すとバージョンが変わるので、開いたままのページで次にリンクをクリックしたときに、ページ全体が読み込み直されます。

注意点

ページキャッシュ:同じ URL でも、通常のアクセスには HTML、Inertia のリクエストには JSON を返します。ページキャッシュのプラグインやサーバー、CDN が X-Inertia ヘッダーを区別せずにキャッシュすると、JSON のはずの応答に HTML が返ってエラーになったり、その逆が起きたりします。キャッシュが Vary: X-Inertia を扱えるか確認し、扱えない場合は X-Inertia ヘッダー付きのリクエストをキャッシュの対象から外してください。

プラグインとの相性:wp_head() と wp_footer() が出力されるのは最初のアクセスのときだけです。ページ遷移では HTML が入れ替わらないため、ページごとに読み込まれる CSS や JavaScript、ページの読み込み時に動くことを前提にしたプラグイン(アクセス解析、広告、スライダーなど)は、遷移後に動かないことがあります。管理バーの「編集」リンクも遷移に合わせて変わりません。

題名とメタ情報:ページ遷移では <title> が更新されません。Inertia の Head コンポーネントで設定するか、SEO 系プラグインの出力に頼らない設計にする必要があります。

フォームと書き込み:この最小構成が扱うのはページの表示(GET)だけです。コメントやお問い合わせのように送信を伴う処理は、REST API を使うか、送信後のリダイレクトやバリデーションエラーの返し方を Inertia のプロトコルに合わせて実装する必要があります。

まとめ

  • Inertia.js には WordPress 用の公式アダプターがない。コミュニティ製のものは版と保守状況を確認してから使う
  • サーバー側の決まりは、最初のアクセスに HTML(ページオブジェクトを埋め込む)、X-Inertia 付きのリクエストに JSON、バージョン違いに 409 を返すこと
  • WordPress のテーマでは、index.php でコンポーネント名とデータを決め、共通の関数で HTML か JSON を返せば動く
  • Vite のマニフェストのハッシュをアセットのバージョンにし、wp_enqueue_script_module() で読み込む
  • ページキャッシュ、遷移後に動かないプラグイン、題名の更新、フォームの扱いを導入前に確認する

お気軽にご相談ください

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