Skip to content

Instantly share code, notes, and snippets.

@ozkanpakdil
Created August 16, 2025 12:30
Show Gist options
  • Save ozkanpakdil/a2bd26328ae388a479ac0883a92e728e to your computer and use it in GitHub Desktop.
Save ozkanpakdil/a2bd26328ae388a479ac0883a92e728e to your computer and use it in GitHub Desktop.
enhanced JekyllToHugo
Jekyll to Hugo Markdown Converter (with folder structure preservation)
Modified version of fredrikloch/JekyllToHugo to:
Recursively convert .markdown files to Hugo-compatible .md format
Preserve year-based folder structure from Jekyll source
Handle malformed YAML frontmatter gracefully
Useful for migrating large Jekyll collections into Hugo with minimal cleanup.
find . -type f -name "*.markdown" -exec bash -c 'for f; do mv -- "$f" "${f%.markdown}.md"; done' _ {} +
python3 jekyllToHugo.py _microservicetests -o hugo --verbose
```python
#!/usr/bin/env python
# Copyright (C) Fredrik Loch 2015 Jekyll-Hugo
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
__author__ = "Fredrik Loch"
__copyright__ = "Copyright 2015, Jekyll-Hugo"
__license__ = "GPL"
__version__ = "1"
__maintainer__ = "Fredrik Loch"
__email__ = "[email protected]"
__status__ = "Development"
import argparse
import re
import os
import sys
import logging
import yaml
def parseCLI():
command_line = argparse.ArgumentParser(description='Options')
command_line.add_argument("-o", "--output", type=str,
help="Path to output folder, will be created if it does not exist. Defaults to content",
default="content")
command_line.add_argument("-v", "--verbose", action="store_true",
help="Print extra logging output",
default=False)
command_line.add_argument("source", type=str, help="Path to folder containing jekyll posts")
args = command_line.parse_args()
return args
def printLog(level,message):
if verbose:
if level == 1:
logger.info(message)
elif level == 2:
logger.warning(message)
if level == 3:
logger.error(message)
def handlePost(source_file):
filename = os.path.basename(source_file)
printLog(1, "Trying to convert: " + filename)
date_in_filename_match = re.search(r"^\d{4}\-\d{1,2}\-\d{1,2}", filename)
if not date_in_filename_match:
printLog(2, "Unable to parse date from filename")
with open(source_file) as f:
printLog(1, "Parsing front matter")
# Fixed Regex when content contains --- other than the frontmatter sepator
frontmatter_regex = re.compile("---\n([\s\S]*?)---\n([\s\S]*)")
date_regex = re.compile("^\s+(\d+\-\d+\d+)")
content = f.read()
parts = content.split('---')
if len(parts) < 3:
printLog(3, f"Malformed frontmatter in file: {source_file}")
return
try:
frontmatter_yaml = yaml.safe_load(parts[1])
except yaml.YAMLError as e:
printLog(3, f"YAML parsing error in file {source_file}: {e}")
return
content_body = '---'.join(parts[2:])
# Compute relative path from source root
relative_path = os.path.relpath(source_file, arguments.source)
relative_dir = os.path.dirname(relative_path)
output_path = os.path.join(arguments.output, relative_dir)
# Create output directory if needed
if not os.path.exists(output_path):
printLog(1, f"Creating folder: {output_path}")
os.makedirs(output_path)
output_filename = os.path.join(output_path, filename)
with open(output_filename, 'w') as nf:
nf.write("---" + os.linesep)
for key in frontmatter_yaml:
if not frontmatter_yaml[key] is None:
if key == 'date':
# Hugo expects to have only YYYY-MM-DD but Jekyll can have time as well
date_regex_search_result = re.search(date_regex, str(frontmatter_yaml[key]))
if date_regex_search_result:
printLog(1, "Date found in Frontmatter: {}".format(date_regex_search_result.group(1)))
nf.write('{}: "{}"{}'.format(key,date_regex_search_result.group(1) + "\"", os.linesep))
elif key in ["tags","categories"]:
value = frontmatter_yaml[key]
# Hugo expects a Go list for tags and categories
nf.write('{}: "{}"{}'.format(key, str(value.split(" ") if isinstance(value, str) else (value if isinstance(value, list) else [])), os.linesep))
elif key == "status":
if frontmatter_yaml[key] == "draft":
nf.write('draft: true{}'.format(os.linesep))
elif key == "summary":
nf.write('description: "{}"{}'.format(str(frontmatter_yaml[key]), os.linesep))
elif key == "permalink":
nf.write('url: "{}"{}'.format(str(frontmatter_yaml[key]), os.linesep))
elif key == "layout":
nf.write('type: "{}"{}'.format(str(frontmatter_yaml[key]), os.linesep))
else:
nf.write(yaml.dump({key: frontmatter_yaml[key]}, default_flow_style=False))
if not "date" in frontmatter_yaml and date_in_filename_match:
printLog(1, "Unable to find date from Frontmatter, using date from filename")
nf.write('date: "{}"{}'.format(date_in_filename_match.group(0), os.linesep))
nf.write("---" + os.linesep)
# Ugly fix for syntax highlighting.
text = content_body.replace(
"{% highlight",
"{{< highlight").replace("%}", ">}}").replace("{% endhighlight",
"{{< /highlight"
)
nf.write(text)
printLog(1, "Converted file: {}".format(output_filename))
if __name__ == '__main__':
logger = logging.getLogger("standard logger")
logger.setLevel(logging.INFO)
format = '%(asctime)s - %(levelname)s - %(message)s'
logging.basicConfig(format=format)
arguments = parseCLI()
arguments.source = os.path.abspath(arguments.source)
arguments.output = os.path.abspath(arguments.output)
verbose = arguments.verbose
if os.path.exists(arguments.source):
printLog(1, "Creating folder: {} for output".format(arguments.output))
if not os.path.exists(arguments.output):
os.makedirs(arguments.output)
for r, d, f in os.walk(arguments.source):
for f1 in f:
if re.match(r".*\.md$", f1):
handlePost(os.path.join(r, f1))
else:
printLog(3, "Source folder not found, make sure that the folder {} exists.".format(arguments.source))
sys.exit(-1)
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment