Modules
Modules extend Textstem with new content types that don't fit pages or posts, such as subscribers, shows or products. A module is an admin section of its own: a model, a controller, a policy, a Livewire table and admin views, all wrapped in the Textstem admin layout.
The quickest start is php artisan textstem:make:module; see Creating Modules with the Console. This page explains the parts, using a 'subscriber module' as the example.
Routes
Put the module's routes under the /textstem prefix with the same middleware as the package's own admin routes. textstem.admin checks the access-textstem-admin gate, so only people allowed into the admin reach them:
// routes/web.php
use App\Http\Controllers\SubscriberController;
Route::prefix('/textstem')
->middleware(['web', 'auth:sanctum', 'verified', 'textstem.admin'])
->group(function () {
Route::resource('subscribers', SubscriberController::class);
});
Permissions and the policy
Textstem uses Spatie's laravel-permission. Create the permissions your policy checks, for example list subscribers, view subscribers, create subscribers, update subscribers and delete subscribers, with a seeder or in Admin → Roles.
Then register the policy. The package replaces Laravel's policy guessing for every model, so App\Policies\SubscriberPolicy is not found on its own:
// AppServiceProvider::boot()
use Illuminate\Support\Facades\Gate;
Gate::policy(\App\Models\Subscriber::class, \App\Policies\SubscriberPolicy::class);
Without this line, every check on the model denies everyone except super-admins. See Authorisation.
Controller
The controller is an ordinary resource controller:
<?php
namespace App\Http\Controllers;
use App\Models\Subscriber;
use Illuminate\Http\Request;
use Illuminate\View\View;
class SubscriberController extends Controller
{
public function index(Request $request): View
{
$this->authorize('viewAny', Subscriber::class);
return view('admin.subscribers.index');
}
// create, store, edit, update, destroy ...
}
Use policy method names (viewAny, create, update, delete) in authorize(), so the calls reach the policy.
Index View
The index view wraps its content in <x-textstem::app-layout> to match the rest of the admin:
<x-textstem::app-layout>
<x-slot name="header">
<h2 class="h2">Subscribers</h2>
</x-slot>
<x-slot name="topcontrols">
@can('create', App\Models\Subscriber::class)
<a href="{{ route('subscribers.create') }}" class="button button-primary float-right">
<x-ts::icons.add class=""/>
{{ __('Create') }}
</a>
@endcan
</x-slot>
<livewire:tables.subscriber-table />
</x-textstem::app-layout>
The list is a Livewire table built on Textstem's Table component:
<?php
namespace App\Livewire\Tables;
use App\Models\Subscriber;
use Illuminate\Database\Eloquent\Builder;
use Medialight\Textstem\Livewire\Table;
use Medialight\Textstem\Table\Column;
class SubscriberTable extends Table
{
public string $resourceName = 'subscribers';
public function query(): Builder
{
return Subscriber::query();
}
public function columns(): array
{
return [
Column::make('email', 'Email'),
Column::make('source', 'Source'),
Column::make('updated_at', 'Modified')->component('columns.formatters.modified'),
Column::make('', '')->component('columns.formatters.editbuttons')->notsortable(),
];
}
}
Adding it to the admin menu
Add the module to the modules key of config/textstemapp.php. The key isn't in the published file, so add it yourself:
'modules' => [
[
'label' => 'Subscribers',
'route' => 'subscribers.index',
'access' => 'list subscribers',
],
],
route is a route name (use path for a literal URL instead), and access is checked with auth()->user()->can(...). Entries go in a "Modules" group; add 'group' => '...' to change it, or 'children' => [...] for a submenu of entries with the same shape.