React/Inertia Rendering (Optional)

Public Wrangler pages render via Blade/Livewire by default. This package also ships an optional Inertia/React rendering mode for public pages — the admin panel stays Blade/Livewire either way; this only swaps how visitors see public pages rendered.

Pick one per site. Both modes live in the same package and read the same WranglerPage/ WranglerComponent data — nothing about your content or admin workflow changes.

Prerequisites

An Inertia + React frontend. Jetstream's own "Jetstream with Inertia" option only ever installs a Vue-based Inertia stack (@inertiajs/vue3) — it doesn't offer React, so this isn't something jetstream:install sets up. Instead:

  • New app: start with Laravel's official React starter kit (laravel new your-app --react, or composer create-project laravel/react-starter-kit) before adding this package.
  • Existing app: add Inertia/React by hand — @inertiajs/react, react, react-dom, and @vitejs/plugin-react in package.json, inertiajs/inertia-laravel via Composer, and the React plugin wired into vite.config.js. See Inertia's own React setup guide — there's no artisan command that does this the way jetstream:install does for Vue.
  • laravel/fortify — used for a role-aware post-login redirect; already present via either path above.

Install

php artisan textstem:install-react

This publishes the React source (under resources/js/) and, if you confirm, appends TEXTSTEM_REACT_RENDERING=true to your .env. Two things it can't do for you, since they touch files you own and customize:

  1. In resources/js/app.tsx, route pages named textstem/* to the published layout:
    import PublicLayout from '@/layouts/public-layout';
    // ...
    layout: (name) => {
        if (name.startsWith('textstem/')) {
            return PublicLayout;
        }
        // ... your existing cases
    },
    
  2. In your own resources/js/types/global.d.ts, add the textstem prop to your sharedPageProps declaration (TypeScript doesn't merge two separate inline shapes assigned to the same property across files, so this can't be a standalone augmentation file the way wrangler.ts's other types are):
    import type { TextstemSharedProps } from '@/types/textstem-shared';
    // ...
    declare module '@inertiajs/core' {
        export interface InertiaConfig {
            sharedPageProps: {
                // ... your existing keys
                textstem: TextstemSharedProps;
                [key: string]: unknown;
            };
        }
    }
    
  3. Confirm @inertiajs/react, react, and react-dom are in package.json (see Prerequisites above), then npm run build (or npm run dev).

What gets published

Under resources/js/:

  • wrangler/registry.tsx, PageSlot.tsx, Container.tsx, ComponentBoundary.tsx, MissingComponent.tsx, AdminBar.tsx — the rendering engine. You shouldn't need to touch these.
  • wrangler/components/Prose.tsx (+ index.ts) — the one component this package ships a Blade view for (src/Wrangler/Components/Prose.php). Add your own the same way you would for Blade: create App\Wrangler\Components\{Name} (PHP) and a matching resources/js/wrangler/components/{Name}.tsx, then registerWranglerComponent('name', Name) in components/index.ts. See Notes-wranglercomponents.md for the PHP side — the build() method (not render()) is what the React path calls; ActiveComponent::build() already has a safe default (returns the component's raw config as data) if you don't override it, same as any component without an explicit build() today.
  • wrangler/page-templates/DefaultTemplate.tsx (+ registry.tsx, index.ts) — the one page template this package ships a Blade view for. Add your own the same way: register the template name your WranglerPage::template column uses.
  • layouts/public-layout.tsx, pages/textstem/page.tsx — the page shell and the actual Inertia page component ReactPageController renders (Inertia::render('textstem/page', ...)).
  • types/wrangler.ts — TypeScript types for page/component data.
  • types/textstem-shared.d.ts — exports TextstemSharedProps, the shape of the textstem prop (see below). You add this to your own global.d.ts by hand (step 2 above) rather than it being a drop-in augmentation — see that file's own header comment for why.

How it works

  • ReactPageController (bound over WranglerPageController while react_rendering is on) replaces the public publish()/notfound() rendering path with Inertia::render('textstem/page', ...), built from PageRenderer::renderPage(..., asArray: true) — the same headless data path the admin's live-preview/skeleton views already use, not a new rendering mode of its own.
  • A lightweight page-props cache sits in front of that (textstemapp.cache.* — the same config keys the Blade path's cache settings use), keyed on path + query, skipped for authenticated visitors and non-public-access pages. This is a separate store from the package's own PageCache (a raw-HTML file cache with no equivalent shape for an Inertia props array); the admin's "Clear Cache" action's Cache::flush() fallback clears both.
  • A textstem prop is shared on every Inertia page (TextstemServiceProvider::boot(), independent of react_rendering, since a host app may want it on its own Inertia pages too): canAccessAdmin, dashboardUrl/loginUrl/logoutUrl (resolved server-side, not with a client route() helper, so the published layout doesn't need Wayfinder or Ziggy), and adminBar (the floating "Dashboard / Edit Page / Show-Hide Editables" overlay, admin-only, mirroring the package's own <x-textstem::adminbar/> Blade component but reading from this shared prop so it stays correct across client-side Inertia navigation instead of only updating on a full page load).
  • Fortify's LoginResponse/RegisterResponse/TwoFactorLoginResponse are bound to role-aware versions while react_rendering is on (only if laravel/fortify is present): a visitor who can access-textstem-admin lands on your dashboard route after login, everyone else lands on /, rather than everyone being sent to config('fortify.home') regardless of role.

Configuration

config/textstemapp.php:

'react_rendering' => env('TEXTSTEM_REACT_RENDERING', false),

Everything else (cache, admin_roles, inline-component-editor) reuses the existing config keys documented elsewhere in this package — nothing new to configure there.

esc