Installation

Installation Guide

Installation Guide

This guide covers installing medialight/textstem-laravel into a new or existing Laravel application.


Requirements

PHP and Laravel

Requirement Version
PHP >= 8.3
Laravel 10, 11, 12, or 13
Livewire ^4.0
Database MySQL 8+ or PostgreSQL 13+ recommended

PHP Extensions

The following PHP extensions must be enabled:

Extension Used for
gd or imagick Image processing via Glide
pdo Database access
fileinfo File type detection on upload
zip Export commands

System Packages

Package Used for
Node.js + npm (or Bun) Compiling Jetstream frontend assets

Step 1 -- Add the Repository

The package is hosted on a private GitHub repository. Add it to the repositories array in your host app's composer.json before requiring it:

"repositories": [
    {
        "type": "vcs",
        "url": "git@github.com:MeccaMedialight/textstem-laravel.git"
    }
]

Ensure your machine or CI environment has SSH access to that repository via a deploy key or personal access token.


Step 2 -- Install via Composer

composer require medialight/textstem-laravel

Laravel's package auto-discovery registers all service providers and facade aliases automatically. No manual entry in config/app.php is needed.


Step 3 -- Run the Post-Install Wizard

php artisan textstem:post-install

The wizard prompts for an installation type and then publishes all required files:

Option What it does
Vanilla - no Jetstream Skips Jetstream installation
Jetstream with Livewire Runs jetstream:install livewire
Jetstream with Inertia Runs jetstream:install inertia (Vue)

If you want the package's own optional React rendering mode for public pages (see Step 12), none of these three options set that up on their own -- Step 12 explains what a React frontend actually requires.

After the stack choice is confirmed, the wizard publishes:

  • config/textstemapp.php, config/openai.php, config/chunk-upload.php (tag: textstem-config)
  • Wrangler views to resources/views/wrangler/ (tag: textstem-components)
  • Auth views to resources/views/auth/ (tag: textstem-components)
  • JS/CSS assets to public/vendor/medialight/textstem/ including TinyMCE (tag: public)
  • Jetstream views (tag: jetstream-views)

Build Frontend Assets (Jetstream installs only)

If you chose a Jetstream stack, compile the frontend before continuing:

npm install && npm run build
# or, if you use Bun:
bun install && bun run build

Step 4 -- Configure Sanctum

The API uses Laravel Sanctum for token authentication. Jetstream installs Sanctum automatically. If you chose the Vanilla option, publish the Sanctum config and migrations manually:

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

If you need cookie-based (stateful) API authentication, add your app's domain to the stateful array in config/sanctum.php:

'stateful' => [
    'localhost',
    'your-app.test',
    env('APP_URL'),
],

Token-based (Bearer) API requests work without this change.


Step 5 -- Configure Environment

Ensure your .env contains a working database connection before running migrations:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_database
DB_USERNAME=your_user
DB_PASSWORD=your_password

Then add the package-specific variables below. All have defaults in config/textstemapp.php; only set what your app needs to change.

Core

# Email address(es) that receive contact form notifications (comma-separated)
CONTACT_NOTIFICATIONS_EMAIL=you@example.com

# Set to true to redirect all outbound mail to CONTACT_NOTIFICATIONS_EMAIL instead of real recipients
EMAIL_TEST_MODE=false

# Items per page in admin list views
PAGINATION_ITEMS_PER_PAGE=10

# Storage disk for uploaded assets (matches a key in config/filesystems.php)
ASSET_DISK=public

Page Cache

WRANGLER_CACHE_ENABLE=false
WRANGLER_CACHE_TTL=3600
# HTTP cache-control header value: public or private
WRANGLER_CACHE_MODE=private
WRANGLER_CACHE_WITHQUERY=false

TinyMCE

# Leave blank to use the locally published TinyMCE build (no account required)
TINYMCE_API_KEY=

OpenAI / AI Features

OPENAI_API_KEY=sk-...
OPENAI_ENABLED=true
OPENAI_USE_QUEUE=true
OPENAI_MODEL=gpt-4o-mini

Google Cloud Vision

Image comparison and analysis features use Google Cloud Vision. Provide a service account credentials file:

GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json

If you do not use image AI features, set textstemapp.images.comparison_driver to fingerprint in config/textstemapp.php to skip Cloud Vision entirely.

Accessibility Analysis

ACCESSIBILITY_ENABLED=true
ACCESSIBILITY_USE_QUEUE=true
ACCESSIBILITY_WCAG_LEVEL=AA         # A, AA, or AAA
ACCESSIBILITY_CHECK_ON_SAVE=false
ACCESSIBILITY_STORE_REPORTS=true
ACCESSIBILITY_REPORT_RETENTION_DAYS=30

SEO Analysis

SEO_ENABLED=true
SEO_USE_QUEUE=true
SEO_USE_AI=true
SEO_CHECK_ON_SAVE=false
SEO_STORE_REPORTS=true
SEO_REPORT_RETENTION_DAYS=30

File Uploads

