Laravel Markdown: Render Markdown in Blade (3 Ways)
September 11, 2026 · 9 min read
Laravel Markdown: Parse and Render Markdown in Blade (3 Ways)
Rendering laravel markdown content takes one line with no package: Str::markdown($text) returns HTML through league/commonmark, which the framework already ships. For a blog or docs site you'll probably want Spatie's laravel-markdown or Graham Campbell's package instead. This tutorial renders the same sample all three ways, then covers the security config and caching that neither package page explains.
Which Laravel Markdown Option Do You Need?
Two packages share the name laravel-markdown, and the framework has a built-in route, so pick before you install anything.
| Option | Install | Blade syntax | Adds | Use when |
|---|---|---|---|---|
Str::markdown() (built in) | nothing | {!! Str::markdown($text) !!} | GFM via league/commonmark | Small amounts of Markdown, notifications, admin notes |
| spatie/laravel-markdown v1 | composer require spatie/laravel-markdown | <x-markdown> component or @markdown | Shiki code highlighting, heading anchors, caching | Blog posts and docs with code blocks |
| graham-campbell/markdown 16.1 | composer require graham-campbell/markdown | @markdown directive, Markdown facade, .md.blade.php views | Full league/commonmark config, view integration | You want Markdown-first view files and extension control |
All three sit on league/commonmark, so the HTML they produce for plain Markdown is identical. The differences are ergonomics, highlighting, and how much configuration you get. Markdown mailables are a fourth, separate thing, covered near the end.
Method 1: Str::markdown() with No Package
If you searched for laravel markdown to html, this is the answer. Laravel's string helpers include Str::markdown() and Str::inlineMarkdown(). Both convert GitHub Flavored Markdown, so tables, task lists, and strikethrough work without configuration. The inline variant skips the wrapping <p> tag, which is what you want for a one-line caption or a table cell.
use Illuminate\Support\Str;
$html = Str::markdown('# Release 2.4');
// <h1>Release 2.4</h1>
$caption = Str::inlineMarkdown('Photo by **Dana**');
// Photo by <strong>Dana</strong>
In Blade, output it with the unescaped syntax. This is deliberate: {{ }} escapes HTML, so the tags would appear as text.
<article>
{!! Str::markdown($post->body, [
'html_input' => 'strip',
'allow_unsafe_links' => false,
]) !!}
</article>
That options array is the security section below in miniature. We prefer this route for anything under a few hundred words per page, because there's nothing to install, nothing to cache, and one less package to update. If you're not writing PHP at all and just need a file converted, the Markdown to HTML methods post covers the no-code routes.
Method 2: Spatie laravel-markdown and the x-markdown Component
Spatie's package adds three things Str::markdown() lacks. You get server-side code highlighting through Shiki PHP for more than 100 languages, automatic anchor links on headings, and cached output. Install it and, if you want highlighting, the Shiki npm package:
composer require spatie/laravel-markdown
npm install shiki
php artisan vendor:publish --provider="Spatie\LaravelMarkdown\MarkdownServiceProvider" --tag="markdown-config"
Spatie's docs say Shiki needs Node 10 or newer on the server, which is the one deployment surprise. In Blade you can wrap literal Markdown in the component or pass a variable to the directive:
<x-markdown>
# Release 2.4
Faster sync and a fix for missing notifications.
</x-markdown>
@markdown($post->body)
Outside Blade, resolve the renderer from the container: app(Spatie\LaravelMarkdown\MarkdownRenderer::class)->toHtml($markdown). The config file exposes code_highlighting.theme (default github-light), add_anchors_to_headings, cache_store, and cache_duration. Set cache_store to false to turn caching off while you're iterating on a post.
Method 3: Graham Campbell's Laravel-Markdown
Graham Campbell's package is the older of the two and the closest to raw league/commonmark. Version 16.1 supports Laravel 10 through 13 on PHP 8.1 to 8.5.
composer require "graham-campbell/markdown:^16.1"
php artisan vendor:publish
It gives you a facade, a directive in two forms, and a view integration that renders .md, .md.php, and .md.blade.php files as Markdown automatically once you enable it in config/markdown.php.
use GrahamCampbell\Markdown\Facades\Markdown;
Markdown::convert('foo')->getContent(); // <p>foo</p>
@markdown('# Inline form')
@markdown
# Block form
Everything until the closing tag is Markdown.
@endmarkdown
The config keys mirror league/commonmark: html_input defaults to strip, allow_unsafe_links defaults to true (change it, see below), and max_nesting_level defaults to PHP_INT_MAX. One thing the README doesn't show is how variables reach a .md.blade.php view. The answer is that Blade runs first, then Markdown, so {{ $title }} in the file is substituted before the Markdown parser sees it.
Preview the Sample Before It Hits Blade
Whichever method you choose, check the Markdown in a GFM renderer before you debug the PHP side. The editor below holds the sample used above; our editor renders the same GitHub Flavored dialect that Str::markdown() targets, so a table that breaks here will break in Laravel too.
When you need the exact HTML to compare against your view's output, the Markdown to HTML converter shows the markup. Fenced code blocks and their language tags, which matter for Shiki, are covered in our Markdown code block guide.
Rendering User-Submitted Markdown Safely
This is the part every top-ranking page skips. Markdown allows raw HTML, so a comment containing <script> renders as a script unless you tell the parser otherwise. league/commonmark's security page is explicit. All HTML input is unescaped by default, and failing to change that "could make your site vulnerable to cross-site scripting (XSS) attacks."
Three options fix it. They apply to all three methods because they're league/commonmark options.
$html = Str::markdown($comment, [
'html_input' => 'strip', // or 'escape'
'allow_unsafe_links' => false, // blocks javascript:, data:, file:
'max_nesting_level' => 100, // stops deeply nested input eating CPU
]);
For Spatie, put the same keys in the commonmark_options array of the config; for Graham Campbell, they're top-level keys in config/markdown.php. Laravel's own docs add one more rule: if you must allow some HTML, run the compiled output through an HTML Purifier rather than trusting the parser.
Remember where the risk actually enters: {!! !!}. That's the only Blade syntax that emits HTML unescaped, so every {!! !!} in your views should be wrapped around output you've already sanitised. Grep for it during code review.
Caching, Highlighting, Tables, and Mailables
Caching. Spatie caches rendered HTML by default, keyed on the Markdown content and the config, in the store named by cache_store. Because the key includes the content, editing a post produces a new key automatically; old entries expire after cache_duration. With Str::markdown() you cache yourself: Cache::remember("post:{$post->id}:{$post->updated_at}", ...) is the pattern we use, with the timestamp in the key so an edit invalidates it.
Highlighting. Spatie renders colours on the server with Shiki, so the HTML arrives styled and no JavaScript ships. Graham Campbell's package and Str::markdown() output plain <pre><code class="language-php"> blocks, which you colour client side with highlight.js or Prism, or through the Torchlight API. Server-side is faster for readers; client-side is simpler to deploy.
Tables. Str::markdown() targets GFM, so pipe tables just work. In the two packages, tables depend on the GFM or table extension being present in the extensions array of the config. If a table renders as a paragraph of pipes, that's the cause. The syntax itself is in our Markdown table guide.
Mailables. Laravel's mail documentation describes Markdown mailables, generated with php artisan make:mail OrderShipped --markdown=mail.orders.shipped. The template mixes <x-mail::message> and <x-mail::button> components with Markdown, and Laravel renders a responsive HTML email plus a plain-text version. It's a mail feature, not a general renderer, so don't reach for it to render blog posts. Don't indent the Markdown inside the components either, or the parser turns it into a code block.
The same decisions come up in other stacks; our Django Markdown and Python Markdown tutorials walk through the equivalents there.
One limitation across every method here: none of them render Markdown on the client as the user types. For a live-preview editor you need a JavaScript renderer in the browser and the PHP renderer on save, and the two can disagree on edge cases. Test the same sample through both.
Common Laravel Markdown Mistakes
Escaped output. Using {{ Str::markdown($text) }} and seeing raw tags on the page. Fix: use {!! !!} for HTML you've sanitised, never for raw user input.
Shiki missing on the server. Highlighting works locally and fails in production because Node isn't installed or npm install shiki didn't run in the deploy step. Fix: add it to the build, or set code_highlighting.enabled to false.
Wrong package's directive. Both packages register @markdown, so installing both causes a conflict. Fix: pick one. If you only need conversion, you probably don't need either.
Laravel Markdown FAQ
For most apps, laravel markdown rendering is Str::markdown() with html_input and allow_unsafe_links set, output through {!! !!} only after sanitising. Reach for Spatie when code highlighting and caching earn their install, and for Graham Campbell's package when you want Markdown view files. Draft the content in the editor first so the only bugs left are PHP ones.