Migrating WordPress To Eleventy
After 11 years on WordPress, I moved this blog to Eleventy (11ty) and I'm very happy with the results. These are my notes on the background and process for anyone going through a similar move.
Why the change?
My blog isn't too big - about 650 posts and 410 images since 2002 - but I've enjoyed the process of keeping it going and keeping everything intact as I've migrated through several blogging tools. (Cool URIs don't change and all that.)
After about 11 years on WordPress I was ready for a change for several reasons:
- Keeping up with WordPress and plugin updates was time consuming and I never finished my ideas for making it more automatic
- I would occasionally have security issues where spam links would be injected randomly across posts
- The worst example was when it was really hacked (essentially taken down), but luckily after I restored the site the traffic returned to normal
- Plugins like Yoast SEO were powerful and quickly added capabilities to the site, but the constant nagging to upgrade to premium was really a drag
- More an issue with my web host rather than WordPress, but I wanted to finally implement SSL and control things like caching myself; removing the need for a MySQL database also helps enable that move
On the positive side:
- It was fun learning about WordPress and its ecosystem, developing my own plugins, tweaking my themes just right
- The ability to post from anywhere was nice (although in practice it didn't use it that way very often)
- Learning WP led me to use it for some site projects for sports clubs I was involved in with my kids
- My WordPress powered by Docker project was fun to build and helped a lot with local testing
Static site generator
Okay so moving off WordPress - but what to switch to? I wanted to go back to a static site generator to avoid the need for a database and have it be more resilient to any hacking (after all it's just static HTML files). But which one to choose? The Jamstack site has a nice directory with 369 different tools as of today: Static Site Generators - Top Open Source SSGs.
For the new system I wanted something that was popular, fast enough, and had simple blog themes I could use to get started.
At first I gave Hugo a try because it's fast and popular and would give me a chance to play with the Go language. I couldn't find a good simple blog theme to start with and struggled to understand how it worked.
Next I tried Eleventy (also abbreviated as 11ty) and it really clicked for me. I'm no JavaScript expert, but the way the build process and layout worked made it easier for me to understand (even if just at the surface level). What really hooked me was finding the 11ty/eleventy-base-blog starter project; I cloned this and I was off and running with the migration.
From the start I've kept everything in a GitHub repo, so that's essentially my new blog database :) It's been great to experiment in branches (especially those that didn't pan out), and to keep track of every step in case I need to revert any changes.
Migration notes
Part of the rationale for choosing something popular was to benefit from those that have gone before us. Migrating WordPress to Eleventy fits that model; these are the posts I benefitted the most from (bits and pieces from each):
- Migrating from WordPress to Eleventy — Eleventy
- A Complete Guide to Building a Blog with Eleventy
- Taking WordPress to Eleventy - Josh Can Help
- Migrating from WordPress to 11ty | DeepakNess
The core of the migration process was to create the new blog project (using the starter project), then run the official 11ty/eleventy-import) to import all the WordPress posts and images. This brought all the content over in Markdown format which was exactly what I wanted and is the default for Eleventy blogs. This part was pretty smooth; the rest of the time was spent wrestling with a few key areas:
Post dates
A key migration requirement was to keep blog post links the same as before. I was okay if images had moved around, but I wanted to ensure the original links didn't change (except for the eventual http → https redirect).
This was my first step into trying to understand the Eleventy template and build process and I definitely struggled. The import script correctly brought over blog posts from WordPress and created the "frontmatter" needed for Eleventy. Here's an example:
---
title: How to Convert Word DOC to DOCX Format
authors:
- name: Brian Cantoni
url: http://www.cantoni.org/author/bcantoni
avatarUrl: >-
https://secure.gravatar.com/avatar/d2a3e9017efa2d8f0212315e590dd6a7?s=96&d=mm&r=g
date: 2020-01-16T05:12:41.000Z
metadata:
categories:
- Tools
- Web
uuid: 11ty/import::wordpress::http://www.cantoni.org/?p=1081
type: wordpress
url: http://www.cantoni.org/2020/01/15/how-to-convert-word-doc-to-docx-format
tags:
- tools
- web
---
My blog uses the path convention /yyyy/mm/dd/post and you can see this post was originally published on /2020/01/15/. However, the date
from the import uses the UTC time which happens to be the next day. Publishing under /2020/01/16/ would be wrong, so I spent a regrettable amount of time trying to make build script changes to correctly handle these (e.g. calculate back to my Pacific time), but also work correctly new posts written after migration. To be fair to myself I was also learning the whole Eleventy system at the same time :)
In the end I decided to simplify and just rely on the file path and use it as the published date. Much simpler! I created a post_permalink
filter (eleventy.config.js):
// determine permalink from the file path in yyyy/mm/dd/slug/index.md format
// this replaces using 'date' in the frontmatter which was problematic after WP migration
eleventyConfig.addFilter("post_permalink", (page) => {
const match = page.inputPath.match(
/\/(\d{4})\/(\d{2})\/(\d{2})\/([^\/]+)\/index\.md$/,
);
if (match) {
const [, year, month, day, slug] = match;
return `${year}/${month}/${day}/${slug}/`;
}
throw new Error(
`Unexpected inputPath format for post_permalink: ${page.inputPath}`,
);
});
And then used it by default for all blog posts (blog.11tydata.js):
export default {
tags: ["posts"],
layout: "layouts/post.njk",
permalink: "2025/07/12/migrating-wordpress-to-eleventy/",
};
Now this old link http://www.cantoni.org/2020/01/15/how-to-convert-word-doc-to-docx-format redirects to https://www.cantoni.org/2020/01/15/how-to-convert-word-doc-to-docx-format/ and it's working as I planned.
Images
Image handling was another tricky area. The Eleventy image plugin is pretty powerful and handles things like automatic height/width attributes, serving images in different modern and legacy formats, and outputting multiple sizes.
I was fine with all of that, but couldn't quite get it to work correctly with my existing images, especially referencing them from these imported blog posts. With the plugin enabled, I couldn't do simple things like referencing "/images/file.png". I also couldn't find or get any help from the community so I decided once again to "do the simple thing" and removed that plugin. Now everything works and as a bonus the images are mostly in their original locations under /images, so any old search results should still work.
I did write my own build script to automatically add height and width attributes; I'll write up more on this later.
Search
The built-in search for WordPress was really nice - being a PHP-based site really helps with dynamic content like this. The new static site doesn't have that obviously, but luckily there are a lot of static site search tools.
I went with PageFind which during build time reads all content and creates a search index JSON file. Then a little bit of JavaScript runs when someone does a search, and the results look pretty good. Future improvement: only load that JavaScript when someone clicks to search (rather than every page load) and make it look nicer.
Hat tip to Robb Knight's article which made this super easy: Using PageFind with Eleventy for Search!
Excerpts
Excerpts needed a little extra help to accommodate the WordPress convention of manually using a <!-- more -->
tag to separate the excerpt from the rest of the post. A quick parsing option made this easy. Still todo: make this more automatic so I don't have to go back and mark all the old posts.
// enable manual excerpts marked in the content with <!-- more -->
// https://www.11ty.dev/docs/data-frontmatter-customize/#example-parse-excerpts-from-content
eleventyConfig.setFrontMatterParsingOptions({
excerpt: true,
excerpt_separator: "<!-- more -->",
});
Other files
A final piece was pretty straightforward, copying over a bunch of files managed outside of WordPress, including:
- images under /images
- downloadable files under /files
- robots.txt
- .htaccess (redirects and cache settings)
- individual .html files
It's also nice now that 100% of the blog files are all in GitHub, so they're all managed in one place.
Comments
This step was simple because I've long disabled comments on the old site. (In fact it used a distinct WordPress plugin just to disable the comments.) There are a lot of different ways to support comments with a static site, but it's not something I need.
Authoring workflow
In my WordPress configuration, I was writing everything in Markdown format (I had disabled the Gutenberg editor). I also managed images through WordPress which was pretty nice. (Although to avoid spam issues I had to manually change permissions on the "upload" directory each time.)
In the new configuration, this is my workflow, aided by a "drafts" Python script I wrote:
- Work on new blog post in Markdown in the ./drafts folder
- Add and reference images as needed
- When ready to publish, run
make draft
which handles several steps- move blog post to correct /yyyy/mm/dd/ folder location
- move images to /images and change references
- suggest and add tags
- suggest and finalize post title and slug
- Test locally with
make start
- (Optional) create and add to a branch if not ready to publish immediately
- Add to GitHub and push
Deployment
For deployment I wanted something that could run locally (which Eleventy already supports) and also handle preview and production pushes automatically. Here's what I came up with:
Local: make start
(which just runs the Eleventy command npm run start
)
Preview: Pull request builds automatically trigger Netlify preview builds. This is really handy especially for anything tricky that I want to double-check before pushing live. I'm only on the free plan, so these are automatically deleted after 30 days, but none are needed that long.
Production: Netlify also can push to production, but I wanted to go more DIY and host this myself. (It's on a small DigitalOcean droplet running the Apache web server.) I created a GitHub Action workflow to do a build and push (rsync) with any push to the main
branch. I can also trigger this manually if needed. I learned how to use restricted SSH keys for this (meaning: an SSH key that can only do the rsync to the specific destination) and it worked well.
I could potentially move this to a different hosting solution in the future, but for now I like combing through the Apache error and access logs directly to keep an eye out for errors.
Performance
I'll do some more work here on performance and accessibility, but it's nice to start out with "4 hundreds" on the Lighthouse performance test.
It definitely helps to have a minimal site without much CSS or JavaScript bundled into each page. I brought over some of my old cache settings but will probably tweak that a bit more.
- ← Previous
Proper Sitemap Update Dates for Eleventy