Frustrated with 11ty

I’ve been flailing off and on over the past few weeks months to migrate my blog from middleman to 11ty. Every time I opened my code editor to write a post… I got side-tracked into an endless hacking session. I had already written a migration script to extract my content, but it took me a while to make a choice as to what to try and make the changes in.1 Here are some of my scars from my battle with the JS-powered possum… while I pick myself up for another bout.

My mistakes are many…

I had bad YAML.

Even though how much I write about it, I was using bad YAML for tags. 🤦

The following is valid but wrong YAML:

---
tags: foo, bar, baz
---

That resolves to the string {tags: "foo, bar, baz"} (what YAML refers to as a “scalar” and not a collection.) It seems that whatever library/method Middleman was using to parse YAML frontmatter was more accepting than the actual spec.

The power of having a repeatable script is that such mistakes are relatively fixable. I just added some yucky-looking ruby to my content migration script to format that YAML:

# clean up tag YAML format
# Since the offending code will all be in one line, find that line
tag_string = content.lines.select { |l| l.match(/^tags: /) }[0]
# if that line exists, grab all the tags out of it.
tags       = tag_string.scan(/[\w-]+/).reject { |t| t == 'tags' } if tag_string
# if those tags exist, rewrite the original line with the correct syntax
content.gsub!(tag_string, "tags:\n- #{tags.join("\n- ")}\n") if tags

This changes the above to a valid YAML collection:

---
tags:
- foo
- bar
- baz
---

Now I have to go around and fix a bunch of Drafts actions. 🤦‍♂️

Part of my goal was not run a “long running branch” where the content of the 11ty fork would lag behind the middleman site. I figured that would slow down my writing even more.

Part of the repeatable migration script was nuking the folder, copying over the 11ty files from a /tmp folder, and migrating the content from the middleman site, voltron-ing them all into a “final folder.” This allowed me to keep writing content in middleman or work in 11ty, and nothing would get lost. I’d run npx @11ty/eleventy --serve in the “final” folder, and be off to work.

However, I got confused often by the two folders.

If I made a change in the “final” folder… git wouldn’t pick it up because I had .gitignored it. I’d have to remember to copy any local changes out of the “final” folder into the tmp folder. If I worked in the tmp folder, git would see it, but it wouldn’t affect what my browser was rendering… because 11ty was watching the final folder. 🤦‍♂️

While bemoaning this to my friend Seth I realized that I could use GNU Stow to symlink the contents of the tmp folder to the final folder, and then I wouldn’t have the problems anymore: one folder for git to track, all changes updated, yay!

Except 11ty has problems with symlinks, so I had to write this lovely little ruby bit into the migration:

# Use stow to copy over tmp to final
`stow tmp -t evantravers.com`
# Nuke the symlink that needs to be passthroughCopy
`rm -rf evantravers.com/dist`
# Manually copy the files over so that we have CSS/JS
`cp -r ./tmp/dist evantravers.com/dist`

It’s gross. It works. Moving on.

Data doctoring

There were a few minor data adjustments that the migration script had to handle…

Blog post times had to be adjusted: timezones removed (did that in middleman via vim search-and-replace), quotes added around the YAML declaration. No idea why.

# 11ty requires string notation around the date?
content.gsub!(/^date:.*/, "date: \"#{Regexp.last_match(0)}\"")

11ty uses prism.js for syntax highlighting, and it has slightly different names for the languages… so more find/replace.