TEXTSTEM_ALLOWED_EXTENSIONS=jpg,jpeg,png,gif,svg,webp,pdf,doc,docx,xls,xlsx,ppt,pptx,txt,zip,mp3,wav,mp4,mov,avi,wmv
TEXTSTEM_MAX_FILE_SIZE=10240             # kilobytes; default is 10 MB
TEXTSTEM_STORAGE_PATH=public/uploads
TEXTSTEM_UPLOADS_USE_QUEUE=false
TEXTSTEM_UPLOADS_ENABLE_FINGERPRINT=true
TEXTSTEM_UPLOADS_GENERATE_THUMBNAILS=true
TEXTSTEM_UPLOADS_EXTRACT_METADATA=true

Redis (optional)

TEXTSTEM_USE_REDIS=false

Set to true if your environment has Redis available. The package will use it for caching and queues.

Admin UI

DARK_MODE=false

Step 6 -- Run Migrations

Installing into an existing app? If your app already has a users table, the package migration 2014_10_12_000000_create_users_table.php will conflict and fail. Skip it by inserting the migration filename into the migrations table manually, then run php artisan migrate. The textstem:migrations:sync command can do this in bulk -- see the artisan commands reference.

Run all outstanding migrations:

php artisan migrate

This creates the following tables (in addition to Laravel defaults):

Table Description
users Extended user model with roles and Jetstream traits
options Global site options
events Event records
previews Page preview snapshots
categories Shared category model for pages, posts, and assets
wrangler_pages Page-builder pages
wrangler_components Page components
assets Media asset library
posts Blog/news posts
post_collections Post groupings
redirects URL redirect rules
tags / taggables Spatie tag tables
messages In-app messages
roles / permissions Spatie permission tables
personal_access_tokens Sanctum API tokens
prompts AI prompt library
seo_reports Stored SEO analysis results
accessibility_reports Stored accessibility analysis results
orchestration_* AI orchestration state tables

Step 7 -- Set Up Asset Storage

Run Laravel's storage link command so uploaded files in the public disk are accessible via the web:

php artisan storage:link

For S3 storage, configure the s3 disk in config/filesystems.php and set ASSET_DISK=s3 in .env.


Step 8 -- Set Up Queues

AI generation, accessibility and SEO analysis, and asset processing jobs are dispatched to a queue. Configure a real queue driver for production:

QUEUE_CONNECTION=redis     # or database, sqs, etc.

Start a worker:

php artisan queue:work --queue=default --tries=3

For production, manage the worker process with Supervisor or Laravel Horizon.


Step 9 -- Register the Scheduler

The package registers scheduled commands automatically via the service provider. Add the scheduler call to your server crontab so they execute:

* * * * * cd /path-to-your-app && php artisan schedule:run >> /dev/null 2>&1

Commands the package schedules:

Command Schedule Description
textstem:prune-reports Daily at 03:00 Removes old SEO and accessibility reports
textstem:health-check Daily at 01:00 Checks application health
cache:manage clear --warm-up Mondays at 01:30 Clears and re-warms the cache

Step 10 -- Create a Super Admin User

Register a user through your app's registration flow, then create the super-admin role (if it does not already exist) and assign it:

use Medialight\Textstem\Models\User;
use Spatie\Permission\Models\Role;

$role = Role::firstOrCreate(['name' => 'super-admin']);

$user = User::find(1);
$user->assignRole($role);

This can be run in Tinker (php artisan tinker) or placed in a seeder.

Super-admin users bypass all gate checks and have unrestricted access to the admin panel.


Step 11 -- Seed Sample Data (optional)

php artisan textstem:dbseed

Creates sample pages, posts, categories, and assets to give the admin panel something to work with immediately.


Step 12 -- Optional: Set Up a React/Inertia Site

Public pages render via Blade/Livewire by default. The package also ships an optional Inertia/React rendering mode for public pages -- the admin panel stays Blade/Livewire either way; this only changes how visitors see public pages rendered. Skip this step entirely for a Blade/Livewire-only site.

Prerequisites

An Inertia + React frontend. Jetstream's own "Jetstream with Inertia" option in Step 3 installs a Vue-based Inertia stack (@inertiajs/vue3) -- it is not a path to React, and choosing it does not help here. 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, then use the "Vanilla - no Jetstream" option in Step 3 -- the starter kit's own auth scaffolding replaces Jetstream's.
  • 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. Follow Inertia's own React setup guide for the exact steps -- 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 (the React starter kit depends on it directly; Jetstream, if installed for its Livewire/API/teams support, pulls it in too).

laravel/jetstream itself is always present regardless -- it's a hard Composer dependency of this package (its default User model uses Jetstream's HasProfilePhoto trait), independent of whether you ever run jetstream:install.

Need team management? The React starter kit's auth scaffolding is single-user only -- no team switching, no invitations. Jetstream's Vue stack is the only ready-made implementation of that feature in this ecosystem; there's no React version of it. If your React site needs teams, either run php artisan jetstream:install inertia --teams and port its team-switching UI to React by hand, or build teams yourself. There's no shortcut here -- factor this in before committing to a React frontend if teams are a hard requirement.

