awcodes/richer-editor
A collection of extensions and tools for the Filament Rich Editor field. Add plugins for embeds, emoji, fullscreen, links, source code, Shiki-highlighted code blocks, and more to enhance your editor. Compatible with Filament 4.x and 5.x.
Install and wire up the theme (non-negotiable). Composer install alone leaves the editor looking broken:
composer require awcodes/richer-editor
If you're on Filament Panels you must have a custom theme first, then in resources/css/filament/admin/theme.css:
@import '../../../../vendor/awcodes/richer-editor/resources/css/index.css';
@source '../../../../vendor/awcodes/richer-editor/resources/views/**/*.blade.php';
Then npm run build (or php artisan filament:assets) and clear cached views. For standalone packages (Form Builder / Table Builder without a Panel) the import goes in resources/css/app.css.
Check your Filament version before picking a constraint. Package 1.x is Filament 4 only; 2.x supports Filament 4 and 5. On a fresh Filament 5 app just composer require awcodes/richer-editor — it resolves to 2.x.
First use case: enable a couple of low-risk plugins. Start with FullScreenPlugin and EmbedPlugin — neither has setup dependencies, and they demonstrate the pattern:
use Awcodes\RicherEditor\Plugins\EmbedPlugin;
use Awcodes\RicherEditor\Plugins\FullScreenPlugin;
RichEditor::make('content')
->plugins([
EmbedPlugin::make(),
FullScreenPlugin::make(),
])
->toolbarButtons([
['bold', 'italic', 'link'],
['embed', 'fullscreen'],
]);
Where to look first. The README's Editor Usage → Plugins block is the canonical list — copy it wholesale, delete what you don't want, then trim toolbarButtons(). Ignore FigurePlugin and VideoPlugin entirely (see Gotchas). Don't start with CodeBlockShikiPlugin; it's the most configurable and the easiest to get half-working.
Treat the plugin list as the source of truth, and derive the toolbar from it. Plugin keys are toolbar keys. A common pattern is a small private method returning the curated set so multiple editors (e.g. content and excerpt) stay consistent:
private static function editorPlugins(): array
{
return [
EmbedPlugin::make(),
EmojiPlugin::make(),
FullScreenPlugin::make(),
IdPlugin::make(),
LinkPlugin::make(),
SourceCodePlugin::make(),
CodeBlockShikiPlugin::make()
->themes(light: Theme::GithubLight, dark: Theme::GithubDark)
->languages(['php', 'blade', 'js', 'css', 'html', 'json', 'bash']),
];
}
Use ToolGroup when the toolbar gets long. This is the main day-to-day quality-of-life win — a nine-button toolbar collapses into two dropdowns, and the grouping structure is what authors actually navigate:
use Awcodes\RicherEditor\Tools\ToolGroup;
->tools([
ToolGroup::make('headingTools')
->label('Headings')
->icon(Heroicon::H1)
->displayAsLabel()
->items(['h1', 'h2', 'h3']),
ToolGroup::make('insertTools')
->label('Insert')
->icon(Heroicon::Plus)
->displayAsLabel()
->items(['link', 'embed', 'codeBlock', 'sourceCode']),
])
->toolbarButtons([
['headingTools', 'insertTools'],
['bold', 'italic', 'bulletList', 'orderedList'],
])
Note the group key (headingTools) goes in toolbarButtons(), not the inner items.
Decide early: CodeBlockShikiPlugin vs HighlightedCodeBlock. They solve the same problem from different directions:
CodeBlockShikiPlugin highlights inside the editor and requires you to register the same plugin on RichContentRenderer to keep stored HTML in sync. Each code block gets a language dropdown so authors pick the language.HighlightedCodeBlock is a Filament block; you pick the theme at render time and get a fixed output. Less author control, simpler pipeline.The pairing you actually want for a public site:
// editor
CodeBlockShikiPlugin::make()->themes(light: Theme::GithubLight, dark: Theme::GithubDark)
// renderer — same theme config or the editor and the page disagree
RichContentRenderer::make($content)
->plugins([
CodeBlockShikiPlugin::make()->themes(light: Theme::GithubLight, dark: Theme::GithubDark),
])
->toHtml()
Build a single renderer helper. Most content pages need the same renderer with TOC, heading links, and Shiki. One static method beats repeating five -> calls per controller:
class ArticleRenderer
{
public static function html(string $content): string
{
return RichContentRenderer::make($content)
->plugins([CodeBlockShikiPlugin::make()->themes(light: Theme::GithubLight, dark: Theme::GithubDark)])
->linkHeadings(level: 3, wrap: false)
->toHtml();
}
}
linkHeadings() also sets id attributes on headings, which pairs naturally with TableOfContents:
TableOfContents::make($content)->asHtml(); // drop into a sidebar
TableOfContents::make($content)->asArray(); // custom markup
LinkPlugin is not standalone. It relies on IdPlugin for ID handling — always register both, and keep IdPlugin in the same array. Neither has a visible toolbar button; they work through the link dialog.
Faker is for two distinct jobs. FakerPlugin (local only) is for authors clicking "fill with fake content" inside the editor while designing layouts. RichContentFaker is for you, in seeders and tests:
// database/seeders/PostSeeder.php
$post->content = RichContentFaker::make()
->heading(level: 2)
->lead(pargraphs: 1)
->paragraphs(count: 3, links: true, bold: true)
->codeBlock(language: 'php')
->table(cols: 3)
->asHtml();
asHtml(), asJson(), asText() are mutually exclusive — call one. asJson() mirrors the editor's stored document format, so it's the right choice for testing renderers against realistic input.
maxHeight() is a UX fix, not cosmetic. Long-form editors with EmbedPlugin/FullScreenPlugin grow unbounded; a fixed max height with internal scroll keeps the Filament form usable:
RichEditor::make('content')->maxHeight('400px')
Export path. If you need markdown (docs site, RSS, LLM ingestion), ->toMarkdown(options: []) uses league/html-to-markdown, so pass that package's config keys.
The CSS import is the #1 support ticket. Without it: dark-mode Shiki styles don't switch, .phiki class has no styling, ToolGroup dropdowns render unstyled, and the embed/fullscreen chrome looks wrong. If code blocks highlight in light mode but stay light in dark mode, you skipped the @import. The @source line is equally required for the vendor Blade views to be scanned by Tailwind.
FigurePlugin and VideoPlugin are explicitly marked "should not be used yet." They're in the plugin list in some examples floating around — do not ship them. If you need media embedding today, use EmbedPlugin.
DebugPlugin and FakerPlugin only function in the local environment. They're safe-ish to leave registered (they no-op elsewhere) but you'll get dead toolbar buttons in staging. Gate them:
->plugins([
...$this->basePlugins(),
...(app()->isLocal() ? [DebugPlugin::make(), FakerPlugin::make()] : []),
])
and omit debug / fakeHeading / fakeParagraphs from toolbarButtons() outside local, or wrap the button array the same way. Guard both lists together — a toolbar key with no registered plugin is a silent no-op, not an error.
EmojiPlugin and IdPlugin have no toolbar buttons. Don't add 'emoji' or 'id' to toolbarButtons() and then wonder why nothing appears. Emoji is typed/triggered inline; ID is a supporting plugin.
Shiki runs client-side, so stored content has no highlighting until rendered. If you dump $post->content into a Blade view with {!! !!} instead of going through RichContentRenderer, you get plain <pre><code>. Always render through the renderer with the plugin registered. This is also the reason the editor and renderer theme configs must match — mismatched themes are the most common "why does it look different in prod" complaint.
Do not register PhikiCodeBlockPlugin globally. The README's caution is specific: it breaks Filament's rich-content attribute handling when the RichEditor is in a form context (storing/reading to the DB). It's for standalone rendering of legacy or externally-sourced HTML:
use Awcodes\RicherEditor\Support\RichContentRenderer;
use Awcodes\RicherEditor\Plugins\PhikiCodeBlockPlugin;
RichContentRenderer::make($content)
->plugins([PhikiCodeBlockPlugin::make()])
->phikiCodeBlocks()
->toHtml();
If you're mid-migration from a non-Tiptap editor, use this in a one-off upgrade command, not in a shared view partial.
Theme arguments accept two types inconsistently. ->defaultTheme() and ->themes() take either a Phiki\Theme\Theme enum case or a Shiki theme string ('tokyo-night'). Enums are safer — string typos fall through to unstyled output rather than throwing. ->themes(light:, dark:) are named args; passing them positionally will bite you.
Version floor for SourceCodePlugin. Two bugs were fixed in the 1.x/2.x line: UTF-8 character corruption in the source-code modal (1.0.1, plus an encoding setting) and empty elements being collapsed into self-closing tags (2.2.1 / 2.3.0). On 2.x, require ^2.3 — the self-closing collapse silently mangles <div></div> into <div /> and breaks Tiptap's re-parse. If your authors use SourceCodePlugin at all, this is not optional.
H4/H5/H6 prebuilt tools are deprecated. They exist because older Filament versions lacked them; current Filament ships heading tools natively. Don't add them to a new project — use 'h4' directly or a ToolGroup. Migrating existing content is fine because the stored markup is identical.
league/html-to-markdown is capped at ^5.0 as of 2.3.2. Earlier releases used *, which could have pulled a major version whose HtmlConverter API the service provider calls directly. If you have a lockfile pinned to an older richer-editor, composer update may surprise you — check composer why league/html-to-markdown before blaming toMarkdown().
ToolGroup uses RichEditorTool for typed items, plain strings for built-ins. Mixing is fine, but a typo'd string inside ->items() fails silently — the dropdown renders with a missing entry. When a group looks short, check the item keys first, then check the CSS import.
Debug with DebugPlugin, not dd(). It exposes the editor's JSON document, which is what you actually need when a custom block renders wrong server-side. Cross-check that JSON against what RichContentFaker::asJson() produces — if the shapes diverge, your renderer plugin isn't seeing the node type it expects.
Stars/downloads are modest (44 stars, 0 dependents). This is a one-maintainer package riding on Filament's and Phiki's APIs. Pin the minor version in composer.json ("awcodes/richer-editor": "^2.3"), and pin Shiki themes at the enums rather than strings so an upstream theme rename surfaces as a Composer resolution error instead of a silent style regression.
How can I help you explore Laravel packages today?