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

Markdown Widget Laravel Package

friendsofvictoire/markdown-widget

A Laravel package that renders Markdown content inside dashboard widgets, letting you format widget text with headings, lists, links, and other Markdown syntax instead of raw HTML.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check: this is not a Laravel package. friendsofvictoire/markdown-widget is a Symfony bundle for Victoire DCMS (a Symfony 2/3-era CMS). It registers a "Markdown" content widget into Victoire's page editor — you drop a widget into a page slot in the Victoire admin, type Markdown in the widget form, and the bundle renders it through a Markdown parser in the front-end template. There is no service provider, no config/markdown-widget.php, no facade, no Blade views.

Given 0 stars, 0 dependents, and a last release of 2016-08-22, it is effectively abandoned. Two realistic entry points:

A. You want Markdown in a Laravel app (99% of readers). Don't install this. Use league/commonmark directly — it's the same underlying ecosystem and is actively maintained.

composer require league/commonmark
// app/Services/Markdown.php
namespace App\Services;

use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
use League\CommonMark\MarkdownConverter;

class Markdown
{
    private MarkdownConverter $converter;

    public function __construct()
    {
        $environment = new Environment([
            'html_input' => 'strip',        // never 'allow' on user input
            'allow_unsafe_links' => false,
        ]);
        $environment->addExtension(new CommonMarkCoreExtension());
        $environment->addExtension(new GithubFlavoredMarkdownExtension());

        $this->converter = new MarkdownConverter($environment);
    }

    public function toHtml(string $markdown): string
    {
        return (string) $this->converter->convert($markdown);
    }
}

Bind it in a service provider as a singleton (the Environment is expensive to build), then use it anywhere:

app(\App\Services\Markdown::class)->toHtml($post->body_markdown);

B. You are maintaining a legacy Victoire site and genuinely need this bundle. Then you are in Symfony, not Laravel:

composer require friendsofvictoire/markdown-widget

Register it in app/AppKernel.php alongside Victoire's core bundles, clear the cache, and the widget appears in the Victoire widget picker in the page editor. Start by reading composer.json (PHP/Symfony constraints it will force on you), then the Widget/ directory (the widget class and its form type), then Resources/views/ (the Twig template that does the actual rendering — that's where escaping decisions live), then DependencyInjection/ for any config knobs.

First use case in either case: a "body" field on a content model (post, page, documentation article) that stores raw Markdown, rendered at read time. Everything else in this assessment builds on that.

Implementation Patterns

1. Render-on-read via a singleton converter (the default)

Keep raw Markdown in the database, convert on output. Store both if you need search:

// migration
$table->text('body_markdown');
$table->text('body_html')->nullable();   // optional denormalized cache
// model observer / saving hook
protected static function booted(): void
{
    static::saving(function (Post $post) {
        $post->body_html = $post->body_markdown
            ? app(\App\Services\Markdown::class)->toHtml($post->body_markdown)
            : null;
    });
}

Cache-on-write is fine for posts, wrong for anything with comments or live previews — for those, convert on read and memoize per request.

2. A Blade component instead of {!! !!} everywhere

This mirrors what the Victoire widget does: one place owns the HTML.

// app/View/Components/Markdown.php
class Markdown extends Component
{
    public function __construct(public ?string $source = null) {}

    public function render() { return view('components.markdown'); }
}
{{-- resources/views/components/markdown.blade.php --}}
<div class="prose">
    {!! app(\App\Services\Markdown::class)->toHtml($source ?? '') !!}
</div>

Usage: <x-markdown :source="$post->body_markdown" />. The html_input => 'strip' setting is what makes {!! !!} safe-ish here; if you must allow raw HTML, add symfony/html-sanitizer or mews/purifier after conversion, not before.

3. Emulate the widget model for structured content

Victoire's value proposition is composable blocks: a page is a list of typed widgets, one of which is Markdown. To get that in Laravel, use polymorphic blocks and a renderer registry:

// content_blocks: id, blockable_type, blockable_id, type, payload (json), position
class ContentBlock extends Model
{
    protected $casts = ['payload' => 'array'];
    protected function blockable() { return $this->morphTo(); }
}
// App\Blocks\BlockRenderer
interface BlockRenderer { public function render(array $payload): string; }

class MarkdownBlock implements BlockRenderer
{
    public function __construct(private \App\Services\Markdown $md) {}

    public function render(array $payload): string
    {
        return view('blocks.markdown', ['html' => $this->md->toHtml($payload['body'] ?? '')])->render();
    }
}

Resolve by type from a container tag or a match statement. Now adding a "Markdown" block is one class plus one entry — the same extension shape Victoire gives you, without the CMS.

4. Live preview endpoint

