Mark Llobrera

Eleventy: Custom Content Type and Collection for Books

Continuing my adventures in Eleventy: I wanted to create a collection for books that I’ve read, separate from the default Post type. But I didn’t want to have the collection called Books, because that would imply that I had written the books.

I settled on a compromise: I would call my custom type Book, but my collection would be called Reading.

Book Type

Creating a type called book was fairly simple:

reading.json is very concise:

{
"layout": "book",
"content_type": "book"
}

This specifies two things:

This means that for my collection I can grab everything of content type book, and that each book file will be rendered by the book.njk template file.

A book’s front matter has metadata (this came out of my recent project for tracking my reading):

---
title: Gideon the Ninth
author: Tamsyn Muir
date: 2020-05-21
description: Tamsyn Muir’s galactic necromancy saga.
publication_date: 2019-09-10
cover_image: reading/gideon-the-ninth.jpg
genre: Fiction
format: Hardcover
reading_start_date: 2020-05-16
reading_end_date: 2020-05-16
tags:
- reading
---

Everything after that Markdown, just like you would find in a default post.

Retrieving the Collection

With the Book content type defined, I can now retrieve my collection in my .eleventy.js file. Originally I did this:

eleventyConfig.addCollection("reading", function (collection) {
return collection.getAll().filter(function (item) {
return item.data.content_type == "book";
});
});

Which worked — I got all my books back in a list. Except … the order of books was unstable. Even with a date set in the YAML front matter for each book post, the order of the books kept changing depending on which was saved last. So I tacked on a sort() call, using this example from the docs:

eleventyConfig.addCollection("reading", function (collection) {
return collection
.getAll()
.filter(function (item) {
return item.data.content_type == "book";
})
.sort(function (a, b) {
return b.date - a.date;
});
});

Now the order is explicitly dependent on the date from the front matter. This also means I don’t really have to use the reverse filter within my books list template.

Templates

Books Shell Template

Now that I have my collection, I can render out a list for my “Reading” page. I created a books.njk template, which looks like this:


---
layout: layouts/home.njk
permalink: /reading/
eleventyNavigation:
key: Reading
order: 2
---
<div class="content-main container">
<h1>Reading</h1>


{% set bookslist = collections.reading %}
{% include "bookslist.njk" %}
</div>

That creates a “Reading” link in the main navigation, and maps /reading as the URL. It also sets a variable, bookslist, that is the result of the collection I retrieved.

Books List Template

bookslist.njk looks very much like postslist.njk — note that I’m not reversing the order of my bookslist, because the collection is already sorted reverse chronologically. Note the book-specific book.data.[X] front matter variables:


<ul reversed class="postlist">
{% for book in bookslist %}
<li class="postlist-item{% if post.url == url %} postlist-item-active{% endif %}">
<a href="
{{ book.url | url }}" class="postlist-link">{% if book.data.title %}{{ book.data.title }}{% else %}<code>{{ book.url }}</code>{% endif %}</a>
<p>
{{ book.data.description }}</p>
<span class="post-meta">Finished <time class="postlist-date" datetime="
{{ book.data.reading_end_date | htmlDateString }}">{{ book.data.reading_end_date | readableDate }}</time></span>
</li>

{% endfor %}
</ul>

Book Detail Template

Finally, book.njk, which renders a single book post. Nothing fancy here, just printing out a bunch of front matter variables and sanitized Markdown content:


---
layout: layouts/base.njk
templateClass: tmpl-book
---
<article class="content-main container book-single">
<div class="book-details">
<div class="book-cover book-shadow">

{% figure cover_image, "", "book-thumb" %}
</div>
<div class="book-meta">
<h1>
{{ title }}</h1>
<h2>
{{author}}</h2>
<dl>
<dt>Publication Date</dt>
<dd>
{{ publication_date | readableDate }}</dd>
<dt>Genre</dt>
<dd>
{{ genre }}</dd>
<dt>Format</dt>
<dd>
{{ format }}</dd>
<dt>Started Reading</dt>
<dd>
{{ reading_start_date | readableDate }}</dd>
<dt>Finished Reading</dt>
<dd>
{{ reading_end_date | readableDate }}</dd>
</dl>
</div>
</div>
<div class="book-body">

{{ content | safe }}
</div>
</article>
<p><a href="
{{ '/reading' | url }}">← All Books</a></p>

What’s Still Confusing

So I’ve got a custom type now, which feels good — I can just treat books as a first-class type instead of using a tag to filter out posts. For the life of me I can’t figure out how to get a custom taxonomy working, however. It’s not quite necessary for what I want to build, but that is usually a default thing I have to consider when I’m in WordPress/Drupal-land.