Created
August 27, 2026 10:03
-
-
Save mxr576/8baeab5f6504a5a7dff8e3dba77e8ff0 to your computer and use it in GitHub Desktop.
Bulk export Drupal content in Markdown via Markdownify
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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