Problem/Motivation

Dries Buytaert begins his blog post The Third Audience with an intriguing lead: "I used Claude Code to build a new feature for my site this morning. Any URL on my blog can now return Markdown instead of HTML.". He could have used the markdown_module v1.3.3 which was available on January 14, 2026, but he did not. My two cents on his decision: there is no YAML front matter in the mardownify_module.

Let us look at the .md output of Dries's blog post:

---
url: 'https://dri.es/the-third-audience'
title: 'The Third Audience'
author:
  name: 'Dries Buytaert'
  url: 'https://dri.es/about'
date: '2026-01-14T17:33:29-05:00'
license: 'https://creativecommons.org/licenses/by/4.0/'
type: blog
summary: 'I made my website easier for AI agents and crawlers to read, and within an hour it was getting hundreds of requests.'
tags:
  - Drupal
  - 'Artificial Intelligence'
  - 'My site'
  - Markdown
image: blog/machines-reading-web-content
discussions:
  - { platform: LinkedIn, url: 'https://www.linkedin.com/posts/buytaert_the-third-audience-is-ai-activity-7417339417524076544-ZPDq' }
published: true
featured: false
id: 6051
---

# The Third Audience

Geared towards raw file readers (LLMs/scrapers), the YAML front matter adds a very valuable semantic layer for GEO. In other words, markdownifying your nodes is a precious asset and adding YAML front matter is the state of the art.

In this perspective, I asked Claude to write a custom module for a custom content type on a Drupal CMS 2.1.0 development instance. And it worked, even using a Drupal Canvas v1.3.3 custom template.

So I share my setup as a proof of concept and hope this feature request will be considered.

Please visit this AI generated node with the following .md output:

---
url: 'https://drupalcms.bailleux.ovh/articles-ia/la-nouvelle-ere-du-cms-intelligent'
title: 'La nouvelle ère du CMS intelligent'
author:
  name: 'gillespierrebailleux'
  url: 'https://drupalcms.bailleux.ovh/utilisateur/gilles-pierre-bailleux'
date: '2026-04-24T17:32:36+02:00'
updated: '2026-04-24T21:28:51+02:00'
license: 'https://creativecommons.org/licenses/by/4.0/'
type: article
image: 'https://drupalcms.bailleux.ovh/sites/default/files/2026-04/drupal-ai.jpg'
published: true
featured: false
id: 68
---
   

  #  La nouvelle ère du CMS intelligent 

Steps to reproduce

Create a custom content type named "Article optimisé IA" (article_optimise_ia)
Create a module named "mon_site_markdownify" with the two following files: mon_site_markdownify.info.yml (see attachment) and mon_site_markdownify.module below
DISCLOSURE
Following the Policy on the use of AI when contributing to Drupal, please note that AI was used to create this module.

<?php

/**
 * @file
 * mon_site_markdownify.module
 *
 * Injecte un frontmatter YAML dans les fichiers .md produits par Markdownify
 * pour les nodes de type "article_optimise_ia".
 *
 * Architecture :
 * - hook_markdownify_entity_build_alter  : mémorise l'entité (variable statique)
 * - hook_markdownify_entity_html_alter   : supprime l'image du HTML avant conversion
 * - hook_markdownify_entity_markdown_alter : préfixe le frontmatter YAML
 */

use Drupal\Core\Render\BubbleableMetadata;
use Drupal\node\NodeInterface;

/**
 * Implements hook_markdownify_entity_build_alter().
 *
 * Mémorise l'entité pour les hooks suivants (qui n'ont pas $context['entity']).
 * Ne touche PAS au view_mode : Canvas continue de gérer le rendu full.
 */
function mon_site_markdownify_markdownify_entity_build_alter(array &$build, array $context, ?BubbleableMetadata $metadata): void {
  $entity = $context['entity'] ?? NULL;
  if ($entity instanceof NodeInterface && $entity->bundle() === 'article_optimise_ia') {
    _mon_site_markdownify_current_entity($entity);
  }
}

/**
 * Implements hook_markdownify_entity_html_alter().
 *
 * Supprime du HTML les balises <img> (et leur conteneur éventuel) produites
 * par le champ image, avant que le convertisseur ne les transforme en ![...]().
 * Le frontmatter contient déjà l'URL propre de l'image.
 */
function mon_site_markdownify_markdownify_entity_html_alter(string &$html, array $context, ?BubbleableMetadata $metadata): void {
  $entity = $context['entity'] ?? NULL;
  if (!$entity instanceof NodeInterface || $entity->bundle() !== 'article_optimise_ia') {
    return;
  }

  // Supprimer toutes les balises <img> et leur éventuel <picture> parent.
  $html = preg_replace('/<picture[^>]*>.*?<\/picture>/is', '', $html);
  $html = preg_replace('/<img[^>]+>/i', '', $html);
}

