Search by

robuust / craft-inertia

robuust

Opt-in public Inertia frontend integration for Craft CMS 6.

Package info

github.com/robuust/craft-inertia

Documentation

Type:craft-plugin

pkg:composer/robuust/craft-inertia

Statistics

Installs: 8

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 4

v0.1.0-beta.1 2026-11-12 11:57 UTC

This package is auto-updated.

Last update: 2026-11-13 10:45:06 UTC


README

Craft CMS 6 plugin for a project-owned Inertia/Vue public frontend. Craft owns content, native URLs, sites, publication rules and preview authority. The plugin connects explicitly selected pages to the official Inertia adapter. Composer identity is robuust/craft-inertia, PHP namespace Robuust\CraftInertia, native handle inertia; license is proprietary. Source and Composer downloads are public; usage remains subject to the proprietary license. This is a beta integration for Craft 6 beta.

How it works

For a homepage mapped to inertia:Site/Home, the request flows through:

flowchart TD
    A["Browser requests /"] --> B["Craft resolves the homepage entry and current site"]
    B --> C["Template: inertia:Site/Home"]
    C --> D["Plugin projects the entry and its fields"]
    D --> E["Official Inertia adapter"]
    E -->|"First visit"| F["HTML with SSR when configured and permitted"]
    E -->|"Navigation via an Inertia Link"| G["JSON with component name and page props"]
    F --> H["Site/Home.vue renders the page"]
    G --> H
Loading
  1. Craft resolves the requested URL, current site and entry. It applies its normal publication and preview rules before the plugin selects a renderer.
  2. The plugin listens to Craft's native entry-routing event. A section template such as inertia:Site/Home selects the Vue component Site/Home. Explicit route/controller destinations keep precedence.
  3. The plugin projects the actual matched entry into JSON-compatible page props. It includes every custom field in the entry's field layout under entry.fields.<handle>, including related entries and Matrix content. Authorized previews retain their matched draft/revision identity.
  4. The plugin renders through the official Inertia Laravel adapter, using an isolated public response factory. It excludes CP/global shares and applies the configured root view, asset version, flash/error handling and response privacy policy.
  5. A first visit receives an HTML document and page data. When guest SSR is configured and permitted, that HTML already contains the rendered Vue page. Navigation through an Inertia Link receives page JSON and updates Vue in the browser. Vue owns the page layout, components and interactions.

The main integration points are EntryTemplates, EntryProjection and EntryController, with the shared PublicRenderer for explicit project controllers.

Owner Responsibilities
Craft Fields, entry types, sections, native URLs, sites, publication, preview identity and authorization.
This plugin Opt-in entry rendering, automatic field projection, public response isolation and the Craft/Inertia integration policies.
Official Inertia adapters HTML/page-JSON negotiation, client navigation, asset-version handling and SSR transport.
Consuming project Vue pages and design, public Blade root, Vite builds, website queries, SEO content and runtime/deployment configuration.

Install

The package requires PHP 8.5, Craft ^6.0.0-beta.1, Laravel ^13.38 and inertiajs/inertia-laravel ^3.7. Install it from Packagist in an existing Craft 6 project that permits the Craft beta dependencies:

composer require 'robuust/craft-inertia:^0.1.0@beta' --with-all-dependencies --no-interaction
php artisan craft:plugin:install inertia --no-interaction
php artisan vendor:publish --tag=craft-inertia-config --no-interaction

No custom VCS repository or GitHub credentials are required. When migrating from a private VCS source, remove that repository entry and update the package lock. Craft discovers the plugin provider automatically. The package publishes its integration configuration; the consuming project supplies the frontend files.

For local package development, a consuming project's path repository can use:

{
  "repositories": [{
    "type": "path",
    "url": "../craft-inertia",
    "options": {"symlink": true, "versions": {"robuust/craft-inertia": "0.1.0-beta.1"}}
  }],
  "require": {"robuust/craft-inertia": "^0.1.0@beta"}
}

The local path must be available wherever Composer runs. Use the published package for CI and deployment environments that do not have that checkout.

Configure the consuming project

The one-time host setup consists of:

