Migrate WordPress to Eleventy with Colocated Images

I have a few old self-hosted WordPress sites that need to be migrated off a VPS. I’m still happy with the content and don’t want to just dump them, but I also don’t want to have to keep managing a WordPress installation for what are now static sites.

The core requirements for my migration are:

  1. Markdown support
    No more fussing with a database or headless CMS. I need flatfile.
  2. URLs to match WordPress
    Some of my posts seem to get hits still and perform quite well, I don’t want to mess with the SEO or anyone’s bookmarks.
  3. Colocated images
    One thing that drives me batty about a lot of these frameworks is the poor support for colocated images with posts. Why is this such a massive issue?
  4. Off the shelf themes, preferably TailwindCSS because that’s what I’m using predominantly at the moment, but I’ll take anything nicely structured and easy to manage and tweak.

I’ve been using Next.js for most of my other projects, but those have more complexity, this is just a blog. NextJS feels like overkill.

Zola seemed like a good idea at first glance, but they don’t support permalink customisations, and the structure is quite restrictive. The themes available are also thin on the ground and mostly unsuitable for what I wanted.

Searches suggested Eleventy, although there are several complaints about the challenges of colocation of images, usability of documentation, amongst other things. I have taken a look at it before as a framework for my other sites, but as others noted, the documentation isn’t as approachable as with other frameworks.

That said, a starter is worth a shot and there is this helpful guide by Scott Dawson for migrating WordPress to Eleventy and deploying to Netlify, which is exactly what I intended to do.

And thus, we press on. Or unWordPress on.

Step 1. Export WordPress

Use the built-in WordPress export tool to dump the entire site to xml.

Step 2. Convert WordPress export to markdown

Thank goodness for tools like wordpress-export-to-markdown.

Does exactly what it says on the tin. I used the options to create year, month, and individual post directories, save the attachments and scrape for external images.

Step 3. Set up Eleventy

The guide by Scott Dawson recommended a different starter project, but I found vredeburg which incorporates TailwindCSS and looks almost the way I want for a simple blog.

Clone it.

The theme works as is out of the box, but you can’t quite just drop in your converted posts. Will get to that shortly.

Still, you can move the converted posts in now. Copy the converted output/posts/ into src/posts/. The conversion also puts pages in date folders but they don’t need to be in that structure in Eleventy, so copy the leaf folders in output/pages/ into src/.

Site configuration

  • Update _site/author.json and _site/site.js with your own details.
  • Update menu.json with your intended navigation items.
  • Update tailwind.config.js to suit your tailwind theming preferences.
  • Update the images in src/assets/img.

Easy enough.

Step 4. Image colocation support

This is the where the bulk of the work needs to occur.

  1. Install the following packages:
npm i -D markdown-it-modify-token
  1. Update eleventy.config.jswith (comments inline):

This allows you to colocate your images and makes pointing them to the right place in dist build’s problem and not yours.

You can keep using ![]() syntax and it will ‘just work’.

In the post-grid.njk partial or wherever you put your post loop, you can also modify the cover image tag to use the colocatedImagePath filter:

<img src="{{ post.data.coverImage | colocatedImagePath(post.filePathStem) }}" alt="{{ post.data.title }}" />

Step 5. Update markdown files

There are a few changes you will need to make to your markdown, sorry.

  1. If you use WordPress galleries, you will have to manually convert the shortcodes to markdown image syntax and map the asset IDs to the colocated files.

  2. If WordPress uses a resized image instead of the original, you will need to remove the -widthxheight portion of the filename. You can use a regex here.

  3. You will need to search for any other shortcodes to replace them with something else.

  4. None of my code or pre content seems to have been converted into markdown. Those had to be found and wrapped with the necessary backticks.

  5. I had some draft posts, apparently. These get converted by wordpress-export-to-markdown without a clear indication that they’re draft. I didn’t want to publish them, but they were fleshed out enough that I didn’t want to delete them either. For each of these I added draft: true to the frontmatter, snaffled eleventy.config.drafts.js from eleventy-base-blog and updated eleventy.config.js to use it:

    const pluginDrafts = require("./eleventy.config.drafts.js"); module.exports = function(eleventyConfig) { ... eleventyConfig.addPlugin(pluginDrafts); ... };

Step 6. Implement categories and tags

Eleventy uses ‘tags’ to represent the internal collections. Apparently, it’s not taxonomy the way categories and tags
are used in WordPress. To keep these separate, you can create a new collection for them.

Replace frontmatter references to tags with hashtags to give them a separate namespace. You can use a regex here.

The following gist contains the collection definitions, their paged equivalents, as well as sample index and individual
pages for each. These have been based on the existing files for posts and tags from the starter.

Step 7. Deployment to Netlify

This was pretty painless. Hook up Netlify to the repo and it’ll trigger a build. The netlify.toml that came with the
starter probably helped, but it really just took care of itself.

After that, I just had to update the DNS to point to the Netlify deployment, hook up the Lets Encrypt SSL cert through
Netlify and off it went.

Additional changes

  • I replaced the search box in the header with a link to /search because I couldn’t get the form to pass the query
    param or it otherwise lost it once it reached search page.

  • Modified the footer to make the year dynamic using this helper in eleventy.config.js:

    config.addHandlebarsHelper('currentYear', () => { return DateTime.format(new Date, 'yyyy'); });
  • Some of my images are smaller than the post width, so I added the following blanket fix to assets/css/main.css:

    .prose img { @apply w-full mx-auto; }

If your blog is still active, there’s some other good stuff you can snaffle
from eleventy-base-blog like RSS feed support. So it’s worth poking
around that starter to see else you might find useful.


This method does none of the optimisation things that eleventy-img does
because build time took forever, I’m impatient, and I honestly don’t care so much about this right this minute. My
priority is to get the sites off before my VPS expires. If image optimisation is a priority, you should be able to
integrate eleventy-img possibly with eleventy.config.images.js
from eleventy-base-blog as a foundation but I don’t know how compatible
that will be with the above, YMMV.


Published September 23, 2023