/**
 * Implements hook_markdownify_entity_markdown_alter().
 *
 * Préfixe le Markdown final avec le frontmatter YAML.
 */
function mon_site_markdownify_markdownify_entity_markdown_alter(string &$markdown, array $context): void {
  $entity = _mon_site_markdownify_current_entity();
  if (!$entity instanceof NodeInterface) {
    return;
  }
  $markdown = _mon_site_markdownify_build_frontmatter($entity) . $markdown;
  _mon_site_markdownify_current_entity(NULL, TRUE);
}

/**
 * Stockage statique de l'entité en cours de traitement.
 */
function _mon_site_markdownify_current_entity(?NodeInterface $entity = NULL, bool $reset = FALSE): ?NodeInterface {
  static $stored = NULL;
  if ($entity !== NULL) {
    $stored = $entity;
  }
  if ($reset) {
    $stored = NULL;
  }
  return $stored;
}

/**
 * Construit le frontmatter YAML pour un node article_optimise_ia.
 */
function _mon_site_markdownify_build_frontmatter(NodeInterface $node): string {
  $node_url = $node->toUrl('canonical', ['absolute' => TRUE])->toString();

  $title = ($node->hasField('field_seo_title') && !$node->get('field_seo_title')->isEmpty())
    ? $node->get('field_seo_title')->value
    : $node->label();
  $title_yaml = str_replace("'", "''", $title);

  $owner = $node->getOwner();
  $author_name = str_replace("'", "''", $owner->getDisplayName());
  $author_url  = $owner->toUrl('canonical', ['absolute' => TRUE])->toString();

  $date_created = date('Y-m-d\TH:i:sP', $node->getCreatedTime());
  $date_changed  = date('Y-m-d\TH:i:sP', $node->getChangedTime());

  $published = $node->isPublished() ? 'true' : 'false';

  $image_uri = '';
  if ($node->hasField('field_image_article_optimise_ia')
      && !$node->get('field_image_article_optimise_ia')->isEmpty()) {
    $image_item = $node->get('field_image_article_optimise_ia')->first();
    $file = $image_item->entity;
    if ($file) {
      $image_uri = \Drupal::service('file_url_generator')
        ->generateAbsoluteString($file->getFileUri());
    }
  }

  $tags = [];
  if ($node->hasField('field_tags') && !$node->get('field_tags')->isEmpty()) {
    foreach ($node->get('field_tags') as $tag_ref) {
      if ($tag_ref->entity) {
        $tags[] = str_replace("'", "''", $tag_ref->entity->label());
      }
    }
  }

  $lines   = [];
  $lines[] = '---';
  $lines[] = "url: '{$node_url}'";
  $lines[] = "title: '{$title_yaml}'";
  $lines[] = 'author:';
  $lines[] = "  name: '{$author_name}'";
  $lines[] = "  url: '{$author_url}'";
  $lines[] = "date: '{$date_created}'";

  if ($date_changed !== $date_created) {
    $lines[] = "updated: '{$date_changed}'";
  }

  $lines[] = "license: 'https://creativecommons.org/licenses/by/4.0/'";
  $lines[] = 'type: article';

  if (!empty($tags)) {
    $lines[] = 'tags:';
    foreach ($tags as $tag) {
      $lines[] = "  - '{$tag}'";
    }
  }

  if (!empty($image_uri)) {
    $lines[] = "image: '{$image_uri}'";
  }

  $lines[] = "published: {$published}";
  $lines[] = 'featured: false';
  $lines[] = "id: {$node->id()}";
  $lines[] = '---';
  $lines[] = '';

  return implode("\n", $lines);
}

Comments

gillesbailleux created an issue. See original summary.

gillesbailleux’s picture

StatusFileSize
new153.18 KB

I forgot to mention that during the writing of the module, Claude experienced difficulties with the custom Image field (field_image_article_optimise_ia) which broke the YAML front matter in the .md output of the node. Claude asked me to create a custom view mode named "Mardownify" with the Image field deactivated as shown in the attachment (custom-view-mode.png).

christophweber’s picture

Status: Active » Closed (duplicate)

Thanks so much for the code and pushing to get this done.
I am closing this issue as a duplicate, and all relevant work will be tracked in the original issue https://www.drupal.org/project/markdownify/issues/3512605#comment-16567732

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.