Str::markdown
Converts GitHub flavored Markdown into an HTML string using League CommonMark.
static Str::markdown(string $string, array $options = [], array $extensions = []): string- Added in Laravel 8.26.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$$string | string | The GitHub flavored Markdown source to convert. |
$$optionsoptional | array= [] | Configuration handed to League\CommonMark\GithubFlavoredMarkdownConverter, most commonly html_input ('strip', 'escape' or 'allow') and allow_unsafe_links (bool). |
$$extensionsoptional | \League\CommonMark\Extension\ExtensionInterface[]= [] | Extra CommonMark extension instances added to the converter's environment before the document is parsed. |
Return value
stringThe rendered block-level HTML. An empty input string returns an empty string; anything else comes back with a trailing newline.
Errors
Raises an Error when league/commonmark is not installed, a League\Config\Exception\ValidationException when an option value is not one CommonMark accepts (an html_input other than strip, escape or allow, for example), and a League\CommonMark\Exception\CommonMarkException if the document cannot be rendered.
Changelog
- PHP 8.26.0Str::markdown() and Stringable::markdown() added (PR #36071).
- PHP 11.13.0Third $extensions argument added, letting you register CommonMark extensions per call (PR #51907).
- PHP 11.27.0The matching extension support landed on Str::inlineMarkdown() (PR #53033).
Runs the string through League\CommonMark\GithubFlavoredMarkdownConverter and returns block-level HTML: headings, paragraphs, lists, blockquotes and fenced code, plus the GitHub extras that plain CommonMark omits — tables, strikethrough, task lists and autolinked bare URLs. Reach for it whenever content is stored as Markdown (post bodies, changelogs, help articles, seeded documentation) and a view needs HTML.
Raw HTML passes through by default
CommonMark deliberately allows inline HTML, so a comment containing <script> is emitted verbatim. For anything a user typed, always pass 'html_input' => 'strip' (or 'escape') together with 'allow_unsafe_links' => false. Those options are all-or-nothing; if you need to permit a few tags, strip here and run the output through an HTML purifier afterwards.
Rendering the result
The return value is HTML, so Blade needs {!! $html !!} rather than {{ $html }} — which is precisely why the sanitising options above are not optional for user content.
It is not cheap
Every call constructs a fresh converter and environment. Rendering fifty posts in a loop boots the parser fifty times, so store the result in a column or a cache entry and re-render only when the source changes.
Blocks versus inline
Use Str::inlineMarkdown() when you want emphasis, code spans and links without a wrapping <p>.
Example
<?php
use Illuminate\Support\Str;
echo Str::markdown('# Laravel');
// <h1>Laravel</h1>
echo Str::markdown('# Taylor <b>Otwell</b>', [
'html_input' => 'strip',
]);
// <h1>Taylor Otwell</h1>
echo Str::markdown('Inject: <script>alert("Hello XSS!");</script>', [
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
// <p>Inject: alert("Hello XSS!");</p>When to use it
- Turn a post body stored as Markdown into HTML for a Blade template.
- Render a repository's CHANGELOG.md as a release notes page.
- Convert user-written comments to HTML with raw HTML stripped and unsafe links blocked.
- Build an HTML email body from a Markdown string without going through a mail view.
Frequently asked questions
Why does Str::markdown wrap my text in p tags?
Because it renders block-level Markdown, so even a one-line string becomes a paragraph followed by a newline. Use Str::inlineMarkdown() instead when you only want inline formatting — emphasis, code spans, links — with no wrapping block element, which is what you normally want for a heading, a table cell or a button label.
Is Str::markdown safe for user-submitted content?
Not with the default options — CommonMark passes raw HTML straight through, so a <script> tag in the source ends up in your page. Pass 'html_input' => 'strip' (or 'escape') and 'allow_unsafe_links' => false for anything a user typed, and if you must permit a subset of tags, run the output through an HTML purifier afterwards rather than loosening html_input.
Do I need to install anything to use Str::markdown?
Yes — league/commonmark ^2.7 is a suggested, not required, dependency of illuminate/support, so run composer require league/commonmark if you pulled the package in standalone or see "Class League\CommonMark\GithubFlavoredMarkdownConverter not found". A full Laravel application ships with it already.
How do I add a CommonMark extension such as footnotes or heading permalinks?
Pass instances as the third argument, available since Laravel 11.13.0: Str::markdown($source, $options, [new FootnoteExtension]). Configuration for each extension goes in the $options array under the key that extension declares, and the extension's package must be installed — only the extensions bundled with league/commonmark are there by default.
More examples
The GitHub flavored extras
The pipe table becomes a real <table> with <thead> and <tbody>, the checklist becomes a <ul> of disabled <input type="checkbox"> elements, ~~footnotes~~ becomes <del>footnotes</del>, and the bare URL is turned into an <a href>. None of that happens in plain CommonMark — it comes from the GitHub flavored bundle this method uses. Note that html_input only filters HTML present in the source; it never strips the tags the converter itself generates.
<?php
use Illuminate\Support\Str;
$source = <<<'MD'
| Method | Returns |
| -------- | ------- |
| markdown | string |
- [x] tables
- [ ] ~~footnotes~~
Docs live at https://laravel.com/docs
MD;
echo Str::markdown($source, [
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);Registering a CommonMark extension
The third argument (added in 11.13.0) adds extensions to the converter's environment, and the second argument carries their configuration under the key the extension declares — here external_link. The link to laravel.com gains target="_blank" and rel="nofollow noopener noreferrer" because its host is not listed in internal_hosts, while /about is left alone. Extension packages must be installed separately; the extension bundled with league/commonmark is the only one guaranteed present.
<?php
use Illuminate\Support\Str;
use League\CommonMark\Extension\ExternalLink\ExternalLinkExtension;
$html = Str::markdown(
'Read the [docs](https://laravel.com/docs) or the [about page](/about).',
[
'html_input' => 'strip',
'allow_unsafe_links' => false,
'external_link' => [
'internal_hosts' => 'example.com',
'open_in_new_window' => true,
'nofollow' => 'external',
],
],
[new ExternalLinkExtension],
);
echo $html;Rendering once and storing the HTML
Parsing happens once per edit rather than once per page view, which matters because each call builds a whole converter and environment. Because the value is sanitised at write time, the {!! !!} in the template is safe. If you later change the sanitising options, remember that existing rows still hold HTML produced under the old ones and need a backfill.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class Post extends Model
{
protected static function booted(): void
{
static::saving(function (Post $post) {
if ($post->isDirty('body')) {
$post->body_html = Str::markdown($post->body, [
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
}
});
}
}
// In the Blade view, the stored HTML is printed unescaped:
// {!! $post->body_html !!}
Discussion