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つあります。
- コミュニティ製のアダプターを使う:
boxybird/inertia-wordpressなどのプラグインが公開されています。ただし公式のものではなく、更新が止まっているものや、古い版のプロトコルを前提にしたものもあります。使う場合は、Inertia のクライアントの版と合うか、保守されているかを確認してください。 - テーマに最小限の処理を書く:上記の決まりは数十行の 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 の題名は ’ のような文字参照を含むことがあり、本文は 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()で読み込む - ページキャッシュ、遷移後に動かないプラグイン、題名の更新、フォームの扱いを導入前に確認する