Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Markdown Blade Component Laravel Package

spatie/laravel-markdown-blade-component

Spatie's Laravel Markdown Blade Component lets you render Markdown directly in Blade views using a simple component. Built on CommonMark, it supports GitHub-flavored Markdown, caching, and safe HTML output, making it ideal for blogs, docs, and user-generated content.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and (optionally) publish its config:

composer require spatie/laravel-markdown-blade-component
php artisan vendor:publish --tag="markdown-config"

The service provider is auto-discovered; there is nothing else to register. The first place to look is config/markdown.php — that file is the source of truth for which CommonMark extensions are enabled by default (core CommonMark, tables, strikethrough, autolinks, task lists, etc.) and for the renderer/CommonMark options the package ships with. Skim it before you write any markup, because half of the "why doesn't this render" questions are answered there.

The fastest first use case is prose in a Blade view — changelogs, help pages, CMS bodies, README-style docs:

<x-markdown>
    # Release notes

    - Added **faster** rendering
    - Fixed the `null` bug in `Post::render()`

    | Version | Date       |
    |---------|------------|
    | 2.0     | 2026-02-22 |
</x-markdown>

The component renders the slot through League CommonMark and returns an HtmlString, so you do not wrap it in {!! !!}.

When you need rendered HTML outside a view (mail, API responses, a model accessor), resolve the renderer directly:

use Spatie\LaravelMarkdown\MarkdownRenderer;

$html = app(MarkdownRenderer::class)->toHtml($post->body);

That is enough to ship. Everything else in this assessment is about choosing the right trust boundary, caching the result, and not tripping over the converter's defaults.

Implementation Patterns

Two trust profiles, chosen per call. Treat internal content and user content as separate pipelines. Author-controlled docs can allow raw HTML; user input must not.

{{-- trusted, authored in-repo --}}
<x-markdown :options="['html_input' => 'allow']">
    <div class="callout">Internal note</div>
</x-markdown>

{{-- untrusted, user-submitted --}}
<x-markdown :options="['html_input' => 'escape', 'allow_unsafe_links' => false]">
    {{ $comment->body }}
</x-markdown>

Set html_input explicitly on every untrusted path rather than relying on the package default — it is the one option you do not want to inherit silently from a config file a teammate edited.

Global defaults in config, per-render overrides in the component. Put extensions the whole app needs (tables, footnotes, front matter) in config/markdown.php; pass :extensions only for one-off cases like a docs route that needs heading permalinks. Same for :options — keep the config as the baseline and override the renderer options that differ for a single view.

Pre-render and store the HTML on write. Rendering on every page view is wasteful for content that changes rarely. Do it in a model observer and keep the column:

use Spatie\LaravelMarkdown\MarkdownRenderer;

static::saving(function (Post $post) {
    $post->body_html = app(MarkdownRenderer::class)
        ->highlightCode()
        ->toHtml($post->body ?? '');
});

Then the Blade view is <x-markdown>{!! $post->body_html !!}</x-markdown> — or skip the component entirely and echo the stored string, since the work is already done.

Cache at the component for read-heavy pages.

<x-markdown :cache="true">…</x-markdown>
<x-markdown :cache="now()->addHour()">…</x-markdown>

Use this for CMS pages that render per request but change infrequently; combine it with a cache_key_prefix per environment so staging and production do not share entries.

Syntax highlighting for technical docs. Add :highlight-code="true" (or flip it on in config) when you want fenced blocks highlighted. This is a Shiki-backed path, so treat it as an infrastructure decision, not a view flag — see the gotchas below.

Wrap it once, not everywhere. If more than a couple of views use it, create a thin resources/views/components/prose.blade.php that forwards to <x-markdown> with your house options. It gives you one place to change the trust profile, add class handling, or swap the package later without touching every view.

Programmatic rendering for mail and exports. Mailables and PDF/export jobs should resolve MarkdownRenderer from the container (it is a singleton) rather than instantiating converters themselves. Chain ->highlightCode(), ->cache(...), and ->toHtml() in one fluent call so the options stay consistent with the views.

Test the rendered HTML, not the markdown. Assert on a substring of the output (assertSee('<h1>', false)) or snapshot the HtmlString. That catches extension regressions and accidental escaping far better than testing the input.

Gotchas and Tips

  • Indented slots become code blocks. If you indent the slot by four or more spaces inside a Blade component, CommonMark treats it as a code block. Keep the content flush-left, or rely on the package's indentation-stripping behavior and verify it in a test — this is the single most common "why is my markdown inside a <pre>" issue.
  • html_input is a security boundary, not a formatting toggle. allow passes raw HTML through verbatim. For any user-supplied markdown, use escape (or sanitize downstream with HTMLPurifier) and set allow_unsafe_links => false to block javascript: URIs.
  • Cache keys do not know about your config edits. Changing extensions or renderer options does not automatically invalidate already-cached renders. Bump cache_key_prefix when you change rendering behavior, or flush the store in your deploy script.
  • Pin the cache store explicitly. Set cache_store in config if your app uses a non-default store; otherwise a queue worker and a web worker can end up on different stores and you get either misses or, worse, stale HTML. Ensure the chosen store is reachable from queue containers.
  • Highlighting drags in a Node dependency. The Shiki path shells out to a Node runtime. Missing Node, a cold start, or a container without it will surface as an exception or unhighlighted output. Keep highlighting off for user content, pre-warm it in long-running workers, and never enable it on a path that renders on every request without caching.
  • config:cache and extension definitions. If your published config lists extensions as class strings, caching is fine; if it uses new, config:cache can break. Check the shipped shape and prefer the form that survives php artisan config:cache.
  • Trim slot trailing whitespace. Extra blank lines inside <x-markdown> produce empty <p> tags. trim() the content before rendering, or normalize it in your wrapper component.
  • Soft line breaks differ from GitHub. By default a single newline stays a newline; users often expect <br>. Set the soft_break renderer option if your content is written like a GitHub comment, not a paragraph-per-line document.
  • Extension ordering and duplicates matter. Registering the same extension twice, or adding an extension that conflicts with the core one, produces confusing parse results rather than a clear error. Add to the default list; do not replace it wholesale.
  • Front matter is consumed. Enabling the front matter extension strips the YAML block before rendering — good for file-based content, surprising if you were rendering the raw block for display.
  • Heading permalinks need CSS/JS. The heading permalink extension emits anchors but no styling or copy behavior; you supply that. Do not assume the package ships it.
  • Reuse the resolved renderer in loops. app(MarkdownRenderer::class) is cheap, but instantiating a new converter per item is not. Resolve once outside a @foreach over comments or feed items.
  • Debug by isolating the converter. When output looks wrong, test the same string through app(MarkdownRenderer::class)->toHtml($markdown) in php artisan tinker — it separates Blade/component/slot problems from CommonMark configuration problems in one step.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin