Displaying your reading journey with Eleventy and Goodreads
As avid readers, many of us track our books on Goodreads. Wouldn't it be nice to display your current reading list directly on your personal site? In this post, I'll walk you through how I created a dynamic bookshelf for my site using Eleventy's powerful data system and the Goodreads RSS feed.
Understanding Eleventy's Data Cascade
One of Eleventy's most powerful features is its data system. The _data directory allows you to load and manipulate external data sources that become globally available to your templates.
Data files in the _data directory can export JavaScript objects or functions that return data. When you use a JavaScript file that exports an asynchronous function, Eleventy waits for the promise to resolve before building your site.
Enter EleventyFetch
For external API calls, 11ty provides a helpful utility called @11ty/eleventy-fetch. This package handles not just the fetching of data but also intelligent caching, which is important for:
- Reducing build times by not re-fetching unchanged data
- Avoiding rate limits on external APIs
- Allowing your site to build even when external services are down
Here's how to install it:
npm install @11ty/eleventy-fetch
Setting Up Our Goodreads Data File
Let's walk through how we set up our Goodreads data fetcher. First, we create a JavaScript file in the _data directory:
// _data/goodreads.js
import EleventyFetch from '@11ty/eleventy-fetch';
import { DOMParser } from '@xmldom/xmldom';
// Your Goodreads RSS feed URL
const GOODREADS_FEED_URL =
'https://www.goodreads.com/review/list_rss/YOUR_USER_ID';
export default async function() {
console.log('Fetching Goodreads feed...');
try {
// Fetch the RSS feed with caching (1 day)
const feedXml = await EleventyFetch(GOODREADS_FEED_URL, {
duration: "1d",
type: "text"
});
// Process the XML response
const parser = new DOMParser();
const doc = parser.parseFromString(feedXml, 'text/xml');
const items = doc.getElementsByTagName('item');
console.log(`Successfully fetched ${items.length} items from Goodreads feed.`);
// Set up our data structure
const books = {
currentlyReading: [],
read: [],
toRead: []
};
// Process each book
for (let i = 0; i < items.length; i++) {
const item = items[i];
const shelves = item.getElementsByTagName('user_shelves')[0].textContent;
const readAt = item.getElementsByTagName('user_read_at')[0].textContent;
// Extract book data
const book = {
title: item.getElementsByTagName('title')[0].textContent,
author: item.getElementsByTagName('author_name')[0].textContent,
coverImageUrl: item.getElementsByTagName('book_large_image_url')[0]
.textContent,
rating: item.getElementsByTagName('user_rating')[0].textContent,
review: item.getElementsByTagName('user_review')[0].textContent,
readAt,
shelves
};
// Categorize books by shelf
if (shelves.includes('currently-reading')) {
books.currentlyReading.push(book);
} else if (shelves.includes('to-read')) {
books.toRead.push(book);
} else if (readAt) {
books.read.push(book);
}
}
return books;
} catch (error) {
console.error('Error fetching or parsing Goodreads feed:', error);
// Return empty structure for graceful failure
return {
currentlyReading: [],
read: [],
toRead: []
};
}
}
Breaking Down the Code
1. Imports and Setup
We start by importing EleventyFetch and a DOM parser to handle the XML response. Then we define the Goodreads RSS feed URL with our user ID.
2. Fetching and Caching
The EleventyFetch call handles both fetching and caching:
const feedXml = await EleventyFetch(GOODREADS_FEED_URL, {
duration: "1d", // Cache for one day
type: "text" // We expect text/XML data
});
The duration parameter tells EleventyFetch how long to cache the response. In this case, we're caching for one day, which means your site will only fetch new data from Goodreads once every 24 hours during builds.
3. Parsing the XML
Goodreads returns data as XML, so we use @xmldom/xmldom to parse it into a navigable document:
const parser = new DOMParser();
const doc = parser.parseFromString(feedXml, 'text/xml');
const items = doc.getElementsByTagName('item');
4. Organizing Books by Shelf
We create a data structure to organize books into three categories:
- Currently reading
- To read
- Read
Then we loop through each book in the feed, extract its data, and categorize it based on its shelf:
if (shelves.includes('currently-reading')) {
books.currentlyReading.push(book);
} else if (shelves.includes('to-read')) {
books.toRead.push(book);
} else if (readAt) {
books.read.push(book);
}
5. Error Handling
We implement robust error handling to ensure our site builds even if the Goodreads feed is unavailable. In case of an error, we return an empty structure that matches the expected format.
Creating the Bookshelf Template
Once we have the data, we need to create a template to display it. Here's a simple example using Nunjucks:
<!-- bookshelf.njk -->
---
layout: base.njk
title: My Bookshelf
---
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">My Bookshelf</h1>
{% if goodreads.currentlyReading.length > 0 %}
<section class="mb-12">
<h2 class="text-2xl font-semibold mb-4">Currently Reading</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for book in goodreads.currentlyReading %}
<div class="flex border rounded-lg overflow-hidden shadow-md
dark:border-gray-700">
<div class="w-1/3">
<img src="{{ book.coverImageUrl }}"
alt="{{ book.title }}"
class="w-full h-full object-cover">
</div>
<div class="w-2/3 p-4">
<h3 class="font-bold">{{ book.title }}</h3>
<p class="text-sm">by {{ book.author }}</p>
</div>
</div>
{% endfor %}
</div>
</section>
{% endif %}
{% if goodreads.toRead.length > 0 %}
<section class="mb-12">
<h2 class="text-2xl font-semibold mb-4">Want to Read</h2>
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4
xl:grid-cols-6 gap-4">
{% for book in goodreads.toRead %}
<div class="border rounded overflow-hidden shadow-md
dark:border-gray-700">
<img src="{{ book.coverImageUrl }}"
alt="{{ book.title }}"
class="w-full h-auto">
<div class="p-2 text-center">
<h3 class="text-sm font-medium truncate">
{{ book.title }}
</h3>
</div>
</div>
{% endfor %}
</div>
</section>
{% endif %}
{% if goodreads.read.length > 0 %}
<section>
<h2 class="text-2xl font-semibold mb-4">Recently Read</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{% for book in goodreads.read | slice(0, 6) %}
<div class="flex border rounded-lg overflow-hidden shadow-md
dark:border-gray-700">
<div class="w-1/4">
<img src="{{ book.coverImageUrl }}"
alt="{{ book.title }}"
class="w-full h-full object-cover">
</div>
<div class="w-3/4 p-4">
<h3 class="font-bold">{{ book.title }}</h3>
<p class="text-sm">by {{ book.author }}</p>
{% if book.rating and book.rating != '0' %}
<div class="mt-2">
<span class="text-yellow-500">★</span>
{{ book.rating }}/5
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</section>
{% endif %}
</div>
Finding Your Goodreads RSS Feed
To get your own Goodreads RSS feed URL:
- Log in to your Goodreads account
- Go to My Books
- Click on the RSS link at the bottom of the page
- Copy the URL, which should look like
https://www.goodreads.com/review/list_rss/YOUR_USER_ID
Conclusion
By leveraging 11ty's data cascade and EleventyFetch, we've created a dynamic bookshelf that automatically updates whenever we change our reading status on Goodreads. This same approach can be applied to any external API or data source, making it easy to keep your site's content fresh and dynamic without manual updates.
The combination of smart caching and error handling ensures that our site builds remain fast and reliable, even when external services are unavailable.
Happy reading, and happy coding!