twig/markdown-extra
Twig extension that adds markdown_to_html and html_to_markdown filters for converting Markdown blocks to HTML and HTML back to Markdown directly in your Twig templates, with configurable parsers.
Install the Package:
composer require twig/markdown-extra
If using Laravel with Twig, ensure your composer.json pins the new release:
"require": {
"twig/twig": "^3.0",
"twig/markdown-extra": "^3.29.0"
}
(Optional) Choose a Markdown Backend:
The extension ships with a default CommonMark-based converter. As of v3.29.0 you can also use tempest/markdown as the rendering engine:
composer require tempest/markdown
Pick a backend up front — converter choice affects supported syntax (tables, footnotes, attributes) and how you write custom extensions.
Register the Extension:
In your AppServiceProvider (or TwigServiceProvider if using a custom setup):
use Twig\Environment;
use Twig\Extension\MarkdownExtraExtension;
public function boot()
{
// Default backend
$this->app->make(Environment::class)->addExtension(new MarkdownExtraExtension());
// Or, v3.29.0+: explicit tempest/markdown backend
// $this->app->make(Environment::class)->addExtension(
// new MarkdownExtraExtension(new \Tempest\Markdown\Markdown())
// );
}
First Use Case: Render Markdown in a Twig template:
{{ markdown_content|markdown_to_html }}
Convert HTML back to Markdown:
{{ html_content|html_to_markdown }}
tempest/markdown support; nothing is deprecated or removed.Environment is built — that is the single place extension registration should happen.{# Forum post rendering #}
<div class="post">
{{ user_post.markdown|markdown_to_html }}
</div>
|e escaping needed.// config/twig.php or a dedicated service provider
use Tempest\Markdown\Markdown;
use Twig\Extension\MarkdownExtraExtension;
$converter = new Markdown(); // tempest/markdown
$twig->addExtension(new MarkdownExtraExtension($converter));
$this->app->bind(MarkdownConverterInterface::class, fn () => new Markdown());
// Laravel Controller: Convert HTML to Markdown for version control
public function updateLegacyContent()
{
$html = file_get_contents('legacy_post.html');
$markdown = $this->twig->render(
'{{ html|html_to_markdown }}',
['html' => $html]
);
file_put_contents('post.md', $markdown);
}
// Blade directive for Markdown (custom extension)
Blade::directive('markdown', function ($expression) {
return "<?php echo app(\\Twig\\Environment::class)->render('{{ content|markdown_to_html }}', ['content' => {$expression}]); ?>";
});
Usage:
@markdown($post->content)
// Cache the rendered HTML for 1 hour
$cachedHtml = Cache::remember("markdown_{$post->id}", now()->addHour(), function () use ($post) {
return $this->twig->render('{{ content|markdown_to_html }}', ['content' => $post->markdown]);
});
tempest/markdown will change output for the same input.markdown_to_html filter in custom fields to render Markdown in the admin panel.public function updatedMarkdown()
{
$this->renderedHtml = $this->twig->render(
'{{ markdown|markdown_to_html }}',
['markdown' => $this->markdown]
);
}
return response()->json([
'content' => $this->twig->render('{{ body|markdown_to_html }}', ['body' => $request->body]),
]);
$markdown = $this->twig->render('{{ html_content|html_to_markdown }}', ['html_content' => $request->html]);
// Override escaping for trusted Markdown (e.g., admin-only)
$twig->addFilter(new \Twig\TwigFilter('trusted_markdown', function ($markdown) use ($converter) {
return $converter->convert($markdown);
}));
Warning: Only use this for explicitly trusted sources (e.g., admin inputs).
// Register extra syntax on the chosen backend, then pass it to the extension
$converter = new \League\CommonMark\CommonMarkConverter([
'commonmark' => ['use_asterisk' => true],
]);
$twig->addExtension(new MarkdownExtraExtension($converter));
MarkdownExtraExtension; the extension holds a reference to the converter, not a copy.Double Escaping:
|e (Twig’s escape filter) after markdown_to_html breaks HTML rendering.{# Wrong: Double-escapes #}
{{ user_comment|markdown_to_html|e }}
{# Correct: Auto-escaping is handled #}
{{ user_comment|markdown_to_html }}
Backend Output Drift (v3.29.0):
tempest/markdown changes rendered HTML (whitespace, table markup, footnote anchors) even though your templates are untouched.Legacy HTML Breakage:
<div><a href="..."> in Markdown).html_to_markdown cautiously for complex HTML. Test with:
{{ legacy_html|html_to_markdown|markdown_to_html }}
Custom Filter XSS Risks:
$twig->addFilter(new \Twig\TwigFilter('safe_markdown', [$extension, 'renderMarkdown'], ['is_safe' => ['html']]));
Twig Version Conflicts:
composer.json:
"require": {
"twig/twig": "^3.0",
"twig/markdown-extra": "^3.29.0"
}
Optional Dependency Guard:
Tempest\Markdown\Markdown when tempest/markdown is not installed throws a class-not-found at boot.class_exists() check or a container binding resolved lazily.Performance with Large Content:
dump filter to debug Markdown parsing:
{{ markdown_content|markdown_to_html|dump }}
{{ '<script>alert(1)</script>'|markdown_to_html }} {# Should render as text #}
dump(get_class(app(\Twig\Environment::class)->getExtension(\Twig\Extension\MarkdownExtraExtension::class)));
config/twig.php:
'debug' => env('APP_DEBUG', true),
Auto-Escaping Override: The package cannot disable auto-escaping post-v3.26.0. For trusted content, use custom filters (see above).
Extension Registration:
MarkdownExtraExtension exactly once, and assert it in a boot test.Markdown Flavor: The default backend is CommonMark-based and may differ from GitHub-flavored Markdown (e.g., tables, task lists). Configure the converter explicitly:
$converter = new \League\CommonMark\CommonMarkConverter([
'html_input' => 'escape',
'allow_unsafe_links' => false,
]);
Alternatively, adopt tempest/markdown (v3.29.0) if its syntax coverage better matches your content — just document the choice in your README.
Custom Markdown Extensions: Register backend-specific syntax extensions on the converter, then hand the configured converter to the Twig extension:
$converter = new \League\CommonMark\CommonMarkConverter([...]);
$twig->addExtension(new MarkdownExtraExtension($converter));
Pluggable Converter (v3.29.0):
Because the Twig extension now accepts alternative implementations such as tempest/markdown, you can wrap your own converter (or a decorator that logs/telemetry-wraps conversions) and inject it:
$this->app->singleton(MarkdownExtraExtension::class, function ($app) {
return new MarkdownExtraExtension($app->make(MyLoggingConverter::class));
});
Pre/Post-Processing: Hook into the parsing pipeline via a decorator around the converter rather than patching the Twig extension — this keeps upgrades (including future backend additions like #4890) non-breaking.
How can I help you explore Laravel packages today?