Tilly The Coder
Tilly The Coder
5 Production-Ready Custom Rich Editor Block Patterns in Filament v4
Tilly The Coder

5 Production-Ready Custom Rich Editor Block Patterns in Filament v4

Tilly The Coder
5 mins
This is the estimated time it takes to read the article.

Reconstructed article, written September 26, 2026. This is new guidance at the recovered URL. It presents five production-oriented block designs and a complete callout implementation to adapt; it does not claim that the lost original code has been recovered.

A custom block earns its place when it captures a meaning that ordinary paragraphs cannot express consistently. For a learning site, that might be a warning, a set of prerequisites, or a link to a relevant lesson. Editors should choose the content; the application should own the markup and behavior.

Filament v4’s rich-editor documentation defines RichContentCustomBlock, its configuration action, preview and HTML methods, and registration through customBlocks(). Register the same block classes with the public RichContentRenderer.

1. A callout with constrained content

Use a callout for a short note the reader should notice before continuing. Start with a heading and plain-text body. Keeping the first version small avoids nesting a second editor inside the block and gives you a predictable rendering contract.

namespace App\Filament\RichContentBlocks;

use Filament\Actions\Action;
use Filament\Forms\Components\RichEditor\RichContentCustomBlock;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Illuminate\Support\Facades\Validator;

final class CalloutBlock extends RichContentCustomBlock
{
    public static function getId(): string
    {
        return 'learning-callout-v1';
    }

    public static function getLabel(): string
    {
        return 'Learning note';
    }

    public static function configureEditorAction(Action $action): Action
    {
        return $action->schema([
            TextInput::make('heading')->required()->maxLength(120),
            Textarea::make('body')->required()->maxLength(1000),
        ]);
    }

    /** @param array<string, mixed> $config */
    public static function toPreviewHtml(array $config): string
    {
        return self::toHtml($config, []);
    }

    /**
     * @param array<string, mixed> $config
     * @param array<string, mixed> $data
     */
    public static function toHtml(array $config, array $data): string
    {
        $validated = Validator::make($config, [
            'heading' => ['required', 'string', 'max:120'],
            'body' => ['required', 'string', 'max:1000'],
        ])->validate();

        return view('rich-content.callout', $validated)->render();
    }
}

Create resources/views/rich-content/callout.blade.php. Escaped Blade output preserves the plain-text contract. The CSS class is an application styling hook; add its appearance to your own stylesheet.

<aside class="learning-callout">
    <p><strong>{{ $heading }}</strong></p>
    <p>{{ $body }}</p>
</aside>

This example rejects malformed configuration during rendering. Validate imported block data before publishing too, so a bad import is caught before a public request. Do not silently replace missing information with invented text.

2. A prerequisites checklist

Give a prerequisite block a short title and a list of required tools or concepts. Use a Repeater containing a required TextInput for each item. Keep a sensible item limit and preserve the saved item order. Render a semantic unordered list with escaped item text.

The editorial question is whether a reader can verify every requirement. “Basic programming knowledge” is vague; “Run Composer from your terminal” is observable. Avoid interactive checkboxes unless you also define whether checked state belongs to the current reader and where it persists.

3. A lesson link

Store a lesson identifier selected from records the editor can access, plus a concise link label. Resolve the public URL from that record when rendering. Do not ask editors to paste an iframe or maintain a second copy of the lesson title.

Define what happens when the lesson becomes private or is deleted. A public page must not disclose a private lesson through its block. Validate the reference at publication time and run the access check when resolving the link. Keep the public and editor previews consistent about unavailable references.

4. A quotation with attribution

Collect the quotation text, an attribution, and an optional approved source URL. Render the quotation in a blockquote and the attribution beside it. An attribution must describe a real source; visual polish never justifies inventing an endorsement.

Validate URL schemes and escape both text and attributes in the view. Test unusually long names and an absent optional link. The quotation should remain understandable when CSS is disabled and when the page is read aloud.

5. A code example with an explanation

Use a language choice from an explicit supported list, a plain-text code field, and a short caption. Render escaped code inside pre and code elements. Syntax highlighting can enhance that output after the content remains readable without JavaScript.

Check indentation, angle brackets, quotes, and a long line on mobile. Never evaluate the stored code to produce a preview. If examples are downloadable, make the filename and content type deliberate rather than accepting arbitrary upload behavior inside the editor.

Register, save, and render the same contract

use App\Filament\RichContentBlocks\CalloutBlock;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\RichEditor\RichContentRenderer;

// In the resource form:
RichEditor::make('content')
    ->json()
    ->customBlocks([CalloutBlock::class]);

// When preparing the public article HTML:
$html = RichContentRenderer::make($article->content)
    ->customBlocks([CalloutBlock::class])
    ->toHtml();

Cast a JSON content attribute to an array in the model. Add the other block classes only after implementing their configuration and rendering methods. Keep their identifiers stable; a renamed PHP class can preserve its stored block ID, whereas changing the ID requires migrating existing documents.

For each block, save a real article, reload it in the editor, and compare the public output. Test malformed configuration, HTML-like text, empty optional fields, and a narrow viewport. Capture both views. Production readiness comes from that verified content lifecycle and explicit access rules, not simply from seeing a block in the insertion menu.