The widget's editor UX is "type Markdown, see it rendered." Reproduce with a small POST route that converts and returns HTML for an iframe/preview pane:

Route::post('/markdown/preview', function (Request $request, \App\Services\Markdown $md) {
    $request->validate(['body' => 'nullable|string|max:100000']);
    return response()->json(['html' => $md->toHtml($request->input('body', ''))]);
})->middleware(['auth', 'throttle:60,1']);

Render the response into an isolated element, and keep that element's HTML sanitized identically to production — a preview that renders differently from the real page is a bug factory.

5. Hybrid / legacy integration

If you truly need this bundle and also run Laravel (e.g. a Symfony CMS serving marketing pages next to a Laravel app), do not try to boot a Symfony bundle inside Laravel. Run two apps and share content over an API or a database table. If you must port the widget's output, treat its Twig template as the spec: read Resources/views/*.twig, note which filter it uses for conversion, and replicate the conversion settings in league/commonmark so old and new output match.

6. Testing

Snapshot tests are the highest-value thing you can write here, because Markdown rendering differences are subtle and silent:

public function test_markdown_matches_expected_snapshot(): void
{
    $cases = [
        "**bold**" => '<p><strong>bold</strong></p>',
        "<script>alert(1)</script>" => '<p>alert(1)</p>',  // stripped
        "[x](javascript:alert(1))" => '<p><a>x</a></p>',   // unsafe link dropped
    ];

    foreach ($cases as $in => $expected) {
        $this->assertSame($expected, app(Markdown::class)->toHtml($in));
    }
}

Pin the converter version in composer.json and run these tests on every upgrade — CommonMark renderer output does change between majors.

Gotchas and Tips

Abandonment is the headline risk. No commits since 2016 means no security patches, no PHP 8 support guarantee, and Symfony 2.x/3.0-era constraints that will likely conflict with anything modern. composer why-not will tell you fast; expect to be stuck on an older Symfony if you adopt it. For a Laravel app this alone disqualifies it.

html_input => 'allow' is an XSS hole. Markdown permits inline HTML by design. Set 'html_input' => 'strip' (or 'escape') and 'allow_unsafe_links' => false as defaults. Note the difference: strip removes the tag, escape shows it as text. If editors need raw HTML, sanitize after conversion with symfony/html-sanitizer and a strict allow-list — never trust {!! !!} with allow.

Don't compile Markdown inside Blade directives. @markdown($var) looks tidy but compiles once and caches with the view. Wrap it in a component or a helper (markdown($var)) so conversion happens at render, not at view-compile.

Config caching. If you put converter settings in a custom config file, remember php artisan config:cache will freeze them; a changed html_input won't take effect until you clear. Prefer constructor config in the service class for this, since it's environment-independent behavior, not deployment config.

Cache directory permissions. league/commonmark (and the Symfony-era bundle) will want a writable storage/cache path for the environment. In containerized deployments this is a classic "works locally, fails in prod" source of Unable to create cache directory errors. Point it at storage/framework/cache/markdown and ensure it's writable.

Line endings and trailing whitespace. Markdown is whitespace-sensitive: two trailing spaces = <br>, tabs vs. spaces in lists changes nesting. If content arrives from a textarea or an import, normalize CRLF→LF before conversion or you'll get phantom <br>s and broken lists that are painful to debug.

Search. Markdown source in the database means LIKE '%term%' matches **term** and link syntax, producing ugly excerpts. Either store a plain-text rendition alongside the HTML (strip tags from the converted output) for search and excerpts, or index the source with a proper full-text/search engine.

Extension points. For anything beyond CommonMark, reach for league/commonmark extensions (TableExtension, AutolinkExtension, FootnotesExtension, HeadingPermalinkExtension for doc sites) rather than regex post-processing the HTML. If you need custom syntax, implement a Parser/Renderer pair as an extension — that's the supported seam, and it keeps the converter testable.

Debugging output. When rendered HTML is wrong, don't stare at Blade. Dump the conversion in isolation: php artisan tinker → app(App\Services\Markdown::class)->toHtml($sample). Ninety percent of "the widget isn't rendering" reports are either a null source, a converter throwing inside a try/catch that swallows it, or a sanitizer running twice and eating legitimate markup. Add a Log::debug of the raw input length and the output length in the service temporarily.

Migration advice. If you're on Victoire today and moving to Laravel: the widget's payload maps 1:1 to a Markdown block row, the stored text needs no transformation (it's already raw Markdown), and only the renderer changes. Diff a handful of pages' old-vs-new HTML to catch extension differences (tables, autolinks, strikethrough) before switching the front-end. That diff is your acceptance test, and it's the only reason to read this repo at all.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource