Skip to main content
Brian Cantoni

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):

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:

  1. Work on new blog post in Markdown in the ./drafts folder
  2. Add and reference images as needed
  3. 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
  4. Test locally with make start
  5. (Optional) create and add to a branch if not ready to publish immediately
  6. 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.

4 100% scores on Lighthouse

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.