Skip to content

Instantly share code, notes, and snippets.

@mxr576
Created August 27, 2026 10:03
Show Gist options
  • Select an option

  • Save mxr576/8baeab5f6504a5a7dff8e3dba77e8ff0 to your computer and use it in GitHub Desktop.

Select an option

Save mxr576/8baeab5f6504a5a7dff8e3dba77e8ff0 to your computer and use it in GitHub Desktop.
Bulk export Drupal content in Markdown via Markdownify
<?php
/**
* @file
* Mass-exports Drupal nodes to Markdown files using the Markdownify module.
*
* DESCRIPTION:
* This Drush script is designed for the mass-export of nodes into Markdown
* (.md) files. It is optimized to handle a large number of nodes (10,000+)
* efficiently by processing them in configurable chunks to prevent out-of-memory
* issues.
*
* The script generates a clean, hierarchical directory structure for the
* exported content based on their URL aliases. It also creates a comprehensive
* 'export-overview.md' file that provides a summary of all exported items,
* their status, and direct links to the generated files.
*
* USAGE:
* drush php-script export_md.php [<output-directory>]
*
* ARGUMENTS:
* <output-directory> Optional. The path to the export directory. Can be
* relative to the Drupal root or absolute.
* Defaults to 'markdown_export' in the Drupal web root.
*
* EXAMPLES:
* # Export to the default 'web/markdown_export' directory
* drush php-script export_md.php
*
* # Export to a custom absolute directory
* drush php-script export_md.php /path/to/my/exports
*/
use Drupal\node\Entity\Node;
// 1. Determine and prepare output directory.
$contentSubDir = 'content';
$outputDirArg = isset($extra[0]) ? $extra[0] : 'markdown_export';
if (substr($outputDirArg, 0, 1) !== '/') {
$outputDir = DRUPAL_ROOT . '/' . $outputDirArg;
} else {
$outputDir = $outputDirArg;
}
$contentDir = $outputDir . '/' . $contentSubDir;
if (!is_dir($contentDir)) {
if (!mkdir($contentDir, 0755, TRUE)) {
fwrite(STDERR, "Error: Failed to create content directory '$contentDir'\n");
exit(1);
}
}
print "Starting Markdown export...\n";
print "Output directory: $outputDir\n\n";
// 2. Fetch markdownify settings for configured view modes.
$config = \Drupal::config('markdownify.settings');
$supported_entities = $config->get('supported_entities') ?? [];
// 3. Query all nodes (both published and unpublished).
$query = \Drupal::entityTypeManager()->getStorage('node')
->getQuery()
->accessCheck(FALSE);
$nids = $query->execute();
if (empty($nids)) {
print "No nodes found to export.\n";
exit(0);
}
print "Found " . count($nids) . " nodes to export.\n";
$converter = \Drupal::service('markdownify.entity_converter');
$aliasManager = \Drupal::service('path_alias.manager');
$overviewRows = [];
// Define a safe chunk size for processing nodes to avoid memory issues.
define('NODE_CHUNK_SIZE', 50);
// Sort nodes by NID to make the overview consistent and processing predictable.
sort($nids, SORT_NUMERIC);
$nid_chunks = array_chunk($nids, NODE_CHUNK_SIZE);
$total_chunks = count($nid_chunks);
foreach ($nid_chunks as $chunk_index => $nid_chunk) {
print "Processing chunk " . ($chunk_index + 1) . "/$total_chunks...\n";
// Load all node objects for the current chunk in a single query.
$nodes = Node::loadMultiple($nid_chunk);
foreach ($nodes as $node) {
$nid = $node->id();
// Explicitly cast the label to a string to satisfy strict type checking.
$title = (string) $node->label();
$bundle = $node->bundle();
$statusStr = $node->isPublished() ? 'Published' : 'Unpublished';
$canonicalPath = '/node/' . $nid;
// Get the human-readable label of the content type.
$nodeType = \Drupal\node\Entity\NodeType::load($bundle);
$bundleLabel = $nodeType ? (string) $nodeType->label() : $bundle;
// Determine view mode based on Markdownify settings or fallback to 'full'.
$view_mode = 'full';
if (is_array($supported_entities) && isset($supported_entities['node']['view_modes'][$bundle])) {
$view_mode = $supported_entities['node']['view_modes'][$bundle];
}
// Get the path alias if one exists.
$alias = $aliasManager->getAliasByPath($canonicalPath);
// Decide the relative export path of the file.
$relativePath = '';
if (!empty($alias) && $alias !== $canonicalPath && $alias !== '/') {
// Trim leading/trailing slashes.
$relativePath = trim($alias, '/');
} else {
$relativePath = 'node/' . $nid;
}
// Handle homepage or empty paths.
if (empty($relativePath) || $relativePath === '/') {
$relativePath = 'index';
}
// Append extension.
$relativePath .= '.md';
// Sanitize path slightly.
$fullPath = $contentDir . '/' . $relativePath;
$dir = dirname($fullPath);
if (!is_dir($dir)) {
mkdir($dir, 0755, TRUE);
}
try {
print "[*] Exporting Node $nid ($title) -> $relativePath... ";
// Generate Markdown using Markdownify.
$markdown = $converter->convertEntityToMarkdown($node, $view_mode);
// Write markdown file.
if (file_put_contents($fullPath, $markdown) !== FALSE) {
print "Success\n";
$overviewRows[] = [
'nid_link' => "[$nid]($canonicalPath)",
'title' => str_replace(['|', "\n", "\r"], ['\|', ' ', ''], $title),
'bundle_label' => $bundleLabel,
'status' => $statusStr,
'file_path' => $contentSubDir . '/' . $relativePath,
];
} else {
print "Failed to write file\n";
}
} catch (\Throwable $t) {
print "Error (Skipped): " . $t->getMessage() . "\n";
}
}
// Optional: Force garbage collection to free up memory after each chunk.
if (function_exists('gc_collect_cycles')) {
gc_collect_cycles();
}
}
// 4. Generate the Overview Markdown Table.
$overviewFile = $outputDir . '/export-overview.md';
$overviewContent = "# Export Overview\n\n";
$overviewContent .= "Generated on: " . date('Y-m-d H:i:s') . "\n\n";
$overviewContent .= "| Node (ID) | Title | Content Type | Status | File Path |\n";
$overviewContent .= "| --- | --- | --- | --- | --- |\n";
foreach ($overviewRows as $row) {
$overviewContent .= "| {$row['nid_link']} | {$row['title']} | {$row['bundle_label']} | {$row['status']} | [`{$row['file_path']}`]({$row['file_path']}) |\n";
}
if (file_put_contents($overviewFile, $overviewContent) !== FALSE) {
print "\n[+] Created overview table: $overviewFile\n";
} else {
print "\n[-] Failed to create overview table: $overviewFile\n";
}
print "Export process complete!\n";
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment