Context & problem statement
Tutorials on this site are written in Markdown with fenced code blocks. The first choice was the contrib drupal/markdown module (3.1.0) with league/commonmark 2.10 as the parser, on Drupal 11.4 and PHP 8.5.
In testing, the module's CommonMark GFM parser returned damaged output. Given ## Heading and **bold**, the rendered body kept ## Heading as literal text and dropped the bold markers and all HTML tags. Adding core's HTML restrictor to the text format did not change the result, because the output was already stripped when it left the parser plugin.
Calling league/commonmark directly with the same input produced correct HTML: <h2>, <strong>, <del> and <pre><code class="language-php">. Uninstalling the contrib module also raised a PHP 8.5 deprecation from its own code (ArrayIterator with an object as backing array).
The site needs a Markdown filter that is correct, safe for untrusted input, and works with highlight.php for server-side syntax highlighting.
Decision outcome
Remove drupal/markdown and add a custom module, tech_markdown, with a single filter plugin that wraps League\CommonMark\GithubFlavoredMarkdownConverter.
- The converter runs with
html_input: escape, so raw HTML in a post is shown as text, and allow_unsafe_links: false, so javascript: links are dropped.
- The Markdown text format runs three filters in order: the CommonMark filter, core's
filter_html with an allowlist suited to technical writing (headings, code and pre with a class, tables, task-list checkboxes, images), then highlight.php.
- The plugin is declared with the
#[Filter] attribute and keeps the converter in a static property so the plugin stays serializable.
- Unit tests cover headings, emphasis, fenced code with a language class, strikethrough, tables, task lists, and escaping of
<script> and javascript: links.
Consequences & trade-offs
Markdown renders correctly and code blocks are highlighted on the server, so pages ship no client-side highlighting script.
The site now owns one small plugin and its tests. Parser upgrades come through league/commonmark in composer.lock and should be checked against the unit tests.
The contrib module's admin UI for choosing parsers and toggling extensions per format is gone. Changing CommonMark options means editing the plugin.
If a later drupal/markdown release fixes the output problem, switching back is a change to the text format's filter list. Existing content does not need to change, because the stored Markdown is the same either way.