Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 4 min read

How to Use the Trix Rich Text Editor in Laravel 13 with Safe HTML

Trix is a lightweight rich text editor from Basecamp. In Laravel 13 you install it with npm, add a hidden input and a <trix-editor> element to your form, and Trix writes the HTML into that input so Laravel can save it li

Trix is a lightweight rich text editor from Basecamp. In Laravel 13 you install it with npm, add a hidden input and a <trix-editor> element to your form, and Trix writes the HTML into that input so Laravel can save it like any other field. The important part is security: HTML written by users must be cleaned before you print it. This tutorial covers installation, saving, file attachments and sanitizing.

Targets: Laravel 13 (PHP 8.3 to 8.5), Vite and the trix npm package.

Step 1: Install Trix

npm install trix
// resources/js/app.js
import 'trix';
import 'trix/dist/trix.css';

Rebuild with npm run dev or npm run build, and make sure your layout loads resources/js/app.js with @vite.

Step 2: Add the editor to a form

<form method="POST" action="{{ route('articles.store') }}">
    @csrf

    <input type="text" name="title" value="{{ old('title') }}">

    <input id="body" type="hidden" name="body" value="{{ old('body') }}">
    <trix-editor input="body"></trix-editor>
    @error('body') <p>{{ $message }}</p> @enderror

    <button type="submit">Publish</button>
</form>

The input="body" attribute connects the editor to the hidden input with the same id. On the edit page, put the saved HTML into the hidden input's value attribute with {{ old('body', $article->body) }}. Blade escapes the HTML for the attribute, and Trix reads it as HTML.

Step 3: Validate and save

public function store(Request $request): RedirectResponse
{
    $data = $request->validate([
        'title' => ['required', 'string', 'max:255'],
        'body' => ['required', 'string', 'max:100000'],
    ]);

    Article::create($data);

    return redirect()->route('articles.index')->with('success', 'Article saved.');
}

Treat the body value as untrusted HTML. A user can send any string to that field without using the editor at all.

Step 4: Sanitize before output

Printing the saved HTML with {!! $article->body !!} is dangerous. If an attacker saves a script tag or an onerror attribute, it runs in every visitor's browser (a stored cross-site scripting attack). Clean the HTML with an HTML purifier library, either before saving or before displaying.

One option is the stevebauman/purify package, which wraps HTMLPurifier. Check its Packagist page for the versions that support Laravel 13 before installing:

composer require stevebauman/purify
use Stevebauman\Purify\Facades\Purify;

$data['body'] = Purify::clean($data['body']);

Configure the allowed tags so they match what Trix produces: bold, italic, strike, links, headings, quotes, code, lists and line breaks, plus the figure and image markup if you allow attachments. Sanitizing on save keeps cleaned data in the database. Sanitizing again on output is an extra safety layer if you also import content from other sources.

Step 5: Handle image and file attachments

By default Trix lets users drop files, but it does not upload them. You must listen to the trix-attachment-add event, upload the file and tell Trix the final URL:

document.addEventListener('trix-attachment-add', (event) => {
    const attachment = event.attachment;

    if (!attachment.file) return;

    const form = new FormData();
    form.append('file', attachment.file);

    const request = new XMLHttpRequest();
    request.open('POST', '/trix-uploads');
    request.setRequestHeader('X-CSRF-TOKEN', document.querySelector('meta[name="csrf-token"]').content);
    request.setRequestHeader('Accept', 'application/json');

    request.upload.addEventListener('progress', (progress) => {
        attachment.setUploadProgress((progress.loaded / progress.total) * 100);
    });

    request.addEventListener('load', () => {
        if (request.status === 201) {
            const response = JSON.parse(request.responseText);
            attachment.setAttributes({ url: response.url, href: response.url });
        }
    });

    request.send(form);
});

Add the CSRF meta tag to your layout. The controller validates and stores the image and returns JSON with a URL:

public function store(Request $request): JsonResponse
{
    $request->validate([
        'file' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:4096'],
    ]);

    $path = $request->file('file')->store('trix', 'public');

    return response()->json(['url' => asset('storage/'.$path)], 201);
}

Register the route with the auth middleware: Route::post('/trix-uploads', [TrixUploadController::class, 'store'])->middleware('auth');. Run php artisan storage:link so the image URL works.

How do I show the saved content?

<div class="trix-content">
    {!! $article->body !!}
</div>

Only use {!! !!} if the body was sanitized as shown. The trix-content class applies Trix's content styles from its CSS file, so headings, lists and quotes look the same as in the editor.

How do I check that it works?

  1. Open the form and confirm the toolbar appears. Write a heading, bold text and a list.
  2. Submit and check the database: the body column should contain HTML.
  3. Edit the article and confirm the content loads back into the editor.
  4. Try saving <script>alert(1)</script> through a direct request and confirm it does not run on the display page.

Common mistakes

  • Printing the HTML without sanitizing: this opens the door to XSS.
  • Hidden input id mismatch: the input attribute of trix-editor must equal the input's id.
  • No upload handler: dropped images show an upload error until you handle the event.
  • Missing CSS import: the toolbar looks broken.
  • Allowing any file type: validate uploads on the server.

FAQ

Is Trix better than Quill or CKEditor?

It is simpler and lighter, with fewer features. Choose based on what your users need.

Does Trix save Markdown?

No. It saves HTML. If you need Markdown, use a Markdown editor.

Can I use Trix in Livewire?

Yes, but synchronizing the editor with Livewire needs extra JavaScript that listens to the trix-change event and updates the property.

References

Originally published on yasiramin.dev.

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.