awcodes/richie
Richie is a Laravel/PHP package by awcodes that brings a rich text editing experience to your applications, making it easy to add and manage formatted content with a simple, developer-friendly integration. Use it to give users a clean editor for writing and formatting text in Laravel projects.
Richie is a Filament form field wrapping a Tiptap/ProseMirror editor, plus a Blade render path for stored content. It has no meaning outside a Filament panel — if you aren't already on Filament v3 (Laravel 10/11), stop here rather than letting an editor requirement drag Filament adoption in behind it.
Install pinned to the exact release. At 0.1.x with a single maintainer, a caret range is a liability, not a convenience:
composer require awcodes/richie:0.1.3
php artisan vendor:publish --tag=richie-config # inspect before committing
php artisan vendor:publish --tag=richie-assets # only if assets aren't auto-registered
php artisan filament:assets
Store the document as JSON, not raw HTML. This is the decision that outlives the package and the one you should make in design review, not by accepting a default:
// migration
$table->json('content')->nullable();
// model
protected $casts = ['content' => 'array'];
Then put one field on one resource form and verify a full round trip — save, reload into the editor, render on the front end:
use Awcodes\Richie\Richie; // verify the namespace against src/ before trusting the README
public static function form(Form $form): Form
{
return $form->schema([
Richie::make('content')->label('Body'),
]);
}
Where to look first, in this order:
src/ — the field class and service provider. Documentation is thin on a 0.1.x package; the source is the documentation. Confirm the namespace, the real make() signature, and whether the HTML renderer actually ships at 0.1.3 or is roadmap. The package blurb sells JSON→HTML rendering while the changelog and third-party write-ups describe mostly the form field. Resolve that on day one — it decides whether you're adopting an authoring tool or a rendering utility, and they imply different integration work.resources/js/ — how the Tiptap instance, actions, and extensions are assembled. This is where per-instance versus global registration lives, and the v0.1.3 fix lives here.config/richie.php — toolbar defaults, upload disk/path, extension list.First use case: one editor field on one internal content type — a help article, an announcement, a CMS page — pinned to 0.1.3, with save → reload → render verified end to end. Do not start on the model with three rich-text fields; multi-instance action leakage is precisely what was broken one release ago.
Keep persistence editor-agnostic. Tiptap JSON is the source of truth; HTML is derived. Optionally store both — content_source for the editor, content_html for search and APIs — so an editor swap later is a UI change, not a data migration. Never hand the raw document to a public API.
public function renderedContent(): string
{
return app(ContentRenderer::class)->toHtml($this->content);
}
Configure toolbars per field, not globally. The action-composition model is the reason to pick this package over a monolith. A "short bio" editor and a "long-form article" editor shouldn't share a toolbar:
Richie::make('body')
->toolbar(['bold', 'italic', 'link', 'h2', 'h3', 'bulletList', 'orderedList', 'blockquote', 'image']);
Richie::make('summary')
->toolbar(['bold', 'italic', 'link']);
Verify the actual method names against src/ first. Because actions register on the editor instance as of 0.1.3, per-field toolbars on the same form are the supported shape — but treat that as unverified until your own regression test passes.
Wrap the field in your own class. One internal App\Filament\Fields\RichText gives you a single place to change defaults, apply sanitization policy, and swap implementations later. Cheap now, expensive after fifty resources reference the vendor class directly:
final class RichText extends Richie
{
protected function setUp(): void
{
parent::setUp();
$this->toolbar(config('cms.editor.toolbar'));
}
}
Write the multi-editor regression test before you need it. The v0.1.3 failure mode was actions binding globally and bleeding across instances — wrong toolbar in the wrong field, or an action mutating the second editor's document. Cover it permanently:
it('keeps toolbars isolated across two editors on one form', function () {
livewire(CreateArticle::class)
->fillForm(['summary' => [...], 'body' => [...]])
->call('create')
->assertHasNoFormErrors();
// assert both documents persisted with their distinct expected content
});
Sanitize at the render boundary, in exactly one place. Editing is admin-side and semi-trusted; rendering may not be. Put the sanitizer in the renderer, not in custom toolbar extensions:
final class ContentRenderer
{
public function toHtml(?array $doc): string
{
$html = $this->tiptap->render($doc); // or the package renderer, if it ships
return HtmlSanitizer::clean($html, allowedTags: [
'p', 'h2', 'h3', 'strong', 'em', 'a', 'ul', 'ol', 'li', 'blockquote', 'img',
]);
}
}
Image uploads: find the owner before you configure it. Confirm whether Richie delegates to Filament's file upload or to spatie/laravel-medialibrary. Whichever it is, keep the disk and directory in config so staging and production don't diverge, and validate MIME type and size server-side regardless of client-side constraints — those are UX, not security.
Feature-flag the rollout. Ship behind a config flag on one content type, widen after one clean patch upgrade. The fallback is a plain Textarea, which is trivial to wire and is what keeps an editor hydration failure from becoming an outage.
Pin the exact version and gate the bumps. awcodes/richie:0.1.3, never ^0.1. Three patches into 0.1 with one maintainer means breakage arrives on their schedule. Add a dependency-bot rule requiring the editor regression suite to pass before a bump merges, and re-run it explicitly on every Filament minor bump — this package couples to Filament's asset and form internals, which move independently.
The one release you're reading about is a correctness fix, and that's the signal. v0.1.3 exists solely because actions were registered globally instead of per instance. That's the bug fixed; it's also evidence the action/toolbar API is still settling. Don't schedule roadmap items depending on custom tools or per-field toolbar behavior without a spike, and re-verify after every bump instead of trusting the changelog.
Never save empty over good content. The realistic production incident: editor hydration fails after an asset-pipeline or Filament bump, the field renders blank, and the form cheerfully persists the blank. Convert that silent content loss into a loud validation error:
->mutateFormDataUsing(function (array $data, ?Model $record) {
if (blank($data['content']) && filled($record?->content)) {
throw ValidationException::withMessages([
'content' => 'Editor returned empty content over an existing document.',
]);
}
return $data;
})
Confirm the asset build story on a clean checkout. Does the field ship compiled JS, or does your app need an npm/Vite build? That determines whether a filament:assets republish is sufficient or whether your front-end pipeline now sits in the deploy path of an admin form control. Verify on a fresh clone, not your warm local environment.
Don't return or index raw JSON. Rich-text JSON is verbose and it bloats on real documents. Return sanitized HTML from APIs. Point search at the rendered-and-sanitized HTML — indexing raw ProseMirror output produces matches on node type names and attribute keys, which is worse than no index at all. If full-text search matters, a generated column over the rendered HTML is the clean answer.
Sanitization posture is unverified in this repo. There's no XSS guidance in evidence. Treat stored HTML as untrusted at every render boundary that reaches a browser — especially if content is ever user-submitted, syndicated into email, or exported. If the content is compliance- or safety-sensitive, you need a server-side sanitization layer you own, and this package alone is the wrong tool.
Maintenance cost is the dominant cost. Single maintainer, no SLA, GitHub issues only, 7 stars and zero dependents means you're the first person to hit most edge cases. Name an internal owner, budget a quarterly review slot, and decide now — before three content types depend on it — whether you'd fork and vendor if the project stalls. Confirm during the spike that the source is small enough to read and patch, rather than discovering that during an outage. Re-evaluate at 1.0, or after one full cycle without action-API churn.
Know the extension points, and use the last one. In rough order of safety: the config file (toolbar defaults, upload disk/path, extension list), the field's fluent toolbar/profile methods, custom Tiptap extensions registered through the package's extension hook, and your own subclass. The subclass is the escape hatch — if you catch yourself patching vendor/, move that change up into it.
How can I help you explore Laravel packages today?