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.
- I've never really liked the wordpress interface. It's quite clunky and, certainly recently, I've noticed how slow it has become. This was actively putting me off using it, so I started using markdown instead of the web interface.
- However, wordpress' markdown importer hasn't yet managed to import any of these posts successfully. Last time I spent almost as much time fixing up the results as I did writing the original post.
- As part of my desire to keen things simple, I'd recently started paying for a wordpress.com account. And yet, I genuinely could not find a theme that I liked, with any vaguely pleasant theme being gated behind the next level of subscription. "Just host it yourself and use one of the many excellent free themes" - yes, but that seemed even more work. And using the wordpress interface to make the themes better was a very frustrating experience.
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.
- From Wordpress, go tools/export. Select
Export all
, and you get a zip file containing your content. - From the command line, convert the contents of the download.
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:
- Some images were missing.
- In some cases links to images pointed to the web-site, rather than the local copies.
- Some code formatting went a bit wonky.
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:
- By default, the eleventy-base-blog doesn't have animated images enabled; it needs to be explicitly enabled. Documentation here.
- Once I did this, I started getting 'image too large' errors, which was very confusing. In the end I found this bug report which explained the problem; the libheic Eleventy uses doesn't support animated images, and instead tries to unroll them into an image series - which in my case resulted in a 'image too large' error.
- The solution was to simply remove the AVIF export option.
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.
- Install the npm package:
npm install --save-dev markdown-it-image-figures
. - Activated it
eleventy.config.js
:
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:
npm install markdown-it-attrs
- Putting this into
eleventy.config.json
:
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
- Set up a repo.
- Add utterances from the github marketplace.
- Run through the utterances wizard/form thing, filling in your repository specifics.
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:
- Insert the post title as a link.
- Insert the post date.
- For each tag in the post, extract the text and turn it into a link, and insert that as a tag.
- Grab the post contents and pass it to a filter called
excerpt
to extract summary, and insert that.
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.
- ← Previous
Event Audio for Godot - Next →
Emacs, snakes and camels