stepanenko3/nova-markdown
stepanenko3/nova-markdown provides a Markdown editor field for Laravel Nova, letting you write and preview Markdown content directly inside Nova resource forms. It stores plain Markdown and integrates cleanly with your existing Nova admin panel and models.
Confirm the ground truth before you install anything
This is a Nova field, not a general Laravel package. Two checks first, in this order:
composer.json (e.g. laravel/nova: ^4.0).composer.json constraint on laravel/nova. Last release is 2023-09, which predates several Nova field toolchain changes. If the constraint doesn't cover your pinned major today and your upgrade roadmap, stop — the cost of adoption is now unbounded relative to the benefit.Then read the package's src/ directory before you write a single line of integration code. At 10 stars and a single maintainer, src/ is the spec, not the README.
Install (feature branch, staging first)
composer require stepanenko3/nova-markdown
php artisan vendor:publish --provider="Stepanenko3\NovaMarkdown\FieldServiceProvider"
php artisan nova:publish
The provider class name above is illustrative — confirm the actual class in src/. If the package resolves from a VCS repository entry rather than Packagist, treat that as real friction for CI and for whoever inherits the repo.
Where to look first
| File | Why |
|---|---|
composer.json |
Nova/Laravel/PHP constraints, license header |
src/*.php |
Actual field class name, namespace, and public option methods |
dist/ or resources/js/ |
Pre-compiled assets (no Node needed) vs. must-build |
Bundled parser in dist/ |
marked / markdown-it version — this is your XSS surface |
First use case: one low-risk internal resource
An FAQ entry, internal doc, or blog draft backed by an existing TEXT/LONGTEXT column. Nothing public-facing, no multi-author workflow.
// app/Nova/FaqEntry.php
use Stepanenko3\NovaMarkdown\Markdown; // verify namespace in src/
public function fields(Request $request)
{
return [
ID::make()->sortable(),
Text::make('Title')->rules('required', 'max:255'),
Markdown::make('Body')
->rules('required')
->help('Markdown supported. Rendering is handled server-side.'),
];
}
The field stores raw Markdown in body. Do not add a cast. Rendering is a separate, app-owned, server-side concern — see below.
Separate authoring from display — always
The editor is a form control. On index and detail views you almost always want something cheap and read-only instead of mounting a full editor per row:
Markdown::make('Body')
->onlyOnForms()
->rules('required'),
Text::make('Body')
->onlyOnIndex()
->displayUsing(fn ($value) => Str::limit(
strip_tags(Str::markdown($value ?? '')), 80
)),
Mounting the editor on the index is the most common cause of slow Nova index pages and duplicated component IDs when several rows render.
Never render the preview's HTML — render your own
The live preview is client-side convenience. The stored value is the contract, and the read path is yours:
use Illuminate\Support\Str;
$html = Str::markdown($post->body ?? '', [
'html_input' => 'strip', // drop raw HTML from authors
'allow_unsafe_links' => false, // block javascript: / data:
]);
If you need more control, wire league/commonmark directly and add an HTML sanitizer. Put this in a single MarkdownRenderer service or Blade component so every consumer (API, Blade, mail) goes through one path. If the app already has a renderer, use it — don't let the field imply a second one.
Treat plain-Markdown storage as a hard constraint
No JSON casts, no block schema, no per-field metadata table. Encode this in a test or a code comment in the model so a future contributor can't casually introduce lock-in. The entire risk-mitigating property of this package is that reverting to a textarea costs you formatting affordances, not content.
Rollout pattern: config-gated swap
Since you're adopting unmaintained code, make the field swappable per resource without a revert PR:
// config/content.php
'editor' => env('CONTENT_EDITOR', Stepanenko3\NovaMarkdown\Markdown::class),
// resource
$editor = config('content.editor');
$editor::make('Body')->rules('required'),
A one-line config flip returns every resource to Textarea::make(). Document that flip in the runbook before the pilot.
Upgrade-day workflow
Every Nova major bump, run in this order on staging:
composer update laravel/nova
php artisan nova:publish
php artisan view:clear
php artisan optimize:clear
npm run build # if the app's own assets are involved
Then load create / edit / preview on the affected resource in Chrome and Safari. Silent asset-publish failures are the single most common breakage mode for custom fields.
Testing
You will not get meaningful coverage of the editor without a JS test harness the package doesn't ship. Test what you own:
required, max: behave on the field.Guard the version matrix
Pin both Nova and the field to exact-ish constraints, and add a CI check that fails when the Nova major changes without a compatibility review:
"laravel/nova": "4.35.*",
"stepanenko3/nova-markdown": "1.0.*"
You are adopting the code, not consuming it. Last release 2023-09, 10 stars, 0 dependents, one maintainer. Plan on being the maintainer: budget a recurring allowance that lands on the same calendar as Nova upgrades, and name an owner by person, not by team.
The preview pane is a staff-session XSS vector. The bundled parser executes inside an authenticated admin session. Two mitigations, do both:
dist/ for the parser and sanitizer (marked, markdown-it, DOMPurify) and their versions.Asset publishing fails silently. Stale files in public/vendor/... after a deploy will give you a blank field or a missing toolbar with no error. If the editor isn't there, check the network tab for 404s on the vendor assets first, then re-run nova:publish. Ensure the publish step runs in your deploy pipeline, not just locally.
Method names are guesses until you read src/. Descriptions of "editor height" and "behavior" options often map to fluent methods that changed between releases. Grep for public function in the field class before writing ->height(400) into ten resources.
Empty string vs. null. Editors commonly emit '' on an empty field. If the column is nullable and your rules say nullable, you'll get '' in the DB and inconsistent whereNull queries. Normalize in a fillUsing if the field doesn't already:
->fillUsing(function ($request, $model, $attribute) {
$model->{$attribute} = $request->input($attribute) ?: null;
})
Media uploads: default to "no." If the field exposes image or file upload, it writes wherever it likes and bypasses your storage abstraction, ACLs, and lifecycle rules. Disable it, or scope it deliberately — that's a separate decision, not an install detail.
Multiple Markdown fields on one resource is the classic duplicate-ID bug in thin editor wrappers. Test body and excerpt on the same form before you ship.
Don't double-escape on the read path. The DB holds Markdown, not HTML. If output looks like <p>, something is escaping the stored value before the renderer or twice after it.
Non-technical authors won't use this. A split-pane Markdown editor with a raw syntax surface is a technical-writer tool. If your pilot authors are marketing or support staff, a WYSIWYG or a proper CMS is the correct answer and no amount of configuration fixes it.
Time-box the whole thing. Audit half a day, spike half a day. If the spike doesn't end with a rendered, sanitized page from a real Nova resource, the honest alternative is a thin in-house field around EasyMDE or Toast UI — roughly 1–2 engineer-days, and you inherit a maintained editor instead of owning an abandoned one. At this package's adoption level, in-house is a genuine competitor, not a fallback.
Pre-agree the exit before you merge. Three documented paths: swap to the in-house field, vendor/fork under MIT, or flip the config back to textarea. All are days, not quarters — which is exactly why this deserves a time-boxed pilot rather than a prolonged evaluation.
How can I help you explore Laravel packages today?