Skip to main content
Site Icon Mysterious Pixel

Moving from Wordpress to Eleventy

Resisting Sirens

Over the weekend I moved my personal blog from wordpress.com, to Eleventy hosted on Cloudflare pages.

My motivation for choosing wordpress 10 years ago was largely born out of a desire for 'enforced simplicity'. Like any coder, particularly coders who use Emacs, I am constantly battling the voices that tell me to 'build it yourself, it will be easy and you can make something so beautiful.' This usually ends with me spending so much time building the perfect system for making The Thing, that I forget to make The Thing.

In using Wordpress I was choosing to strap myself to myself to a mast and filling my ears with wax while the sirens sang their song.

So, when part of my brain decided to move away from Wordpress I was very suspicious of my motivations - like cats, sirens can pounce at any time - but after some reflection I was convinced that my reasons were sound.

Finally, the recent wordpress.com drama was the final kick I needed to investigate an alternative.

I settled on Eleventy only because I'd seen others recommending it recently over Hugo and Jekyll, and it seemed fairly simple. Implementing it took a little longer than I expected; partly because Eleventy's documentation is a little Spartan in places, and for a web-dev non-native a number of things were not obvious to me. I documented the process, which I'm sharing below in case it's helpful for others.


Process

Inital setup

Cloudflare's tutorial got me started. Since I was intending on storing things on GitHub, rather than cloning the repo locally as they suggest I got Github to do it for me by clicking on the 'Use this template' button on the official base blog repo.

After that I installed Eleventy, and cloned my new repo.

# Install Eleventy
npm install -g @11ty/eleventy

# Clone the new repo
git clone https://github.com/<my-blog-repo>

# Move to the new location
cd <my-blog-repo>

# Install the dependencies
npm install

# Start the local web server
npx @11ty/eleventy --serve

This gave me a local URL which, when thrown in a browser, showed the running blog. Celebrate!

Exporting from Wordpress

This bit was remarkably easy, thanks to the excellent wordpress-export-to-markdown package.

npx wordpress-export-to-markdown

This spits out an output folder, the contents of which I put directly into my repo-folder/contents/blog folder.

This did 95% of the work, but the result needed a bit of fixing up:

But overall, this saved a lot of time.

Additional features

The resulting site was fine, but needed some tidying up.

Animated GIFs

It took me ages to find out why GIFs weren't animating

Ultimately, this is what I needed to change in eleventy.config.js:

