Str::markdown

Converts GitHub flavored Markdown into an HTML string using League CommonMark.

Signature
static Str::markdown(string $string, array $options = [], array $extensions = []): string
  • Added in Laravel 8.26.0

Parameters

ParameterTypeDescription
$$stringstringThe GitHub flavored Markdown source to convert.
$$optionsoptionalarray= []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

Example · php
<?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(&quot;Hello XSS!&quot;);</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.

Example · php
<?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.

Example · php
<?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.

Example · php
<?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

  • Be the first to comment on this lesson.