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:
- I created a folder called reading
- Within the folder, a reading.json file (this follows the convention of the posts.json definition for the default Post content type)
reading.json is very concise:
{
"layout": "book",
"content_type": "book"
}
This specifies two things:
- A layout file (book.njk)
- A content type for the collection (book)
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.