Extending CommonMark in Laravel: Custom Syntax & Highlighting
One of the main reasons to build a custom blog instead of using a pre-packaged CMS is control. When your content is Markdown-first, the parser is the heart of your application.
Laravel (and most of the PHP ecosystem) relies on league/commonmark. While it's excellent out of the box, its true power lies in its extensibility.
In this post, we'll explore how to write custom extensions to add features like "Alert" boxes and enhanced code highlighting.
The Goal: Custom Alert Blocks
We want to support a syntax like this in our Markdown files:
::: note
This is a note for the reader.
:::
::: warning
Watch out! This is a warning.
:::
And render it as:
<div class="alert alert-note">
<p>This is a note for the reader.</p>
</div>
Step 1: Install Dependencies
While we can write the parser from scratch, the league/commonmark ecosystem already has a nice "attributes" extension, but for block-level elements, a custom syntax is often cleaner.
For this example, checking out league/commonmark documentation on Extensions is key.
Step 2: Architecture of an Extension
An extension in CommonMark usually consists of:
- Parser: detailed logic to regex-match the
:::and identify the block. - Node: A class representing the Abstract Syntax Tree (AST) node.
- Renderer: A class that turns the Node into HTML.
However, a simpler way for many use cases is using the generic AttributesExtension or writing a simple Inline Parser if you just want simple text replacements. But for blocks, we need a Block Parser.
Let's do something simpler effectively supported by the default GFM extensions: using Blockquotes with a twist, or just handling it via styling, but let's go for a real custom extension approach using a container package if you want to avoid writing regex parsers manually.
A popular alternative is simonvane/commonmark-admonitions-extension (if available) or similar. But let's look at how to register a simple renderer.
Configuration in Laravel
Where do we wire this up? In your MarkdownPostService or a MarkdownServiceProvider.
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
use League\CommonMark\MarkdownConverter;
class MarkdownPostService
{
public function parse(string $content): string
{
$environment = new Environment([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new GithubFlavoredMarkdownExtension());
// Register custom extensions here
// $environment->addExtension(new MyCustomAlertExtension());
$converter = new MarkdownConverter($environment);
return $converter->convert($content);
}
}
Syntax Highlighting with Torchlight or Shiki
Standard highlight.js or Prism runs on the client. For a performance-obsessed blog, we want server-side highlighting.
Option A: Torchlight (SaaS)
Torchlight is fantastic for Laravel.
composer require torchlight/torchlight-commonmark- Add the extension:
$environment->addExtension(new \Torchlight\Commonmark\V2\TorchlightExtension()); - Set your API token in
.env.
Option B: Spatie Shiki (Local Node)
If you have Node installed on your server (or build process):
composer require spatie/laravel-markdown
npm install shiki
This package wraps standard CommonMark and adds Shiki support.
Writing a Simple HTML Renderer
If you don't want a full parser, you can hook into the rendering phase. For example, ensuring all external links open in a new file.
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
class ExternalLinkRenderer implements NodeRendererInterface
{
public function render(Node $node, ChildNodeRendererInterface $childRenderer)
{
/** @var Link $node */
$attrs = $node->data->get('attributes');
if ($this->isExternal($node->getUrl())) {
$attrs['target'] = '_blank';
$attrs['rel'] = 'noopener noreferrer';
}
return new HtmlElement('a', $attrs, $childRenderer->renderNodes($node->children()));
}
private function isExternal($url) {
return parse_url($url, PHP_URL_HOST) !== request()->getHost();
}
}
Register it with high priority:
$environment->addRenderer(Link::class, new ExternalLinkRenderer(), 10);
Conclusion
Customizing the Markdown parser allows you to treat Markdown as a domain-specific language for your blog. Whether it's safely handling external links, adding custom "Call to Action" buttons, or rendering complex diagrams, the league/commonmark architecture in Laravel is flexible enough to handle it all.