Back to Top

Inertia.js Adapter for WordPress and PHP

Updated 15 July 2026

The story behind Inertia.js Adapter for WordPress and PHP — bringing the Laravel SPA experience
To a WooCommerce ERP plugin, and turning it into a reusable package.

The Goal

We were building an ERP interface for WooCommerce — dashboards, orders, products — and we wanted it to feel like a modern single-page application:

Instant navigation, no full page reloads, shared state across pages. At the same time, we did not want to give up PHP as the source of truth.

WordPress already knows everything about the store; rebuilding all of that behind a REST API.

A separate client-side router, duplicated URL logic, and loading spinners everywhere felt like solving the wrong problem.

That is exactly the problem Inertia.js solves in the Laravel world:

The server keeps deciding which page to show and what data it gets, while the client swaps React components without reloading.

Laravel has an official adapter (inertia-laravel). WordPress has nothing. So we built one.

The Inspiration

Our inspiration was Inertia-Laravel itself. We read how it works and set a simple bar for ourselves.

Whatever the Laravel adapter gives Laravel developers, our adapter should give WordPress and PHP developers. Concretely, that meant implementing the full Inertia protocol:

  • First / standard visit -> a full HTML document with the page object embedded in the markup, so the React app can boot from it.
  • Inertia visit (an XHR carrying the “X-Inertia: true” header) -> a bare JSON page object, no HTML, so the client swaps props in place.
  • Stale assets -> when the client’s asset version (X-Inertia-Version) no longer matches the server’s, answer 409 with an X-Inertia-Location header, telling the client to do one hard reload and pick up new bundles.
  • Partial reloads -> honor X-Inertia-Partial-Data / X-Inertia-Partial-Except, so a page can refresh just one prop (say, the orders table) without recomputing everything else.

None of this exists in WordPress out of the box. We had to build it from the HTTP layer up.

First Iteration: An Adapter Inside the Plugin

We did not start with a package. We started pragmatically, with a single class inside the plugin (class-erp-inertia.php, about 230 lines).

A custom query var routes our ERP URLs to a render() method, which inspects the request headers and either prints the HTML shell or the JSON page object, then terminates the request.

That first version taught us most of the hard lessons.

The Difficulties

  1. WordPress is not Laravel — there is no middleware, no response objects. In Laravel, the adapter is a middleware that wraps a Response. In WordPress, we had to work with raw superglobals and headers: read $_SERVER[‘HTTP_X_INERTIA’] directly, send headers by hand, call status_header(409) ourselves, and exit() at the right moments so WordPress does not keep rendering a theme underneath our JSON.
  2. Embedding JSON safely inside HTML. On a first visit, the whole page object is embedded in a script tag. If any prop ever contained the string “” or a stray quote, it would break out of the tag — a real XSS risk. The fix was encoding with JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT, which guarantees those characters can never appear literally in the output.
  3. The 409 redirect URL kept doubling the site path. Our first attempt built the X-Inertia-Location URL with home_url() plus REQUEST_URI — and got /wp/wp/… on subdirectory installs, because
    REQUEST_URI already contains the site path. The correct approach is to build the absolute URL from scheme + host only and append REQUEST_URI. Small bug, hours of confusion, since it only shows up on subdirectory WordPress installs — which is to say, on a lot of real WordPress sites.
  4. Version pinning against WordPress’s React. WordPress core ships React 18, and we build with @wordpress/scripts and @wordpress/element to stay a good citizen in the WP ecosystem. Inertia’s newest client requires React 19. Upgrading React inside WordPress is not an option — core and other plugins depend on it — so we deliberately pinned @inertiajs/react to v2 and made the server adapter compatible with both the classic data-page attribute convention and the newer JSON script tag convention. Knowing your platform’s constraints beats chasing
    The latest version.
  5. Lazy props that are actually lazy. Partial reloads are pointless if the server still runs every query and then throws the data away. We made props accept closures, and we resolve them only after partial-reload filtering. A prop that the client did not ask for costs zero database queries. This one design decision made the ERP dashboards noticeably cheaper on navigation.
  6. Keeping Redux in sync with Inertia. Our pages use Redux for state, but Inertia delivers fresh props on every visit. We hooked Inertia’s router “navigate” event and seeded the store from the incoming page object — on first load from the embedded JSON, and on every SPA navigation (links, search, pagination, back/forward) from the XHR response. PHP stays the single source of truth; Redux is just the client-side mirror of it.

From Plugin Class to Standalone Package

Once the in-plugin adapter was stable, an obvious thought surfaced: nothing in it was actually specific to our ERP.

Other plugins on our roadmap would want the same SPA foundation. So we extracted it into a Composer package — webkulwp/inertia — and this was a rewrite with a design goal:

Framework-agnostic core, WordPress-aware by detection.

The package runs on plain PHP. Every place where we used WordPress helper,

we now feature-detect it: wp_json_encode() when it exists, json_encode() otherwise; status_header() or http_response_code(); sanitize_text_field() or a plain-PHP sanitiser;

the blog_charset option or a configurable charset; wp_head()/wp_footer() in the default HTML shell. When WordPress is loaded, clean fallbacks when it is not.

The same package works in a bare PHP app and integrates the moment WordPress transparently
Is present — no configuration needed.

The API became a fluent singleton instead of hard-coded plugin constants:

use Webkul\Inertia\Inertia;

    Inertia::instance()
        ->set_version( (string) ERP_SCRIPT_VERSION )
        ->set_root_view( array( Erp_Template::instance(), 'render_ui_template' ) )
        ->render( 'Order', array(
            'orderData' => fn() => $this->get_orders(), // lazy
        ) );
  • set_version() accepts a string or a callable (e.g. a build hash), powering the 409 stale-asset handshake.
  • set_root_view() lets the host application own the HTML shell — our plugin renders its own template, but the package ships a sensible default shell for projects that do not care.
  • The plugin side shrank to three lines of configuration; the deleted in-plugin class was 230 lines.

We shipped the package with examples for both React and vanilla JS, and the
plugin now simply requires it through composer.json.

What We Ended Up With

  • A WooCommerce ERP that navigates like an SPA: PHP routes and authorises, decides the component and its props; React renders; nothing reloads.
  • A reusable, framework-agnostic Inertia server adapter for PHP that any of our WordPress plugins — or any plain PHP project — can install with one composer require.
  • A clear separation of concerns: the plugin knows about orders and products; the package knows about the Inertia protocol; neither knows about the other’s internals.

Lessons Learned

  1. Build it inline first, extract it second. The in-plugin version taught us the real requirements; the package is better because it came second.
  2. Read the protocol, not just the docs. Implementing Inertia from its headers up (X-Inertia, X-Inertia-Version, X-Inertia-Partial-Data) gave us an adapter we fully understand and can debug.
  3. Respect the platform. Pinning to React 18 / Inertia v2 because WordPress ships React 18 was the right call, not a compromise.
  4. Security details are not optional. JSON-in-HTML escaping and header sanitisation were the difference between a demo and something we can ship to stores.
  5. Feature detection beats configuration. The package needs zero setup to become WordPress-aware — it just notices where it is running.

WordPress may never have an “official” Inertia adapter, but it turns out it does not need one.

The protocol is small, honest, and well designed — and now our plugins get the modern-monolith experience Laravel developers have been enjoying for years.

. . .

Leave a Comment

Your email address will not be published. Required fields are marked*


Be the first to comment.

Back to Top

Message Sent!

If you have more details or questions, you can reply to the received confirmation email.

Back to Home