tonysm/rich-text-laravel
Rich Text Laravel brings Action Text-style rich content to Laravel. It integrates the Trix editor, stores HTML with attachments, and provides helpers for sanitizing, rendering, and managing rich text fields on Eloquent models.
Install and scaffold (about 5 minutes).
composer require tonysm/rich-text-laravel
php artisan vendor:publish --tag="rich-text-laravel-config"
php artisan vendor:publish --tag="rich-text-laravel-migrations"
php artisan migrate
rich_texts table (record_type, record_id, name, body), not in a column on your model. That single fact drives everything else: eager loading, cleanup on delete, querying, and why adding another rich text field needs no schema change.config/rich-text-laravel.php controls the attachment disk, model/table overrides, and any URL-signing or sanitation options.use Tonysm\RichTextLaravel\RichText; on Post, Page, Comment, etc.resources/js/app.js (whichever the current README specifies). Trix must be available as window.Trix before the input initializes.php artisan storage:link if you're using the local/public disk for attachments.First use case: a Post with a content field.
Form:
<x-trix-input id="content" name="content" :value="old('content', $post->richText('content'))" />
Controller:
$post = Post::create($validated);
$post->richText('content')->update(['body' => $request->input('content')]);
Show page:
<div class="trix-content">
{!! $post->richText('content')->toHtml() !!}
</div>
The .trix-content wrapper is not optional styling vanity — it's where Trix's own CSS (and your prose overrides) apply, and it's the class the package's styles target.
name is what lands in $request->input(). Validate it as an ordinary string, then persist to the rich text body. On validation errors, re-render the component with :value="old('content', $post->richText('content'))".richText('content'), richText('summary'), richText('notes') are separate rows keyed by name. Adding a field is a form change, not a migration.toHtml() outputs stored HTML. Treat it as untrusted: Trix lets users paste arbitrary markup. Confirm whether your version sanitizes on storage or render, and if not, add mews/purifier or Symfony's HtmlSanitizer before {!! !!}.<meta> tag.public locally, S3 in production). The Trix component posts files to the package's attachment endpoint, which stores the file, creates a backing record, and returns a signed URL that gets rehydrated inside the body at render time. Keep that upload route behind auth, and make sure your layout has <meta name="csrf-token" content="{{ csrf_token() }}"> — Trix's XHR uploads need it."" for empty content, not null. required works, but min:/max: rules behave oddly because of wrapper <div>s and <br>s in the HTML. Normalize (strip tags, trim) before measuring length.wire:ignore, bind the hidden input with wire:model, and relay trix-change into Livewire. Without wire:ignore, Livewire's DOM diffing will detach Trix from its element.attachment callback at the same upload route the package exposes so attachments still work.posts.body, write it via richText('content'), keep the old column for one release as a fallback, then drop it.RefreshDatabase. Assert rows in rich_texts with assertDatabaseHas, and assert on visible fragments of HTML (assertSee('...', false)) rather than exact markup — Trix normalizes whitespace and wrappers.deleting observer on models that own rich text to delete rich_texts rows and their files. Morph relations can't use DB-level cascadeOnDelete, so this is on you.rich-text-laravel.php run php artisan config:clear. A missing storage:link produces the same 404 symptom on a different layer.value only at init. Setting input.value later won't update the editor — call trix.loadHTML() or re-render the component. This is the usual cause of "old input doesn't repopulate."<x-trix-input> needs a unique id; duplicates confuse Trix's toolbar and input element association.richText('content') in a loop queries per row. Eager load the rich text relation (or select a precomputed excerpt column) when listing.Tonysm\RichTextLaravel\Models\RichText and register it — that's the clean place for custom sanitization, observers, or attachment handling.<div> vs <p> vs <br> differences that vary by paste path. Assert visible text and DB state.How can I help you explore Laravel packages today?