- Home
-
/ Batch Convert
Batch Convert Markdown to Word: Process Multiple Files at Once
When you have dozens or even hundreds of Markdown files to convert to Word, doing them one by one is simply not practical. This guide walks you through three proven methods to bulk convert multiple .md files to .docx format -- from simple shell scripts to full Node.js automation and CI/CD pipelines.
3 Approaches Compared
Before diving into each method, here is a quick comparison to help you choose the right approach for your situation:
| Feature | Pandoc Script | Node.js Automation | Online Tools |
|---|---|---|---|
| Setup Difficulty | Easy (install Pandoc) | Medium (Node.js + packages) | None |
| Max File Volume | Unlimited | Unlimited | ~10-20 files |
| Formatting Control | Excellent (reference-doc) | Full programmatic control | Basic |
| CI/CD Integration | Straightforward | Straightforward | Not supported |
| Custom Styling | Via reference .docx | Via code (headers, fonts, etc.) | Limited |
| Best For | Quick bulk conversions | Complex workflows | Small one-off batches |
Method 1: Pandoc Batch Conversion
Pandoc is the gold standard for document conversion. It runs on Windows, macOS, and Linux, and supports Markdown to DOCX natively. With a simple shell loop, you can convert hundreds of files in seconds.
Prerequisites
Install Pandoc from the official site or via a package manager:
# macOS brew install pandoc # Ubuntu / Debian sudo apt-get install pandoc # Windows (via Chocolatey) choco install pandoc
Bash Script (macOS / Linux)
Save the following script as batch_convert.sh in the root of your Markdown folder:
#!/bin/bash
# batch_convert.sh - Convert all .md files to .docx using Pandoc
# Usage: bash batch_convert.sh [input_dir] [output_dir]
INPUT_DIR="${1:-.}"
OUTPUT_DIR="${2:-./docx_output}"
mkdir -p "$OUTPUT_DIR"
# Count total files for progress tracking
TOTAL=$(find "$INPUT_DIR" -name "*.md" -type f | wc -l)
COUNT=0
echo "Found $TOTAL Markdown files in $INPUT_DIR"
echo "Output directory: $OUTPUT_DIR"
echo "---"
find "$INPUT_DIR" -name "*.md" -type f | while read -r file; do
COUNT=$((COUNT + 1))
# Preserve relative directory structure
REL_PATH="${file#$INPUT_DIR/}"
REL_DIR=$(dirname "$REL_PATH")
BASENAME=$(basename "$file" .md)
# Create output subdirectory if needed
mkdir -p "$OUTPUT_DIR/$REL_DIR"
OUTPUT_FILE="$OUTPUT_DIR/$REL_DIR/$BASENAME.docx"
echo "[$COUNT/$TOTAL] Converting: $REL_PATH"
pandoc "$file" -o "$OUTPUT_FILE" \
--from markdown \
--to docx \
--standalone
if [ $? -eq 0 ]; then
echo " -> $OUTPUT_FILE"
else
echo " ERROR: Failed to convert $file" >&2
fi
done
echo "---"
echo "Batch conversion complete!"
Make it executable and run:
chmod +x batch_convert.sh ./batch_convert.sh ./docs ./output
PowerShell Script (Windows)
For Windows users, save this as batch_convert.ps1:
# batch_convert.ps1 - Convert all .md files to .docx using Pandoc
# Usage: .\batch_convert.ps1 -InputDir .\docs -OutputDir .\docx_output
param(
[string]$InputDir = ".",
[string]$OutputDir = ".\docx_output"
)
# Create output directory
New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null
$files = Get-ChildItem -Path $InputDir -Filter "*.md" -Recurse
$total = $files.Count
$count = 0
Write-Host "Found $total Markdown files in $InputDir"
Write-Host "Output directory: $OutputDir"
Write-Host "---"
foreach ($file in $files) {
$count++
# Preserve relative directory structure
$relativePath = $file.FullName.Substring((Resolve-Path $InputDir).Path.Length + 1)
$relativeDir = Split-Path $relativePath -Parent
$baseName = $file.BaseName
$outputSubDir = Join-Path $OutputDir $relativeDir
New-Item -ItemType Directory -Force -Path $outputSubDir | Out-Null
$outputFile = Join-Path $outputSubDir "$baseName.docx"
Write-Host "[$count/$total] Converting: $relativePath"
pandoc $file.FullName -o $outputFile `
--from markdown `
--to docx `
--standalone
if ($LASTEXITCODE -eq 0) {
Write-Host " -> $outputFile"
} else {
Write-Error "Failed to convert $($file.FullName)"
}
}
Write-Host "---"
Write-Host "Batch conversion complete!"
Run it from PowerShell:
.\batch_convert.ps1 -InputDir .\my-markdown-files -OutputDir .\converted
Tip: Both scripts preserve the original folder hierarchy. If your source has docs/api/README.md, the output will be output/api/README.docx.
Method 2: Node.js Automation
Why Node.js? When you need fine-grained control over headings, fonts, table styles, or want to inject metadata into each document, a programmatic approach gives you full flexibility. This method uses the unified ecosystem to parse Markdown and the docx library to build Word files from scratch.
Project Setup
mkdir md-to-docx-batch && cd md-to-docx-batch npm init -y npm install unified remark-parse remark-stringify docx glob fs-extra
Conversion Script
Create convert.js:
const fs = require("fs-extra");
const path = require("path");
const { glob } = require("glob");
const { unified } = require("unified");
const remarkParse = require("remark-parse");
const { Document, Packer, Paragraph, TextRun, HeadingLevel,
AlignmentType, Table, TableRow, TableCell, BorderStyle } = require("docx");
// Configuration
const INPUT_DIR = process.argv[2] || "./markdown";
const OUTPUT_DIR = process.argv[3] || "./docx_output";
const FONT_FAMILY = "Calibri";
const FONT_SIZE = 24; // Half-points (24 = 12pt)
// Parse Markdown AST into docx Paragraphs
function astToDocxChildren(node) {
const children = [];
for (const child of node.children || []) {
switch (child.type) {
case "heading": {
const levels = {
1: HeadingLevel.HEADING_1,
2: HeadingLevel.HEADING_2,
3: HeadingLevel.HEADING_3,
};
const text = extractText(child);
children.push(
new Paragraph({
heading: levels[child.depth] || HeadingLevel.HEADING_3,
children: [new TextRun({ text, font: FONT_FAMILY })],
})
);
break;
}
case "paragraph": {
const runs = extractRuns(child);
children.push(new Paragraph({ children: runs }));
break;
}
case "code": {
children.push(
new Paragraph({
children: [
new TextRun({
text: child.value,
font: "Courier New",
size: 20,
}),
],
spacing: { before: 120, after: 120 },
})
);
break;
}
case "list": {
for (const item of child.children) {
const text = extractText(item);
children.push(
new Paragraph({
children: [
new TextRun({ text: ` \u2022 ${text}`, font: FONT_FAMILY, size: FONT_SIZE }),
],
})
);
}
break;
}
case "blockquote": {
const text = extractText(child);
children.push(
new Paragraph({
children: [
new TextRun({ text, italics: true, font: FONT_FAMILY, size: FONT_SIZE }),
],
indent: { left: 720 },
})
);
break;
}
default:
break;
}
}
return children;
}
// Helper: extract plain text from an AST node
function extractText(node) {
if (node.value) return node.value;
if (node.children) return node.children.map(extractText).join("");
return "";
}
// Helper: extract TextRuns with basic formatting
function extractRuns(node) {
const runs = [];
for (const child of node.children || []) {
if (child.type === "text") {
runs.push(new TextRun({ text: child.value, font: FONT_FAMILY, size: FONT_SIZE }));
} else if (child.type === "strong") {
runs.push(new TextRun({ text: extractText(child), bold: true, font: FONT_FAMILY, size: FONT_SIZE }));
} else if (child.type === "emphasis") {
runs.push(new TextRun({ text: extractText(child), italics: true, font: FONT_FAMILY, size: FONT_SIZE }));
} else if (child.type === "inlineCode") {
runs.push(new TextRun({ text: child.value, font: "Courier New", size: 20 }));
} else if (child.type === "link") {
runs.push(new TextRun({ text: extractText(child), font: FONT_FAMILY, size: FONT_SIZE, color: "2563EB" }));
} else {
runs.push(new TextRun({ text: extractText(child), font: FONT_FAMILY, size: FONT_SIZE }));
}
}
return runs;
}
async function convertFile(inputPath, outputPath) {
const markdown = await fs.readFile(inputPath, "utf-8");
const tree = unified().use(remarkParse).parse(markdown);
const docChildren = astToDocxChildren(tree);
const doc = new Document({
sections: [{ children: docChildren }],
});
const buffer = await Packer.toBuffer(doc);
await fs.ensureDir(path.dirname(outputPath));
await fs.writeFile(outputPath, buffer);
}
async function main() {
const files = await glob("**/*.md", { cwd: INPUT_DIR });
console.log(`Found ${files.length} Markdown files in ${INPUT_DIR}`);
let success = 0;
let failed = 0;
for (const file of files) {
const inputPath = path.join(INPUT_DIR, file);
const outputPath = path.join(OUTPUT_DIR, file.replace(/\.md$/, ".docx"));
try {
await convertFile(inputPath, outputPath);
console.log(` Converted: ${file}`);
success++;
} catch (err) {
console.error(` FAILED: ${file} - ${err.message}`);
failed++;
}
}
console.log(`\nDone! ${success} converted, ${failed} failed.`);
}
main().catch(console.error);
Run the script:
node convert.js ./my-markdown-files ./word-output
This approach is ideal when you need to programmatically control fonts, heading sizes, page margins, or inject custom metadata like author names and creation dates into each document. You can extend the astToDocxChildren function to handle tables, images, horizontal rules, and any other Markdown element your documents use.
Method 3: Online Tools for Small Batches
If you only have a handful of files (under 10-20) and do not want to install any software, browser-based converters are the fastest path. Our own Markdown to Word converter handles this elegantly.
Step-by-Step with Our Converter
- Open the Markdown to Word converter in your browser.
- Paste the contents of your first Markdown file into the editor, or drag-and-drop the
.mdfile directly. - Preview the rendered output to verify formatting is correct.
- Click "Download DOCX" to save the Word file.
- Repeat for each remaining file. The converter runs entirely in your browser, so your data never leaves your machine.
When to Use Online Tools vs Scripts
Use Online Tools When:
- - You have fewer than 20 files
- - You need a quick one-time conversion
- - You cannot install software on your machine
- - You want to preview before downloading
Use Scripts When:
- - You have 20+ files or convert regularly
- - You need consistent, repeatable output
- - You want to integrate with CI/CD
- - You need custom styling or metadata
Folder Structure and Output Organization
When batch converting, maintaining an organized output structure is critical. Both the Bash and Node.js scripts above preserve the source folder hierarchy by default. Here is a practical example of what that looks like:
# Source structure
docs/
README.md
api/
endpoints.md
authentication.md
guides/
getting-started.md
advanced-usage.md
troubleshooting.md
# After batch conversion
docx_output/
README.docx
api/
endpoints.docx
authentication.docx
guides/
getting-started.docx
advanced-usage.docx
troubleshooting.docx
Tips for Large Projects
-
1.
Keep source and output separate. Never write
.docxfiles into the same directory as your.mdsources. This avoids confusion and makes cleanup easy. -
2.
Add the output directory to
.gitignore. Generated Word files should not be committed to version control -- they are build artifacts. -
3.
Use timestamps for versioning. Append a date or git hash to the output directory name (e.g.,
docx_output_2026-03-18) to keep track of different batches. -
4.
Exclude specific files. Add a pattern to skip files like
CHANGELOG.mdornode_modules/**/*.mdthat should not be converted.
For the Bash script, add an exclusion pattern by modifying the find command:
find "$INPUT_DIR" -name "*.md" -type f \
-not -path "*/node_modules/*" \
-not -name "CHANGELOG.md" \
| while read -r file; do
# ... conversion logic
done
Maintaining Consistent Styles Across All Documents
One of the biggest challenges with batch conversion is ensuring every Word document looks identical -- same fonts, heading sizes, margins, and spacing. Pandoc solves this elegantly with the --reference-doc parameter.
Creating a Reference Document
- Generate a default reference: Run
pandoc -o custom-reference.docx --print-default-data-file reference.docx - Open
custom-reference.docxin Word and modify the styles:- Set your desired font family and sizes for Normal, Heading 1, Heading 2, Heading 3
- Configure page margins (e.g., 2.5 cm left/right, 1 cm top/bottom)
- Set line spacing, paragraph spacing, and indentation
- Customize table styles, code block formatting, and blockquote styles
- Save the file -- this becomes your brand template.
Using the Reference Document in Batch Scripts
Update the Pandoc command in your batch script to include the reference document:
pandoc "$file" -o "$OUTPUT_FILE" \
--from markdown \
--to docx \
--standalone \
--reference-doc=custom-reference.docx
Every converted document will now inherit the styles defined in your reference file. This is particularly powerful for corporate documentation where brand consistency matters.
Pro Tip: Store your custom-reference.docx in version control alongside your Markdown source. This way, style updates propagate automatically when you re-run the batch conversion.
CI/CD Integration with GitHub Actions
Automate your batch conversions so that every time you push Markdown changes, Word documents are generated automatically. Here is a complete GitHub Actions workflow:
# .github/workflows/convert-markdown.yml
name: Batch Convert Markdown to Word
on:
push:
paths:
- 'docs/**/*.md'
workflow_dispatch:
jobs:
convert:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Pandoc
run: |
sudo apt-get update
sudo apt-get install -y pandoc
- name: Run batch conversion
run: |
INPUT_DIR="docs"
OUTPUT_DIR="docx_output"
mkdir -p "$OUTPUT_DIR"
find "$INPUT_DIR" -name "*.md" -type f | while read -r file; do
REL_PATH="${file#$INPUT_DIR/}"
REL_DIR=$(dirname "$REL_PATH")
BASENAME=$(basename "$file" .md)
mkdir -p "$OUTPUT_DIR/$REL_DIR"
OUTPUT_FILE="$OUTPUT_DIR/$REL_DIR/$BASENAME.docx"
echo "Converting: $REL_PATH"
pandoc "$file" -o "$OUTPUT_FILE" \
--from markdown \
--to docx \
--standalone \
--reference-doc=custom-reference.docx
done
- name: Upload Word documents as artifact
uses: actions/upload-artifact@v4
with:
name: word-documents
path: docx_output/
retention-days: 30
- name: Summary
run: |
echo "### Conversion Complete" >> $GITHUB_STEP_SUMMARY
echo "Converted $(find docx_output -name '*.docx' | wc -l) files" >> $GITHUB_STEP_SUMMARY
How This Workflow Works
-
1.
Triggers on any push that modifies
.mdfiles inside thedocs/directory, or manually viaworkflow_dispatch. - 2. Installs Pandoc on the Ubuntu runner.
- 3. Runs the batch conversion with your reference document for consistent styling.
- 4. Uploads the results as a downloadable GitHub Actions artifact retained for 30 days.
- 5. Posts a summary with the file count to the Actions run page.
Advanced: You can extend this workflow to commit the generated DOCX files back to a separate branch, upload them to a cloud storage bucket (S3, GCS), or send a Slack notification when the conversion completes.
Frequently Asked Questions
How many Markdown files can I batch convert at once?
With Pandoc or Node.js scripts, there is no practical limit. Users routinely convert thousands of files in a single run. The speed depends on file size and system resources, but Pandoc typically processes 50-100 files per second on modern hardware. Online tools are limited to roughly 10-20 files per session due to browser memory constraints.
Will images and tables survive the batch conversion?
Pandoc handles images and tables well when converting to DOCX. Local images referenced with relative paths (e.g., ) are embedded into the Word file. Remote images (URLs) are downloaded and embedded automatically. Tables are converted to native Word tables with proper cell formatting.
Can I convert Markdown to .doc (old Word format) instead of .docx?
Pandoc natively outputs .docx (Office Open XML), not the legacy .doc format. If you need .doc files, you can batch convert the DOCX output using LibreOffice in headless mode: libreoffice --headless --convert-to doc *.docx
How do I handle front matter (YAML) in Markdown files?
Pandoc reads YAML front matter automatically and uses fields like title, author, and date as document metadata in the DOCX output. Add the --standalone flag (already included in our scripts) to ensure metadata is written to the Word file properties.
Is it possible to convert only files that changed since the last run?
Yes. In a Git-based workflow, use git diff --name-only HEAD~1 -- '*.md' to get the list of changed Markdown files, then pass only those to Pandoc. In the GitHub Actions workflow, you can use the tj-actions/changed-files action to detect which files were modified in the current push.
Start Converting Your Markdown Files
Try our free online converter for quick conversions, or use the scripts above for large-scale automation.
Try the Online Converter100% free - No sign-up - Privacy-first