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 thetextstem::prefix - Registers Blade component namespaces (
textstem::for class-based,ts::for anonymous) - Applies the
Localizationmiddleware to thewebgroup - 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 analysisSEO-- 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