Migrating to Dune
Dune ships four migration commands for importing content from other platforms. They read your existing content, convert it to Dune's folder-based structure, and write the result into your site's content/ directory. Your original files are never modified.
dune migrate:from-grav <src>
dune migrate:from-wordpress <src>
dune migrate:from-markdown <src>
dune migrate:from-hugo <src>
Common options
All four commands accept the same set of options:
| Option | Default | Description |
|---|---|---|
--out <dir> |
content/ |
Write imported content to a different directory |
--dry-run |
off | Print what would be created without writing any files |
--verbose |
off | Print each file as it is processed |
--root <dir> |
. |
Dune site root (if running from outside the site directory) |
Always do a dry run first and review the output before writing files to your live content directory.
Migrating from Grav
Grav uses nearly the same folder + frontmatter structure as Dune, so this migration is the simplest. Content files, numeric folder prefixes, co-located media, and taxonomy are all preserved as-is. Grav-specific fields (process, access, sitemap) are silently dropped.
dune migrate:from-grav /path/to/grav-site
Point at either the full Grav installation directory or just its user/pages/ subdirectory — the command auto-detects which you provided.
What is preserved
| Grav field | Dune field | Notes |
|---|---|---|
title |
title |
Direct |
date |
date |
Direct |
published |
published |
Direct |
visible |
visible |
Direct |
routable |
routable |
Direct |
slug |
slug |
Direct |
template |
template |
Direct |
taxonomy |
taxonomy |
Direct |
metadata |
metadata |
Direct |
routes.aliases |
routes.aliases |
Direct |
header.image |
image |
Promoted to top level |
header.author |
custom.author |
Moved under custom |
| Media files | Media files | Copied unchanged |
What is dropped
process(Grav rendering flags)access(Grav ACL)sitemap(Grav sitemap config — Dune auto-generates sitemaps)cache_control,never_cache(replaced by Dune's cache config)
Migrating from WordPress
Export your WordPress site via Tools → Export and choose "All content". This produces a WXR file (WordPress eXtended RSS, an XML format).
dune migrate:from-wordpress /path/to/export.xml
What is imported
Posts are written into content/01.blog/ with a post.md template, sorted by publish date and numbered sequentially. A content/01.blog/default.md listing page is created automatically if one does not exist.
Pages are written as top-level pages using the default.md template.
| WordPress field | Dune field |
|---|---|
post_title |
title |
post_name (slug) |
folder name |
post_date |
date |
post_status |
published (publish → true) |
| Category terms | taxonomy.category |
| Tag terms | taxonomy.tag |
content:encoded |
Markdown body (HTML passthrough) |
Content format
WordPress content is HTML. Dune's markdown renderer passes raw HTML through unchanged, so your content will display correctly without conversion. If you want clean markdown, use a tool like Pandoc on the individual files after migration.
Not imported
- Comments (Dune has its own comment system — migrate manually if needed)
- Users (create admin users via
dune newor the admin panel) - Attachments / media (download your
wp-content/uploads/separately and place files in the relevantcontent/page folders or a sharedstatic/directory) - Custom post types beyond
postandpage - Plugin data (ACF fields, WooCommerce products, etc.)
Migrating from a markdown folder
Import any flat or nested directory of .md files. This works for content exported from Notion, Obsidian, Bear, or any other tool that produces standard markdown files.
dune migrate:from-markdown /path/to/markdown-folder
What happens
- Each
.mdfile becomes a numbered folder (e.g.01.my-post/default.md) - Existing YAML frontmatter is preserved as-is
- Files with no
titleget a title derived from the filename - Files with no
publishedfield default topublished: true - All-uppercase files (
README.md,CHANGELOG.md) are skipped - Subdirectories are processed recursively, producing nested Dune folders
- Non-markdown files (images, PDFs) are copied alongside their markdown source
Tips
- Run with
--dry-run --verbosefirst to review the folder mapping before committing - If your files have frontmatter, Dune will use it as-is — clean it up before or after migration
- Files without frontmatter get minimal Dune frontmatter auto-generated (title + published)
Migrating from Hugo
Point at the root of your Hugo site. The command reads from content/ and copies static/ assets.
dune migrate:from-hugo /path/to/hugo-site
Frontmatter mapping
Hugo supports YAML (---), TOML (+++), and JSON ({}) frontmatter — all three are handled.
| Hugo field | Dune field | Notes |
|---|---|---|
title |
title |
Direct |
date / publishDate |
date |
ISO date, time part stripped |
draft: true |
published: false |
Inverted |
slug |
slug |
Direct |
tags |
taxonomy.tag |
|
categories |
taxonomy.category |
|
aliases |
routes.aliases |
|
description / summary |
metadata.description |
|
author / authors |
custom.author |
|
weight |
order |
Used for nav ordering |
Section index pages
Hugo's _index.md section index files are renamed to default.md — the Dune equivalent.
Static assets
Files in Hugo's static/ directory are copied to static/ in your Dune site root (creating it if needed). Reference them in content as /filename.ext.
Not imported
- Hugo layouts and partials (Dune uses its own theme system — see Themes)
- Hugo shortcodes (convert to plain HTML or Dune's block system manually)
- Hugo data files (
data/directory) - Hugo
config.toml/config.yaml(recreate your settings in Dune'ssite.yaml)
After migration
Once the import is complete:
- Start the dev server to preview your content:
dune dev - Check for issues:
dune content:checkreports missing titles, broken internal links, and missing templates - Assign templates: Dune looks for a theme template matching each page's filename (e.g.
post.md→post.tsx). Add any missing templates to your theme or update thetemplate:frontmatter field - Review taxonomy: Run
dune content:listto see all pages and their published state - Handle media: For WordPress imports, download your media files and place them in the appropriate
content/page folders
Example workflow
# 1. Dry run to see what will be created
dune migrate:from-hugo ~/old-blog --dry-run --verbose
# 2. Run the actual migration
dune migrate:from-hugo ~/old-blog
# 3. Check for problems
dune content:check
# 4. Start the dev server and review
dune dev
Upgrading Dune
v0.19 → v0.20: trailing-slash URLs for page-folder pages
v0.20 changes the canonical URL form for folder-based pages. A page at 02.blog/01.my-post/default.md now serves at /blog/my-post/ (trailing slash) instead of /blog/my-post. Flat-file pages (articles/my-article.md) are unaffected — they keep the no-slash form.
Automatic (nothing to do): Dune issues a 301 redirect when a visitor arrives at the wrong slash form. Existing bookmarks, search engine entries, and external links all continue to resolve. Co-located media (./photo.jpg) and relative cross-page links (./related/) now resolve correctly in browsers without server-side rewriting.
Manual audit recommended:
Hardcoded links in content files — search for root-relative links that point to folder pages:
grep -r '](/[a-z]' content/Change
/about→/about/,/blog/my-post→/blog/my-post/, etc. The 301s mean this is optional, but eliminating the redirect hop is cleaner.Hardcoded hrefs in theme templates — any
href="/contact"orhref="/blog"pointing to folder pages should gain a trailing slash. Dynamic links usingpage.routeor nav items from the content index are already correct.site.yamlredirects — if yourredirects:section targets folder pages, update the target URLs:# Before redirects: /old-path: /new-section # After redirects: /old-path: /new-section/Theme
homeRoute— if your layout derives the home route fromsite.home, add the trailing slash:// Before const homeRoute = site?.home ? `/${site.home}` : "/"; // After const homeRoute = site?.home ? `/${site.home}/` : "/";Without this, the active-state highlight on the home nav item breaks.
isActiveRoutein themes — if your layout checks whether the current route is inside a section, update the prefix check to handle routes that already end with/:// Before return currentRoute === route || currentRoute.startsWith(route + "/"); // After const prefix = route.endsWith("/") ? route : route + "/"; return currentRoute === route || currentRoute.startsWith(prefix);Without this, a nav item for
/blog/would not show as active when browsing a child post at/blog/my-post/.