Developer Overview

Textstem (medialight/textstem-laravel) is a Laravel package that provides a full CMS layer on top of a standard Laravel application. It ships a visual page builder (the Wrangler), a structured content model, a REST API, Livewire admin UI, media asset management, AI-powered content tools, and SEO/accessibility analysis -- all auto-registered via a single service provider.

This document is a starting point for developers integrating with or extending Textstem. It assumes familiarity with Laravel.


Architecture

Service Provider

TextstemServiceProvider is the package entry point. On register() it binds all services, facades, and drivers to the container. On boot() it:

  • Loads routes, migrations, views, and translations
  • Auto-registers every class in src/Livewire/ as a Livewire component under the textstem:: prefix
  • Registers Blade component namespaces (textstem:: for class-based, ts:: for anonymous)
  • Applies the Localization middleware to the web group
  • Registers gate policies and the super-admin bypass
  • Schedules daily/weekly maintenance commands

Additional providers loaded by the main provider:

Provider Purpose
TextstemEventServiceProvider Model lifecycle events (created, updated, deleted)
TextstemBladeServiceProvider Custom Blade directives
TextstemLangServiceProvider i18n / translation loading
TextstemMacroServiceProvider Collection and string macros

Directory Structure

src/
  Actions/          # Single-responsibility action classes
  Console/Commands/ # Artisan commands (textstem: prefix)
  Facades/          # Laravel facades
  Http/
    Controllers/    # Web admin controllers
    Controllers/Api/ # REST API controllers
    Middleware/
  Jobs/             # Queue jobs (AI, asset processing, SEO)
  Livewire/         # All Livewire components
    PageBuilder/
    Inputs/
    Browsers/
    Modals/
  Models/           # Eloquent models
  Policies/         # Gate policies (auto-guessed by model name)
  Providers/        # Sub-providers
  Services/         # Business logic services
  Tools/            # Pluggable admin dashboard tools
  Wrangler/         # Page-builder engine
resources/
  views/
    wrangler/
      components/   # Published to host app on install
      pages/
      posts/

The Wrangler Page Builder

The Wrangler is the core page-building system. A WranglerPage is a container that holds an ordered set of WranglerComponent records. Components are placed into named slots defined by a page template.

WranglerPage

Medialight\Textstem\Models\WranglerPage extends BaseModel and uses:

  • SoftDeletes + Prunable (auto-pruned after one month)
  • EloquentSluggable (slug auto-generated from title)
  • HasTags (Spatie tags)
  • HasTemplate (resolves the Blade template)

Key properties:

Property Description
title Page title
slug URL slug (auto-generated)
url Full path (e.g. /about/team), rebuilt on every save
template Template identifier
enabled 1 = published, 0 = draft
listed 1 = appears in menus and sitemaps
greedy When true, unmatched child paths resolve to this page
parent_id Parent page ID (-1 for root)
access public, private, or null
meta Arbitrary JSON metadata

Page URLs are rebuilt automatically from slug and parent hierarchy by WranglerPageUrl on every save.

WranglerComponent

Each component has a type (maps to a Blade view in resources/views/wrangler/components/), a slot (layout region), a position (sort order within slot), and a data JSON payload edited through the PageBuilder UI.

Template Commands

src/Wrangler/TemplateCommands/ provides Blade directives usable inside page templates to inject Wrangler slots, navigation, and other dynamic content.


Eloquent Models

All models extend BaseModel, which itself extends Illuminate\Database\Eloquent\Model.

Model Table Notes
WranglerPage wrangler_pages Pages in the page builder
WranglerComponent wrangler_components Components attached to pages
Post posts Blog/content posts with types, tags, publish scheduling
Asset assets Media library items (images, PDFs, video, etc.)
Category categories Polymorphic category taxonomy
Collection collections Grouped content sets
Event events Calendar events
Message messages Contact/form submissions
GlobalOption global_options Key/value site-wide settings
User users Extends Jetstream user; the package overrides auth.providers.users.model

The package also ships versioning models (WranglerPageVersion, WranglerComponentVersion, PostVersion) used to snapshot content before mutations.


Facades

Auto-aliased via composer.json extra configuration:

Alias Facade Class Underlying Service
Textstem Facades\Textstem Core package helper (current page, etc.)
Preferences Facades\Preferences User/site preferences store
Notifier Facades\NotifierFacade In-app notification system
Image Helpers\Image Image URL and transformation helper

Additional facades registered in the container but not aliased by default:

  • Accessibility -- WCAG analysis
  • SEO -- SEO analysis and scoring

Usage example:

use Medialight\Textstem\Facades\Textstem;

$page = Textstem::getPage();

REST API

All authenticated endpoints are under /api/v1/ and require auth:sanctum. One public endpoint requires no authentication.

Public

Method Endpoint Description
GET /api/v1/public/pages/by-url Fetch a page by full URL; pass include_components=true to include components grouped by slot

Authenticated (auth:sanctum)