Location Project configuration
config/craft-inertia.php Select the public root view, for example frontend::app, and an explicit indexing policy when required.
Project service provider Register the public view namespace and exclude native CP/action paths from the public SSR resolver.
resources/views/app.blade.php Provide the HTML shell, Vite assets, <x-inertia::head /> and <x-inertia::app />.
resources/js/app.js Bootstrap the official Vue adapter with createInertiaApp().
resources/js/pages/ Supply the Vue components selected by section templates.
vite.config.js and package.json Configure Vue and the official Inertia Vite plugin, and build the client and SSR bundles.
Project runtime Configure SSR connectivity, a production SSR process when enabled, persistent asset storage and worker lifecycle.

Set the published configuration to the project's public namespace:

return [
    'rootView' => 'frontend::app',
    'indexingPolicy' => null,
];

For example, register the namespace and SSR exclusions in the project service provider's boot() method. When cpTrigger is null, first configure Craft's baseCpUrl with the dedicated control-panel origin, such as https://cms.example.test. This lets the same setup distinguish CP requests from the public website without a global /* exclusion:

use CraftCms\Cms\Cms;
use CraftCms\Cms\Support\Url;
use Illuminate\Support\Facades\View;
use Inertia\Inertia;

// Inside the project's service provider boot() method:
View::addNamespace('frontend', resource_path('views'));
$cpUrl = rtrim(Url::cpUrl(), '/');
$actionTrigger = trim(Cms::config()->actionTrigger, '/');
Inertia::withoutSsr([
    $cpUrl,
    $cpUrl.'?*',
    $cpUrl.'/*',
    $actionTrigger,
    $actionTrigger.'/*',
]);

The namespace separates the public root from Craft's CP root. Keep the plugin's own provider under native Craft discovery; do not register it a second time.

The official @inertiajs/vite plugin can resolve pages and handle development SSR from the normal application entry point. Cross-container development must make its SSR endpoint reachable through the official inertia.ssr.hot_url setting. Production uses the built SSR bundle and a separately supervised Node process. SSR/protocol settings belong to inertia.*; integration settings belong to craft-inertia.*. See INSTALLATION.md for the complete host setup and the official SSR guide.

Native entry templates

Keep the section's native URI and select a Vue component in its Template:

Content Native URI Template
Homepage single __home__ inertia:Site/Home
Articles articles/{slug} inertia:Site/Articles/Show
Structure entry Native nested URI inertia:Site/Places/Show

inertia:Site/Articles/Show resolves through the project's normal resolver, for example resources/js/pages/Site/Articles/Show.vue. All fields from the entry's field layout are automatically supplied under their native handles:

<p>{{ entry.fields.summary }}</p>
<img v-for="image in entry.fields.heroImage" :key="image.id" :src="image.url" :alt="image.alt" />

No field-selection configuration or admin selector is required. Craft's native project-config YAML already stores the section template and entry-type layout. The plugin supplies entry, site, preview, seo and official protocol props. It automatically includes nested Matrix and related entry fields, with bounded circular-reference handling. Assets use their original native URLs; other field types use their own native serializers. CONTENT.md defines the exact shapes and boundaries. All attached fields, including internal notes, are part of the page data, including fields hidden by editor layout conditions; the old pages[component].fields setting is ignored.

Page prop Contents
entry Native identity, title, URL, entry type and all attached custom fields under fields.
site Current site identity, language and base URL.
preview Whether Craft supplied a preview entry.
seo Entry title, canonical URL and the response's robots policy.
Protocol props Official adapter errors, flash and other protocol state.

Assets retain list cardinality, even when a field allows one image. They include the original URL, dimensions and alt text, with srcset: null. Table rows retain their native serialized column keys, such as col1 and col2. The project renders those values in Vue and owns description, hreflang and structured data.

Ordinary mapped entry pages need no project entry controller, copied Laravel entry route, custom PHP renderer or per-page SSR middleware. Projects still own Vue/layout/build, search/navigation queries and business rules.

The exact inertia: prefix is opt-in. Explicit native route/controller destinations take precedence. Twig/Blade, dynamic view routes, CP, actions, API/feed endpoints and unknown paths retain native handling. No catch-all is registered. An already configured project route remains ordinary Laravel code.

Bespoke project routes

Use Robuust\CraftInertia\Http\PublicRenderer for project controllers that select their own props. It runs the same isolated response factory, SSR eligibility and public protocol policy as the native entry renderer. Resolve props inside its callback and use the supplied robots directive in project metadata:

