dinandmentink/nova-markdown
A Laravel Nova field package that adds Markdown editing and rendering to your admin resources, letting you author content in Markdown with preview and display it as formatted HTML. Easily integrate Markdown-based text fields into Nova.
dinandmentink/nova-markdown is a Nova field, not a content system. It gives you an editor UI (toolbar, live preview) that reads and writes plain markdown into a normal database column. Rendering that markdown on your public site is entirely your job. Treat it as a nicer <textarea>, and the package makes sense immediately.
Minimal steps:
composer require dinandmentink/nova-markdown
Because the package is archived, check composer.json first — its Nova constraint will only satisfy the Nova major version it was built against (Nova 2/3 era). If Composer refuses to resolve, you are looking at a fork or a different field.$table->text('body')->nullable();
use Dinandmentink\Markdown\Markdown;
public function fields(Request $request)
{
return [
Markdown::make('Body'),
];
}
Where to look first:
src/*.php — the field class extending Laravel\Nova\Fields\Field; note the $component name, since that string must match the registered Vue component.resources/js/field.js and the editor component — this is where the bundled markdown library, toolbar config, and any props are wired up.dist/js/field.js — the compiled asset that actually loads; if this is stale or missing, nothing works.Nova::script() / Nova::style() registration and any publish tags.First use case: a blog post or documentation body. Field on the form, truncated plain text on the index, rendered HTML on the detail page. That covers 90% of real usage.
Split the field across index, detail, and forms. Markdown in an index table is unreadable and slow. Do this:
public function fields(Request $request)
{
return [
Markdown::make('Body')
->onlyOnForms()
->rules('required', 'max:65535'),
Text::make('Body')
->onlyOnIndex()
->displayUsing(fn ($value) => Str::limit(strip_tags(Str::markdown($value ?? '')), 60)),
Text::make('Body')
->onlyOnDetail()
->asHtml(),
];
}
asHtml() renders unescaped on the detail view (sanitize — see Gotchas).
Render on the front end via a model accessor, so every consumer gets consistent output:
use League\CommonMark\CommonMarkConverter;
public function getBodyHtmlAttribute(): string
{
return (new CommonMarkConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]))->convert($this->body ?? '')->getContent();
}
On Laravel 9+ you can use Str::markdown($this->body, ['html_input' => 'strip']) and skip the dependency.
Normalize on save with fillUsing if authors keep pasting HTML or heading levels:
Markdown::make('Body')
->fillUsing(function ($request, $model, $attribute) {
$model->{$attribute} = trim($request->input($attribute));
})
Reuse the field anywhere Nova renders a form — actions with fields, lenses (read-only), and custom tools all accept the same field class. Nothing is resource-specific.
Pair with global search. Markdown is stored as plain text, so ->searchable() on the underlying model works exactly as it would for a text column. No special indexing needed — but see the performance note below.
Validation belongs on the server. Put ->rules('required', 'max:65000') on the field, not reliance on the editor's client-side required state. The editor usually hides the real <textarea>, so browser-level validation is unreliable.
Archived means Nova-version-locked. This is the single biggest risk. Nova 4+ changed the asset pipeline (Nova::script/Nova::style semantics, Vite, component registration). If your app is on Nova 4 or 5, assume this field will not load out of the box. Verify by checking the composer constraint and the compiled dist/js/field.js before committing to it. If you must use it, vendor the package and patch the service provider.
"Unknown custom element" / blank field = asset registration. If the editor doesn't appear but the column saves, the JS never registered. Run php artisan nova:publish, clear bootstrap/cache, and hard-reload the browser. When editing the package locally, npm run dev (or watch) inside the package directory and confirm dist/js/field.js is regenerated.
XSS is your responsibility. Markdown fields store raw text; any author can type raw HTML or a javascript: link. Never do {!! $post->body !!} on unrendered content. Use html_input => 'strip' in CommonMark, and sanitize again if you ever enable raw HTML. Nova's own ->asHtml() is also unescaped — for a detail view that's usually fine (admin-only), but don't copy that pattern to the front end.
Pasted images silently bloat the database. Editors often accept drag-and-drop and inline as base64. A few screenshots turn a 2 KB post into a 4 MB row. Check the editor component for image/upload handling and disable it if you're storing into a plain text column. Prefer a dedicated media field with real file storage.
Don't sort or index on the markdown column. ->sortable() on a TEXT column sorts lexicographically on the first characters, which is meaningless, and it can force filesorts. Keep markdown out of the index entirely.
->searchable() on a large text column works but can be slow at scale. If posts get long, move search to a generated summary column or a proper full-text index rather than scanning body.
Cache the rendered HTML if the accessor runs in a loop — CommonMark conversion on a list of 50 posts is noticeable. Cache per-model and bust on save:
protected static function booted()
{
static::saved(fn ($post) => Cache::forget("post.html.{$post->id}"));
}
Extension point if you outgrow it: the field is thin. If Nova compatibility breaks permanently, the cheapest migration is to swap Markdown::make('Body') for a plain Textarea::make('Body')->rows(20) (or a maintained markdown field) — your data, accessors, and rendering pipeline are unaffected. That's the real argument for choosing this package: the storage format is portable, so the field itself is disposable.
Test in a real browser, not just tinker. The failure modes here are almost all front-end (asset registration, component re-initialization after Vue re-renders on dependent fields, stale compiled JS). Server-side tests will pass while the field is visibly broken.
How can I help you explore Laravel packages today?