Resource Endpoint
Pages /api/v1/pages
Wrangler Components /api/v1/wrangler-components
Posts /api/v1/posts
Assets /api/v1/assets
Categories /api/v1/categories
Roles /api/v1/roles
Permissions /api/v1/permissions

Nested routes are also available, e.g.:

GET  /api/v1/categories/{category}/wrangler-pages
GET  /api/v1/wrangler-pages/{wranglerPage}/wrangler-components
GET  /api/v1/posts/{post}/tags
POST /api/v1/wrangler-pages/{wranglerPage}/tags/{tag}

All resource responses use timacdonald/json-api formatting.


Configuration

The config is published to config/textstemapp.php in the host application (source: config/textstem.php).

Key sections:

Key Description
asset_disk Storage disk for uploaded assets (default: public)
cache.enabled Enable Wrangler page caching (default: false)
cache.expire Cache TTL in seconds (default: 3600)
tinymce.api_key TinyMCE cloud API key; omit to use bundled local install
openai.enabled Enable OpenAI integration
openai.model OpenAI model name (default: gpt-4o-mini)
accessibility.wcag_level WCAG conformance level: A, AA, or AAA
uploads.max_file_size Max upload size in KB (default: 10240)
uploads.allowed_extensions Comma-separated allowed file extensions
glide Image transformation server settings (source/cache disks, base URL)
image_presets Named image presets (poster, thumb, tile, icon, hero)
search.models Models included in the CMS global search

Environment Variables

ASSET_DISK=public
TINYMCE_API_KEY=
OPENAI_API_KEY=
OPENAI_ENABLED=true
OPENAI_MODEL=gpt-4o-mini
WRANGLER_CACHE_ENABLE=false
WRANGLER_CACHE_TTL=3600
ACCESSIBILITY_ENABLED=true
ACCESSIBILITY_WCAG_LEVEL=AA
SEO_ENABLED=true
TEXTSTEM_MAX_FILE_SIZE=10240
TEXTSTEM_USE_REDIS=false

Artisan Commands

All commands are prefixed textstem:.

Command Description
textstem:post-install Run post-install setup tasks
textstem:refresh-assets Republish compiled assets to public/vendor/medialight/textstem/
textstem:make:template Scaffold a new page template
textstem:make:wrangler-component Scaffold a new Wrangler component
textstem:make:collection Scaffold a new Collection class
textstem:make:module Scaffold a new Textstem module (controller, model, migration, views)
textstem:make:tool Scaffold a new admin dashboard tool
textstem:make:dashboard-widget Scaffold a new dashboard widget
textstem:sitemap:generate Generate the XML sitemap
textstem:prune-reports Delete old SEO/accessibility reports (runs daily at 03:00)
textstem:health-check Run system health checks (runs daily at 01:00)
textstem:export:tool Export a tool for distribution
textstem:export:wrangler-component Export a Wrangler component
textstem:normalize-asset-paths Normalise legacy asset path formats

RBAC

Textstem uses spatie/laravel-permission. Super-admins bypass all gates:

Gate::before(function ($user, $ability) {
    if ($user->isSuperAdmin()) {
        return true;
    }
});

Policies live in src/Policies/ and are auto-resolved from the model class name (e.g. Post -> PostPolicy). There is no manual registration required.


AI Integration

OpenAI features are opt-in via openai.enabled in config. Queue-based jobs in src/Jobs/ handle:

  • Alt text generation for assets
  • Article and description generation
  • SEO keyword extraction
  • Tag suggestion
  • Image comparison (fingerprint or OpenAI vision driver)

Set OPENAI_USE_QUEUE=true (default) to process AI requests asynchronously. The image comparison driver is selected via textstemapp.images.comparison_driver (fingerprint or openai).


Extending Textstem

Wrangler Components

Create a Blade view in resources/views/wrangler/components/ in the host app. The filename becomes the component type key. Data from the data JSON column is available as $data in the view.

Use the scaffold command to generate a new component with an editor form:

php artisan textstem:make:wrangler-component MyComponent

Admin Dashboard Tools

Tools appear as panels in the admin dashboard. Scaffold one with:

php artisan textstem:make:tool MyTool

Tools are Livewire components placed in the host app under app/Tools/.

Modules

A module bundles a controller, model, migration, routes, and views for a new content type:

php artisan textstem:make:module MyModule

Testing

Tests use Orchestra Testbench with an SQLite file database. The test suite is split into Unit/, Feature/, and Integration/ directories.

# Run all tests
./vendor/bin/pest

# Run a specific file
./vendor/bin/pest tests/Feature/WranglerPageWebTest.php

# Filter by description
./vendor/bin/pest --filter "user can create"

The base TestCase runs migrations once per process. Call $this->setupUser() in a test to create an authenticated user with all gates open.

Static analysis runs at PHPStan level 4 against src/ only:

./vendor/bin/phpstan analyse

Code style is enforced with Laravel Pint:

./vendor/bin/pint
esc