Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Markdown Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. 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.
  2. Add a column. You do not need anything special — markdown is just text:
    $table->text('body')->nullable();
    
  3. Use the field in a resource:
    use Dinandmentink\Markdown\Markdown;
    
    public function fields(Request $request)
    {
        return [
            Markdown::make('Body'),
        ];
    }
    
  4. Open the form in Nova and confirm the editor renders. If it doesn't, the JS asset didn't register — see Gotchas.

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.
  • The service provider — for 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.

Implementation Patterns

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.

Gotchas and Tips

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.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums