awcodes/scribble
Scribble is a lightweight Markdown editor for Filament that brings a clean, distraction-free writing experience to your admin panels, with a side-by-side live preview and a simple toolbar for authoring and formatting Markdown content.
Resolve what the package actually is before you install it. The source material is contradictory: the title says "Rich Text Editor plugin for Filament Forms," the description says Markdown editor, one internal note describes an AI text-rewriting service, and another describes a signature pad / Markdown blog. The release history (v0.8.1-alpha, a Faker link fix) and the Filament/Forms packaging point to a TipTap/ProseMirror-backed rich-text or Markdown form field. Open src/, composer.json, and resources/dist/ in the repo first. Everything below assumes the form-field interpretation; if the source says otherwise, stop.
Install against a Filament 3-era app, pinned to the exact tag.
composer require awcodes/scribble:0.8.1-alpha
php artisan vendor:publish --tag=scribble-config
php artisan vendor:publish --tag=scribble-assets
Do not use ^0.8 — there is no upstream to ship a compatibility fix, and a floating constraint will either break resolution or silently pull a broken alpha. Check the composer.json constraints against your Filament/PHP/Laravel versions before requiring, because that is where an archived plugin fails first.
Look in this order:
composer.json — Filament/Laravel/PHP constraints; whether TipTap is bundled or peer-depended.src/ — the field class, its namespace, and what it dehydrates into form state (HTML string vs. ProseMirror JSON).config/scribble.php — toolbar/profile config surface. This is your only supported extension point.resources/dist/ — the compiled JS. This is what breaks under CSP or Livewire upgrades.tests/ — the Faker-link release note lives here; it tells you what content shapes the package itself exercises.First use case to validate fit: a PostResource with a body longText column. Create, edit, save, reload, render in Blade with sanitization. If that round-trip works you understand the package; if it doesn't, you've spent an hour instead of a sprint.
use Awcodes\Scribble\Scribble; // verify namespace in src/
public static function form(Form $form): Form
{
return $form->schema([
TextInput::make('title')->required(),
Scribble::make('body')
->columnSpanFull()
->required(),
]);
}
Then decide, on day one, whether the field writes HTML or JSON — that single fact determines your exit cost later.
Store sanitized HTML, not TipTap JSON. This is the most consequential decision you will make and you cannot revisit it cheaply. HTML in a longText column means swapping to Filament's native RichEditor is a field change; ProseMirror JSON means a content migration across every row. Check what getState()/dehydration returns before building anything on top. If it's JSON, convert to HTML on save via a model mutator immediately.
Sanitize on write, sanitize on render. Rich-text fields persist markup that gets rendered unescaped. Never trust editor output — it's client-side.
// app/Models/Post.php
protected function body(): Attribute
{
return Attribute::make(
set: fn (?string $value) => $value ? app('purifier')->clean($value) : null,
);
}
<div class="prose">{!! clean($post->body) !!}</div>
Wrap the field in your own factory class. This is the highest-leverage pattern with an archived dependency: every resource touches one class, and migration is a one-file edit later.
namespace App\Filament\Forms\Components;
use Awcodes\Scribble\Scribble;
use Filament\Forms\Components\Field;
class RichText extends Field
{
public static function make(string $name = 'body'): Scribble
{
return Scribble::make($name)
->columnSpanFull()
->profile('default'); // verify the profile API in config/scribble.php
}
}
Resources call RichText::make('body'). When the day comes, you swap the return type and you're done.
Keep it top-level and full-width. Scribble is a form field with a JS lifecycle. Put it as a columnSpanFull() component or inside a plain Section/Grid. Avoid Repeater items, Wizard steps, and lazy-loaded Tabs unless you've explicitly tested the mount/unmount cycle — TipTap initialising into Livewire-torn-down DOM is where "editor went blank" bugs come from.
Test the round-trip, not the editor. You cannot meaningfully unit-test a TipTap canvas; test that form state accepts markup and persists it.
Livewire::test(PostResource\Pages\CreatePost::class)
->fillForm(['title' => 'Hello', 'body' => '<p>Body <strong>text</strong></p>'])
->call('create')
->assertHasNoFormErrors();
$this->assertStringContainsString('<strong>text</strong>', Post::first()->body);
Pair media handling explicitly. The editor's image node will not upload anything. Use awcodes/curator if you're already in that ecosystem, otherwise your own upload field plus URL references in markup.
Roll it out somewhere non-critical first. Internal notes or an ops runbook page. Let it survive a Livewire minor and a Filament patch before it touches customer-facing content.
Archived, alpha, pre-1.0, zero dependents. Last release 2024-09-26, 48 stars, 0 dependents. No fix is coming, no Filament v4 tracking, and — critically — no community fork waiting in the wings. If you adopt it, you own it. Register an owner and a review date the same day you add it.
Filament's native RichEditor is the default answer. On Filament v3+, the framework ships a maintained rich-text field that versions in lockstep. The honest question is not "does Scribble work?" but "why are we adding an archived plugin for something the framework already does?" Adopt Scribble only for a specific gap the native field doesn't cover, and price the lifecycle cost in.
Stored XSS is entirely your problem. There is no upstream to ship a bypass fix. Sanitize on write and on render, and audit any legacy rows written before you added the purifier.
Duplicate TipTap bundles fail subtly. If native RichEditor and Scribble both bundle their own ProseMirror in the same panel, you get divergent editor versions, inconsistent HTML output between fields, and weird selection/cursor bugs that look like Livewire problems but aren't. One editor per panel, or verify they share a bundle.
Asset publishing and CSP drift. The editor ships compiled JS. A strict CSP, a Vite pipeline change, or a Livewire major can break it with no upstream fix. If the editor renders but the toolbar doesn't, it's almost always an unpublished asset or a CSP block on resources/dist/.
The v0.8.1-alpha release note is a testing hint. "Fix: random links in Faker breaking tests" means the package's factories generate randomly-shaped links. If you mirror them or snapshot rendered HTML, assert on structure (assertSee('<strong>')), never on exact strings.
Content lock-in is a schema decision, not a field decision. HTML = afternoon swap. JSON = data migration. Decide before the first save, because the first save is what makes it expensive.
Reconcile identity before it reaches a roadmap. The artifact has been described four different ways across its own materials (rich text, Markdown, AI rewrite, signature pad). Treat the README as a claim to verify, not a spec — that's a due-diligence gate, not a nitpick.
Plan replacement, not maintenance. Forking a 48-star, zero-dependent archived component is not defensible. The cheap path is: wrap it in your own factory, store sanitized HTML, and remove it before the next Filament or Laravel major — never during one.
Exit checklist: swap the class in RichText → confirm stored content is sanitized HTML → purifier pass over existing rows → composer remove awcodes/scribble → delete published config/assets → drop service provider/plugin registration → add "no archived dependencies for new form fields" to engineering standards.
How can I help you explore Laravel packages today?