Install

php artisan textstem:install-react

This publishes the React source under resources/js/ -- the rendering engine, a Prose component and default page template matching the package's one bundled Blade component and default template, the page shell, and TypeScript types -- 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 (this can't be a drop-in augmentation file -- TypeScript doesn't merge two separate inline shapes assigned to the same property across files):

    import type { TextstemSharedProps } from '@/types/textstem-shared';
    // ...
    declare module '@inertiajs/core' {
        export interface InertiaConfig {
            sharedPageProps: {
                // ... your existing keys
                textstem: TextstemSharedProps;
                [key: string]: unknown;
            };
        }
    }
    

Then confirm @inertiajs/react, react, and react-dom are in package.json (see Prerequisites above) and build:

npm run build
# or, for development:
npm run dev

What changes

Concern Blade/Livewire (default) React/Inertia (react_rendering: true)
Public page rendering WranglerPageController ReactPageController
Admin panel Blade/Livewire Unchanged -- Blade/Livewire
Post-login redirect config('fortify.home') for everyone Role-aware: the dashboard route for a user who can access-textstem-admin, / for everyone else
Admin bar (edit-this-page overlay) <x-textstem::adminbar/>, only reflects the page you loaded, not later client-side navigation React AdminBar, driven by a shared Inertia prop -- stays correct across client-side navigation
Page-props cache The package's own PageCache (raw HTML) A separate cache keyed on path + query, reusing the same textstemapp.cache.* config

Adding your own components and page templates

Add components and templates the same way you would for Blade: create the PHP class (App\Wrangler\Components\{Name}), then add a matching React component (resources/js/wrangler/components/{Name}.tsx) and register it in components/index.ts. ActiveComponent::build() (not render()) is what the React path calls -- a component that doesn't override it already gets a safe default (its raw config as data), the same as it does today. See docs/react-rendering.md for full details, and docs/Notes-wranglercomponents.md for the PHP side.

Configuration

config/textstemapp.php:

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

Verification

After completing the steps above, confirm the following:

  • public/vendor/medialight/textstem/ exists and contains JS/CSS files
  • resources/views/wrangler/ exists with components/, pages/, posts/, and template-commands/ subdirectories
  • config/textstemapp.php exists in the host app
  • php artisan migrate:status shows all package migrations as Ran
  • The admin panel loads at /textstem without errors
  • /textstem/dashboard is accessible after logging in as the super-admin user

If you completed Step 12, also confirm:

  • resources/js/wrangler/, resources/js/layouts/public-layout.tsx, and resources/js/pages/textstem/page.tsx exist
  • TEXTSTEM_REACT_RENDERING=true is set in .env
  • A public page loads through Inertia (check the response for X-Inertia on a client-side navigation, or that the page renders via a .tsx component in your browser's dev tools) rather than a server-rendered Blade page
  • The admin panel at /textstem is unaffected -- still Blade/Livewire

Updating the Package

composer update medialight/textstem-laravel
php artisan view:clear
php artisan migrate
php artisan textstem:refresh-assets

view:clear matters even when nothing looks wrong: Laravel's compiled Blade cache is keyed by file path, not content hash, so an updated package template can silently keep serving the pre-update compiled version until the cache is cleared -- this has caused Alpine/Livewire components to reference stores or handlers that exist in the new source but not in the stale compiled output.

textstem:refresh-assets is also required for any update that touches package CSS/JS (e.g. new Tailwind utility classes) -- public/vendor/medialight/textstem/style.css and textstem.js are prebuilt and committed to the package, not regenerated by the host app's own asset build.

If you completed Step 12, an update can also touch the published React source. Re-publish it and rebuild:

php artisan vendor:publish --tag=textstem-react --force
npm run build

Review any files you've since customized (components/index.ts, page-templates/index.ts, public-layout.tsx) against the freshly-published versions before overwriting -- --force does not merge.

Review config/textstem.php in the package against your published config/textstemapp.php after each update and merge in any new keys.


Notes

User Model

The service provider sets auth.providers.users.model to Medialight\Textstem\Models\User by default (Laravel's standard user model plus Spatie role/permission support and Jetstream traits).

If your app needs its own user model instead -- for example, to attach Spatie roles directly to App\Models\User -- set user_model in config/textstemapp.php:

'user_model' => \App\Models\User::class,

No changes to App\Models\User itself or to AppServiceProvider are needed; your own model does not need to extend the package's.

Localization Middleware

The Localization middleware is appended to the web group automatically by the service provider. It reads a locale from the session and calls app()->setLocale(). No manual registration is needed.

Re-publishing Assets

If you update the package or suspect published files are out of date, re-run:

php artisan textstem:refresh-assets

This clears public/vendor/medialight/textstem/, re-publishes the JS/CSS and TinyMCE build, and ensures the required Wrangler view directories exist.

esc