eleventyConfig.addPlugin(eleventyImageTransformPlugin, {
  ...
  // formats: ["avif", "webp", "auto"] <- REMOVE AVIF!
  formats: ["webp", "auto"],

  sharpOptions: {
	animated: true, //<-- ADD THIS!
  },
  ...

Short explanation:

Image captions

For some images I wanted captions of some sort, ideally centered under the image. This post put me onto markdown-it-image-figures package, which inserts the tool-tips as captions.

Using this involved fiddling with the markdown parser, described here.


import markdownIt from "markdown-it";
import markdownItImageFigure from "markdown-it-image-figures";

export default async function(eleventyConfig) {
  ...

  // Default options for markdownIt
  let options = {
      html: true,
      breaks: true,
      linkify: true,
  };

  eleventyConfig.setLibrary("md", markdownIt(options));

  // Tell eleventy to add markdownItImageFigure when it loads
  eleventyConfig.amendLibrary("md", (mdLib) => mdLib.use(markdownItImageFigure))
  ...
}

Side-by-side images

In some cases I wanted multiple images to be next to each other, and there's not an obvious way to do this. This suggestion on stackoverflow provided a (to me) non-obvious solution - use GitHub flavoured tables.

Image 1       | Image 2
:------------:|:-------------:
![](img1.png) | ![](img2.png)

This gave me exactly what I needed - and also gave an alternative way of captioning images.

Image sizes

Markdown doesn't have a standard way to manipulate image sizes, and by default some of the images I was using were using the full page width. I found several ways of approaching this.

Good defaults
The easiest thing is to set the basic image size throughout the site to something reasonable. I was getting some absurd sizes, so I put this into index.css

img {
  max-width: 100%;
}

Custom attributes
The markdown-it-attrs package provides a convenient way to push HTML tags from markdown, and can be used to control image widths - there's a great post on this here.

To use it, it's a similar process to adding the image captioning plugin above:


import markdownIt from "markdown-it";
import markdownItAttrs from "markdown-it-attrs";

export default async function(eleventyConfig) {
  ...
  let options = {
      html: true,
      breaks: true,
      linkify: true,
  };

  eleventyConfig.setLibrary("md", markdownIt(options));
  
  // Add the attrs plugin
  eleventyConfig.amendLibrary("md", (mdLib) => mdLib.use(markdownItAttrs))

With this done, you can modify image tags directly in markdown like so, using eleventy:widths:

Get markdown to show image with a width of 300px:
[](img.png){eleventy:widths="300"}` 

Custom shortcode
Finally, for very precise control you can write a short-code that emits the exact HTML you'd like. Here's a simple example, added to eleventy.config.js

export default async function(eleventyConfig) {
  ...
  eleventyConfig.addShortcode("image", async function (src, alt, width) {
    return `<img style="max-width:${width}px;height:auto;width:auto" src="${src}" alt="${alt}"/>`;
  });
  ...
}

And then in markdown:

Here's an image:
{% image, "./img.png", "Some image", 300 %} 

In the end, I mostly ended up using a good default image size and tables.

Comments

I'm sure if I need it, but I always liked that wordpress had an optional comments feature - sometimes people like to ask questions. I wasn't expecting Eleventy to have comments, at least without descending into some sort of third-party ad-supported hell; but then found this post about 'utterances'. Utterances cleverly re-appropriates Github issues for comment threading, and is extremely easy to add, so I've added it for now.

The instructions are very easy to follow

At the end you get a little chunk of html code, which I placed this in my base.njk file, just before my footer tag.

Excerpts and tags

For my post list, I wanted to show the tags of each post, and a short excerpt. This involved jumping into nunjuck templating, which looks weird but is mostly macros. My postlist.njk ended up looking like this:


{%- css %}.postlist { counter-reset: start-from {{ (postslistCounter or postslist.length) + 1 }} }{% endcss %}
<ol reversed class="postlist">
  {% for post in postslist | reverse %}
  <li class="postlist-item{% if post.url == url %} postlist-item-active{% endif %}">
    <a href="{{ post.url }}" class="postlist-link">{% if post.data.title %}{{ post.data.title }}{% else %}<code>{{ post.url }}</code>{% endif %}</a>
    <div class="postlist-details">
      <div>
        <time class="postlist-date" datetime="{{ post.date | htmlDateString }}">{{ post.date | readableDate("LLLL yyyy") }}</time>
      </div>
      {% if post.data.tags %}
      <div class="postlist-tags">
        {% for tag in post.data.tags | filterTagList %}
        {%- set tagUrl %}/tags/{{ tag | slugify }}/{% endset %}
        <a href="{{ tagUrl }}" class="postlist-tag">{{ tag }}</a>{%- if not loop.last %}, {% endif %}
        {% endif %}
        {% endfor %}
      </div>
      {% endif %}
    </div>
    <div class="postlist-excerpt"> {{post.templateContent | excerpt}}
    </div>
  </li>
  {% endfor %}
</ol>

This is basically an exercise in iteration, function calling and text substitution. So, for each post:

That 'excerpt' filter is then just a bit of javascript code in the eleventy.config.js

eleventyConfig.addFilter("excerpt", function(content) {
    const length = 30;

    // Remove HTML tags
    let text = content.replace(/<\/?[^>]+(>|$)/g, "");
    // Split into words
    let words = text.split(/\s+/).slice(0, length);

    // Join words, up to a maximum of 'length'
    return words.join(" ") + (words.length >= length ? "..." : "");
});

This just strips out html tags and emits the first 30 words. It's a crap regex at the moment, which should be replaced with something nicer - but for now it works.

Conclusion

I did a bunch of other minor things - a footer with social links, fiddled with code formatting so it looked nicer, various style fixes - but these didn't take long. Overall it only took a couple of days, and so far I'm happy with the results.

However, as expected I now have a strong urge to drop everything I'm doing and tinker - after all with a few easy tweaks I could make it perfect - so excuse me while I plug my ears with yet more wax.