Import a Jekyll Site into Hugo Without Overwriting Work
You will import an existing Jekyll site into a separate Hugo target directory, then inspect the generated files before using them. The examples use Hugo 0.123.7, installed from the Ubuntu hugo package on this machine. Allow about fifteen minutes for a small site, plus time to review the imported content. The import changes files in the target directory, so keep the Jekyll source and a backup of any existing Hugo work.
The route
Jump straight to the step you need, or tick off Done means at the end.
This guide uses the installed command's documented contract: hugo import jekyll JEKYLL_ROOT TARGET. Both paths are required. The command accepts --force when you deliberately need to import into a non-empty target.
1. Check the installed command
First confirm the binary and version. These are ordinary read-only commands and do not need elevated privileges:
$ command -v hugo
/usr/bin/hugo
$ hugo version
hugo v0.123.7+extended linux/amd64 BuildDate=2026-03-17T19:51:14Z VendorInfo=ubuntu:0.123.7-1ubuntu0.3+esm2
$ hugo import jekyll --help
Import from Jekyll requires two paths, e.g. hugo import jekyll jekyll_root_path target_path.
The exact help output can include additional inherited options. The useful boundary is that jekyll is the subcommand, followed by the source root and target path. Do not substitute a general Hugo build command: this operation is an import.
2. Prepare a clean target
Choose an explicit source directory and a new target. Replace the example paths with directories you control:
$ JEKYLL_ROOT=/srv/sites/example-jekyll
$ HUGO_TARGET=/srv/sites/example-hugo-import
$ test -d "$JEKYLL_ROOT" && echo "source directory exists"
$ test ! -e "$HUGO_TARGET" && echo "target path is unused"
source directory exists
target path is unused
Checkpoint: the first test should succeed and the second should print target path is unused. If the target already exists, stop and inspect it. The normal command refuses a non-empty target, but it is safer to decide what belongs there before asking Hugo to write.
You need read access to the Jekyll source and write access to the target's parent directory. Run as your normal account where possible. Use sudo only if the chosen paths genuinely require it; elevated privileges can leave root-owned output that your ordinary account cannot later edit.
3. Confirm the Jekyll root has importable content
Look for the content that makes this a Jekyll root, without modifying anything:
$ find "$JEKYLL_ROOT" -maxdepth 2 -type d \( -name _posts -o -name _drafts \) -print
/srv/sites/example-jekyll/_posts
$ find "$JEKYLL_ROOT/_posts" -maxdepth 1 -type f -print | head
/srv/sites/example-jekyll/_posts/2026-09-20-first-post.md
This is a checkpoint, not a requirement to create empty directories. On this installed version, an empty root fails with abort: jekyll root contains neither posts nor drafts and exits with status 1. If neither _posts nor _drafts exists or contains the material you expect, correct the source selection rather than forcing the import.
4. Run the import into the clean target
With the source and target checked, run the import:
$ hugo import jekyll "$JEKYLL_ROOT" "$HUGO_TARGET"
Import Jekyll from: /srv/sites/example-jekyll to: /srv/sites/example-hugo-import
A zero exit status means the command completed. It does not prove that every post, draft, link or template matches your intended Hugo site. Hugo writes the imported result under the target path. Do not treat the target as disposable until you have checked it.
Verify that the target now contains files and that the source remains present:
$ test -d "$HUGO_TARGET" && echo "target exists"
$ find "$HUGO_TARGET" -type f | head
$ test -f "$JEKYLL_ROOT/_posts/2026-09-20-first-post.md" && echo "source still exists"
target exists
/srv/sites/example-hugo-import/config/_default/hugo.yaml
/srv/sites/example-hugo-import/content/posts/first-post.md
source still exists
File names and generated configuration can vary with the source site. The important checks are that the target exists, contains the imported files, and the original Jekyll tree has not been altered.
5. Review before building or replacing anything
Inspect the target as an ordinary directory:
$ find "$HUGO_TARGET" -maxdepth 2 -type f -print | sort
$ git -C "$HUGO_TARGET" status --short 2>/dev/null || true
$ hugo --source "$HUGO_TARGET" --destination "$HUGO_TARGET/public" --quiet
$ test -f "$HUGO_TARGET/public/index.html" && echo "site build produced index.html"
site build produced index.html
The last command is a separate Hugo build and may expose missing themes, templates or configuration that the import itself cannot diagnose. Review the generated HTML and content before copying it into a live site. The destination in this example is inside the imported project and is not a service directory.
If the review is wrong, preserve the Jekyll source and discard or rename only the new target after checking its path carefully. Never use --force as a repair mechanism for a bad import: it permits writing into a target that already contains files and can obscure which files came from which attempt.
6. Import into a non-empty target only deliberately
The normal command protects a non-empty target. If you have backed up that target and have decided that an additive import is what you need, pass the documented flag explicitly:
$ cp -a "$HUGO_TARGET" "${HUGO_TARGET}.before-jekyll-import"
$ hugo import jekyll --force "$JEKYLL_ROOT" "$HUGO_TARGET"
Import Jekyll from: /srv/sites/example-jekyll to: /srv/sites/example-hugo-import
This is the destructive boundary in the workflow. The backup command changes state and consumes disk space; check that the backup exists before continuing:
$ test -d "${HUGO_TARGET}.before-jekyll-import" && echo "backup exists"
backup exists
If the forced import is unsuitable, stop using the target and restore from that backup only after verifying the two paths. Restoration can overwrite newer work, so do not automate it blindly. The Jekyll source is your other recovery copy.
Common failure checks
- Two paths missing: check the command shape and pass both the Jekyll root and target. A file inside the source is not a substitute for the root directory.
- Empty or wrong source: check for
_postsor_drafts. The installed command rejects a root containing neither. - Target is non-empty: use a new target for a first attempt. Reserve
--forcefor a reviewed, backed-up merge into an existing directory. - Permission denied: check ownership and permissions with
ls -ld. Prefer fixing the directory ownership or choosing a writable target over running the whole migration as root. - Build fails after import: separate import success from theme and template compatibility. Read the build error, then fix the generated project while keeping the source tree untouched.
Done means
- The installed Hugo version and command path were recorded.
- The Jekyll root contains the posts or drafts intended for migration.
- The first import used a clean target and completed with exit status 0.
- The target contains imported files, the source remains intact, and a test build was reviewed.
--forcewas used only with an explicit backup, or not used at all.