use CraftCms\Cms\Entry\Elements\Entry;
use CraftCms\Cms\Route\CurrentElement;
use Illuminate\Http\Request;
use Robuust\CraftInertia\Http\PublicRenderer;
use Symfony\Component\HttpFoundation\Response;

public function __invoke(Request $request, #[CurrentElement] Entry $entry): Response
{
    return app(PublicRenderer::class)->render(
        $request,
        'pages/work/[slug]',
        fn (string $robots): array => $this->pageProps($entry, $robots),
        $entry,
    );
}

The project owns pageProps(), including its selected content and SEO shape. This API adds no automatic entry, site, preview or seo props. The native entry renderer retains its existing automatic projection. Components passed directly to PublicRenderer use the host resolver; the stricter inertia: template syntax is unchanged. Craft's actual matched live or authorized preview entry is passed without requerying a canonical ID or slug. Missing entry context and non-200 page statuses always disable public SSR and indexing.

The optional publicPagePolicy(Request, Entry): bool config callback can restrict eligible pages or sections; it cannot allow authentication, preview, token or form-state contexts. Existing indexingPolicy(Request): bool remains default deny, and Craft's disallowRobots overrides it. Projects own their content selection and authorization. See RENDERING.md for controller, policy and error-response migration examples.

Keep web and craft context resolution before the controller. The shared renderer runs the public Inertia middleware itself; do not wrap it in a second Inertia protocol middleware. Native registered named/controller routes still match their own URI/domain/method and retain other middleware. No catch-all or routing listener is added for explicit controllers.

Existing manual integrations using HandleInertiaRequests and Inertia::render() remain supported. Those integrations retain responsibility for their own share isolation, guest SSR and indexing eligibility. The shared renderer centralizes that boundary for selected project props and custom error pages.

The native entry renderer currently has a fixed set of props. Project-wide navigation/settings queries need a deliberate project integration; global Inertia::share() values are excluded from that renderer. A dedicated public props hook is planned below.

Rendering, privacy and lifecycle

Integration config belongs to craft-inertia.*. Official adapter SSR/protocol config belongs only to inertia.*. Mapped pages use the project root and Vite manifest version, private/no-store headers and Vary: X-Inertia. Indexing defaults to deny; an explicit callable must return true, and Craft disallowRobots wins. Private/auth/token/preview/form-state requests remain non-indexable and do not reach SSR. Public guest SSR respects official configuration and host callbacks. CP/global shared props are excluded from mapped pages; the host factory/gateway are restored afterward. See RENDERING.md.

Native plugin disable/enable removes/restores the listener on a new boot, including with cached routes. Restart long-lived workers after lifecycle changes. The plugin does not rewrite section templates, DB state or project YAML. Disabling an inertia:-only template leaves normal native template interpretation; a working Twig/Blade fallback must be deliberately configured and retained. There is no automatic reconstruction of a previous Twig design.

INSTALLATION.md covers current setup, local-only migration, and native lifecycle/fallback behavior. FORMS.md covers native mutations/files, and CONTACT_FORM.md records bounded optional compatibility.

Planned extensions

These proposals extend the current integration. They are not implemented capabilities; the linked issues define their scope and acceptance criteria.

Issue Extension
#13 Safe frontend scaffolding and installation diagnostics.
#14 An explicit hook for project-owned public page props.
#15 Opt-in field serializers and responsive image profiles.
#16 TypeScript page props generated from Craft field layouts.
#17 A versioned compatibility and production acceptance matrix.

Verification scope

This PHP plugin contains no automated tests, fixtures or test harness. Its Vue application and frontend toolchain belong to the consuming Craft project. Use Composer validation and PHP syntax checks for the package, and verify native URLs, complete field data, preview identity, SSR/hydration, forms, privacy and fallback in the actual consuming project with its own locked dependencies.

Earlier development reports and test sources remain available in Git history. They describe their original versions and bounded outcomes; they do not establish current compatibility or production acceptance.

Four native account regressions and Contact Form's N01 nested-message failure remain separate open failures. The local unaccepted account adapter is excluded. Full STEP-05, full AC-06/AC-07, general compatibility, release and production acceptance remain incomplete. No PHP coverage percentage is claimed without a compatible driver. FRONTEND.md owns the agreed requirements.

Generic public project props, serializers and image profiles are documented in Public page extensions.