# prism.js is a picky boi
content.gsub!(/```viml/, '```vim')
content.gsub!(/```fish/, '```')
content.gsub!(/```make/, '```makefile')
content.gsub!(/```conf/, '```apacheconf')
content.gsub!(/```apacheconfig/, '```apacheconf')

As part of the original migration script, I had put in a folder structure to ensure that the SSG I would choose would respect my URLs, but honestly… I’ll probably remove that.

11ty is weird

So those were my mistakes… what about the possum?

Data isn’t coherent at all levels

This really bothered me. Frontmatter data of the current page is made available as a variable with no namespace, while outside the current page it’s nested under data. Quick context:

In my blog, the HTML that drives the list pages (home, calendar, tags) and the header for a single article is the same partial.

<a class="blog__header  u-url" href="<%= article.url %>">
  <% tag = current_article ? "h1" : "h2" %>
  <<%= tag %> class="blog__title  p-name"><%= article.title %></<%= tag %>>
  <time class="blog__date  dt-published" datetime="<%= article.date %>">
    <%= article.date.strftime("%B %d, %Y") %>
  </time>
</a>
<% unless current_article %>
  <%= partial 'article_tags', locals: { article: article }%>
  <%= partial 'article_series', locals: { article: article }%>
<% end %>

If appears on the current_article, it shows as an <h1> and hides some things. This helps me keep the look and feel consistent. All it needs is article passed into it on render:

<%= partial 'article_header', locals: { article: article }%>

In 11ty, it’s not that simple. On its equivalent of current_article, I can’t reach article.date, it’s just date. The data structure on a paginated object (more on that later) is different than in a for loop. As a result, instead of passing in a single object to a partial, I have to pass in every attribute manually:

{%
  render '_article_header',
    title: article.data.title,
    url: article.url,
    date: article.data.date,
    tags: article.data.tags,
    series: article.data.series
%}

There is an issue on github and discussion around this… and it’s entirely possible I’m doing encapsulation wrong… but it’s annoying to me.

Collections are clever, but naive

11ty by default groups your content by the tag metadata into what it calls Collections. This makes building and finding blog posts by tag very simple… unless you want to do something like say… list all your tags.

This simple task is excruciatingly hard because Collections are the only way to group, list, or generate pages in 11ty. By default, it adds an all collection to represent all your content, so you can’t just iterate over Collections to show your tags, even if you aren’t doing anything else with them!

To show your tags you can iterate over all your collections, filter out all and any other custom collection you’ve made, then assign a variable to that Collection to iterate over it:

<h1>All Tags</h1>
<ul>
  {% for tag in collections.filter(c => c[0] != "all" ) %}
    {% assign tagName = tag[0] %}
    <li>
      <a href="/articles/tags/{{ tagName }}"><h5>{{ tagName }}</h5></a>
      {% assign articles = collections[tagName] %}
      <ul>
        {% for article in articles %}
          <li><a href="{{ article.url }}">{{ article.data.title }}</a></li>
        {% endfor %}
      </ul>
    </li>
  {% endfor %}
</ul>

Collections are designed to work together with Pagination… let’s talk about that.

Pagination needs to be split

Pagination does a little bit more than splitting up a Collection into pages, it’s also the only way to generate pages from templates using your data.

Because it is trying to do both things, it is not very flexible for generating content. It can’t handle multidimensional data, or a permalink with more than one variable… say year and month.

I have spent hours trying to build a calendar system in 11ty collections with pagination, and I’m about at the end of my rope. I used a fancy reduce to create an object with years, months, and article objects:

eleventyConfig.addCollection(`articles`, function(collection) {
  return articles(collection)
    .reduce(
      function(map, article) {
        let year  = article.date.getFullYear().toString();
        let month = `${article.date.getMonth() + 1}`.padStart(2, "0")

        if (map[year]) {
          if (map[year][month]) { map[year][month].push(article); }
          else { map[year][month] = [article] }
        }
        else {
          map[year] = {};
          map[year][month] = [article];
        }
        return map;
      },
      {} // initial value
    )
})

This correctly generated a JS object as a Collection… I could print it out in JSON, I just couldn’t paginate over it. I am sure there’s a way, and I’m doing it wrong, but it seems like pagination is designed to iterate over a simple array of resources, writing permalinks based on a single attribute of the object. I couldn’t make it generate the nested permalinks I needed.

I’m not the first to joust with this problem.

* le sigh *

I think that eleventy is a fantastic tool and I’m just the pickiest of customers2… but man is this taking waaaay too long. If you are simply looking for a JS-based tool to build a simple blog or to transform a collection of data/files into web pages, it’s a great tool. It seems like it is just not for me.

I am very excited about the possibility of having the content for my blog live in my Creativity System as the final folder in a writing workflow… thoughts move from ideas to completion all in the same system… I just don’t know what static site generator will best fit my rather ridiculous requirements.

I really don’t want to write react in Gatsby. 🤦‍♂️


  1. I chose 11ty based on some selfish criteria: I didn’t want a SSG that used a compiled language, I wanted a lot of speed… and I didn’t want it tied to popular front-end framework like React or Vue. That left 11ty. 🤷‍♂️ If I was going to work with other people or get paid for it, I’d probably choose Gatsby, but my command-line heart hates the filenames that the File System Route API requires. 

  2. Don’t believe me? Here’s the todo list for migrating my blog: 

    Process

    • [x] Develop Exfil Script for Middleman
    • [ ] Implement Known Features
    • [ ] Implement Middleman Features
    • [ ] Find a way to check all URLs/perm-links?
    • [ ] Rework the GitHub action?

    Custom Features

    • [ ] article_image_url that finds the first image in the blogpost
    • [ ] Blog roll
    • [ ] YouTube
    • [ ] Series
    • [ ] Git change log
    • [ ] Middleman mentions?
    • [ ] Essays vs. links RSS
    • [ ] Image in frontmatter -> blog template
    • [ ] ???

    Middleman Features

    • [ ] Index.html.erb
    • [ ] Calendar.html.erb
    • [ ] Tags.html.erb
    • [ ]
    • [ ] Tags for blogs vs tags for collections?
    • [ ] Markdown anchors on headers
    • [ ] Markdown features?

🔖