northernco/markdown-bundle
Northern Co's Markdown Bundle brings CommonMark-powered Markdown rendering to Laravel and PHP apps. Configure extensions, sanitize user input, and render Markdown in Blade views with simple helpers via a publishable config file. Composer-installable and framework-friendly, it keeps content parsing c...
composer require northernco/markdown-bundle. This is a Symfony bundle, not a Laravel package, so Composer only gives you the classes — nothing auto-registers in a Laravel app.README.md (for the Twig extension name and config keys), src/*Bundle.php, DependencyInjection/Configuration.php (the canonical config schema, e.g. html_input, allow_unsafe_links, max_nesting_level), and Resources/config/services.* (the actual service IDs and constructor arguments you'll need to reproduce).body_markdown column on a Post model rendered to HTML in a Blade view. Wire the bundle's converter class manually in a service provider:// config/markdown.php
return [
'html_input' => 'strip',
'allow_unsafe_links' => false,
];
// app/Providers/MarkdownServiceProvider.php
public function register(): void
{
$this->app->singleton(Converter::class, function ($app) {
return new Converter(config('markdown')); // class from the bundle
});
}
{!! app(Converter::class)->convert($post->body_markdown) !!} — then immediately add sanitization (see Gotchas) before you ship.Service provider bridge. The bundle's Extension class builds a container definition you cannot reuse directly in Laravel. Two options: (a) port the few services.yaml definitions into a Laravel provider by hand — usually 1–3 lines of constructor arguments; or (b) boot a tiny ContainerBuilder, load the bundle's *Extension, and pull the service out. Option (a) is far less fragile for a 0-star bundle.
Blade ergonomics. Expose it as a directive and a helper so views stay readable:
Blade::directive('markdown', fn ($expr) => "<?php echo app(\\...\\Converter::class)->convert($expr); ?>");
Eloquent cast for the rendered form. Cache rendered HTML in a body_html column via an attribute cast or an updating observer, so list pages and APIs never parse on read:
protected static function booted(): void
{
static::saving(function (Post $post) {
$post->body_html = app(Converter::class)->convert($post->body_markdown);
});
}
Versioned cache for expensive documents. Key on sha1($source . json_encode(config('markdown'))) so a config tweak (a new extension, changed html_input) invalidates output automatically instead of silently serving stale HTML.
Custom syntax. If the bundle exposes the underlying CommonMark environment, register custom inline/block parsers and extensions there; keep those registrations in one provider so both web and queue workers get identical output.
Reuse across channels. Notifications, mail templates, PDF export, and the API's content_html field should all go through the same singleton — divergent converters are the most common source of "looks right in the browser, broken in email" bugs.
config/bundles.php; Laravel ignores that. If you composer require and expect NorthernMarkdown to appear in the container, you'll get a Target class does not exist or a silently missing binding. Always wire manually.northern_markdown: { html_input: strip }; Laravel's config('markdown.html_input') won't map automatically unless you publish/flatten it yourself. Mismatched keys are silent — the converter just falls back to defaults.html_input to strip/escape and run the output through HTMLPurifier (mews/purist) before {!! !!}. Never widen this "just for the embed feature" without a whitelist.composer.json, commit a lockfile, and write your own integration test asserting a handful of known inputs (headings, links, fenced code, tables). If a release breaks you, be prepared to fork — the surface area is small.composer show northernco/markdown-bundle --all for the actual tag list and whether a stable constraint exists; you may be forced onto dev-main.new the converter per render (easy to do inside a Blade directive), long pages with inline markdown drop in throughput noticeably. Resolve from the container once.debug:container; here, php artisan tinker → app(Converter::class) plus dd(app()->getBindings()) confirms what actually registered. If the underlying class name differs from the README (common in low-traffic packages), search vendor/northernco/markdown-bundle/src for class .*Converter rather than trusting docs.html_input, allow_unsafe_links, max_nesting_level, and any custom parser registration. Set max_nesting_level low (e.g. 20) to blunt pathological-input CPU attacks on public comment fields.How can I help you explore Laravel packages today?