Subscribe for updates and more.

Making a book list

Planted 02019-10-19

Learning how to make a book list with Jekyll.

gbooksapi.jpg
Google Developer APIs to the rescue.

2020 Update: I have now optimized the book list for static loading

The Problem: finding all the information on books is annoying and time consuming.

The Solution: make technology do all the heavy lifting.

Some things we need to get

  • Title
  • Author
  • Book cover image

How to get them

Now that we have some vague ideas of what we need to get, let’s explore how we’re going to get them.

My first thought and the one I see implemented on other sites is Goodreads. Complete with it’s own API Goodreads is the choice for being in-sync. I’ve seen people implement progress updates, book reviews and ratings. But that’s all more than I’m looking to do with this. Also the Goodreads API is very slow.

Instead of fumbling around searching how to easily get an image for nearly any book cover like I did (cause hunting down an image for every update seriously sucks) I’ll lay out all the details here.

The solution: Google Books API.
Honestly I should’ve thought of that, thanks Google.

Google Books API is able to return the following information:

  • The kind of book
  • Google Books link
  • Title Yes!
  • Subtitle
  • Authors Yes!
  • Publisher
  • Published date
  • Description
  • Industry identifiers (ISBN, ASIN)
  • Page count
  • Print type
  • Categories
  • Average rating (from Google Books)
  • Ratings count (from Google Books)
  • Maturity rating
  • Image thumbnails Yes!
  • Text snippit
    They’re really all yesses.

Getting the information

That all sounds cool but I’ve never dealt with an API before. What’s the least amount of information we can use to extract all the data from the API?

My thoughts: ISBN codes.

Jekyll supports YAML for it’s _data directory and the structure is relatively simple. To add a book the only thing I need to find is the ISBN code. Everything else auto populates from that.

booklist.yaml

---
#Book List
books:
  - isbn: 9781250237231
  - isbn: 9780316478526
  - isbn: 9780525540830

Now we can pass this YAML formatted document as a JSON file and assign it to a variable on the /booklist/ page.

// Initializing variables for later
var book_url;
var imageLink;
var isbnslist = [];
var i=0;

{% assign isbncheck = site.data.booklist.books | jsonify %}

var isbns = {{ isbncheck }}

I had to do a lot of looking to figure out how to optimize performance and query every book in a single request. I couldn’t find this solution anywhere in the docs. Glad those errors are over with.

for ( i = 0; i < isbns.length; i++)
    {
      isbnslist.push(String(isbns[i].isbn));
    }

const url = isbnslist.reduce(
  (res, isbnslist, index) => res + (index ? "+OR+" : "") + "isbn:" + isbnslist,
  "https://www.googleapis.com/books/v1/volumes?q="
)

book_url = url + "+&fields=items(volumeInfo/description,volumeInfo/title,volumeInfo/authors,volumeInfo/imageLinks/thumbnail,volumeInfo/industryIdentifiers/identifier)+&maxResults=40"

Just to show a little bit of what’s going on here without explaining it.

// ISBN list is now an array
console.log(isbnslist)
[
  "9781250237231",
  "9780316478526",
  "9780525540830"
]

// All books full data return
console.log(url)
"https://www.googleapis.com/books/v1/volumes
?q=isbn:9781250237231+OR+
isbn:9780316478526+OR+
isbn:9780525540830"

// Filter fields for only needed data
console.log(book_url)
"https://www.googleapis.com/books/v1/volumes
?q=isbn:9781250237231+OR+
isbn:9780316478526+OR+
isbn:9780525540830+
&fields=items(
  volumeInfo/description,
  volumeInfo/title,
  volumeInfo/authors,
  volumeInfo/imageLinks/thumbnail,
  volumeInfo/industryIdentifiers/identifier
)
+&maxResults=40"

Now that we have the URLs to the data, all we have to do fetch it and plug it where we want it.

fetch(book_url)
.then(res => res.json())
.then((out) => {

  for (i=0; i < out.items.length; i++){
  document.getElementById("grid-container").innerHTML += "<div class="grid-item"> <a rel="nofollow" target="_blank" href="https://www.google.com/search?q=" + out.items[i].volumeInfo.title.replace(/ /g, "+") + "+by+" + out.items[i].volumeInfo.authors[0].replace(/ /g, "+") + ""> <div> <img alt="" + out.items[i].volumeInfo.title + " book cover" src="" + out.items[i].volumeInfo.imageLinks.thumbnail.replace("&edge=curl","") + ""> </div></a> <div> <h3><a rel="nofollow" target="_blank" href="https://www.google.com/search?q=" + out.items[i].volumeInfo.title.replace(/ /g, "+") + "+by+" + out.items[i].volumeInfo.authors[0].replace(/ /g, "+") + "">" + out.items[i].volumeInfo.title + "</a></h3> <p class="item">by " + out.items[i].volumeInfo.authors[0] + "</p> </div> </div>";
}

That super long ‘document.getElementByID’ provides the entire HTML structure of the books selection. It loops for every book returned from the Google Books API (so it adds that HTML for every book). For a cheat sheet on what the variables link to:

/*
  Title: out.items[0].volumeInfo.title
  Author: out.items[0].volumeInfo.authors[0]
  Thumbnail: out.items[0].volumeInfo.imageLinks.thumbnail
  Description: out.items[0].volumeInfo.description
*/

To add some error handling in case the ISBN provided does not show up within the Google API I managed to throw an error to the console to determine which ISBN would need to be fixed.

  i=0
  var loadedBooks = []
  while (i < Object.keys(out.items).length){
    //(Google API Supports ISBN_10, ISBN_13, ASIN...)
    // Find and save only ISBN 13 codes to loadedBooks array
    if (out.items[i].volumeInfo.industryIdentifiers[0].identifier.length > 10){
      loadedBooks.push(out.items[i].volumeInfo.industryIdentifiers[0].identifier)
    } else {
      loadedBooks.push(out.items[i].volumeInfo.industryIdentifiers[1].identifier)
    }
    i++;
  }

  Array.prototype.diff = function(a) {
    if (this.filter(function(i) {return a.indexOf(i) < 0;}).length !== 0){
      console.log(this.filter(function(i) {return a.indexOf(i) < 0;}))
    }else{
      return "All books indexed";
    }
  };

  console.log(isbnslist.diff(loadedBooks))
})
.catch(err => { console.log({{ isbn.isbn }}); throw err });

The loadedBooks array saves ISBNs that returned from the Google Books API. The Array.prototype.diff compares the loadedBooks against our isbnslist and logs any left out ISBNs to the console.

View the book list

Full code

// Initializing variables for later
var book_url;
var imageLink;
var isbnslist = [];
var i=0;

{% assign isbncheck = site.data.booklist.books | jsonify %}

var isbns = {{ isbncheck }}

for ( i = 0; i < isbns.length; i++)
    {
      isbnslist.push(String(isbns[i].isbn));
    }

const url = isbnslist.reduce(
  (res, isbnslist, index) => res + (index ? "+OR+" : "") + "isbn:" + isbnslist,
  "https://www.googleapis.com/books/v1/volumes?q="
)

book_url = url + "+&fields=items(volumeInfo/description,volumeInfo/title,volumeInfo/authors,volumeInfo/imageLinks/thumbnail,volumeInfo/industryIdentifiers/identifier)+&maxResults=40"

fetch(book_url)
.then(res => res.json())
.then((out) => {

  for (i=0; i < out.items.length; i++){
  document.getElementById("grid-container").innerHTML += "<div class="grid-item"> <a rel="nofollow" target="_blank" href="https://www.google.com/search?q=" + out.items[i].volumeInfo.title.replace(/ /g, "+") + "+by+" + out.items[i].volumeInfo.authors[0].replace(/ /g, "+") + ""> <div> <img alt="" + out.items[i].volumeInfo.title + " book cover" src="" + out.items[i].volumeInfo.imageLinks.thumbnail.replace("&edge=curl","") + ""> </div></a> <div> <h3><a rel="nofollow" target="_blank" href="https://www.google.com/search?q=" + out.items[i].volumeInfo.title.replace(/ /g, "+") + "+by+" + out.items[i].volumeInfo.authors[0].replace(/ /g, "+") + "">" + out.items[i].volumeInfo.title + "</a></h3> <p class="item">by " + out.items[i].volumeInfo.authors[0] + "</p> </div> </div>";
}

/*
  Title: out.items[0].volumeInfo.title
  Author: out.items[0].volumeInfo.authors[0]
  Thumbnail: out.items[0].volumeInfo.imageLinks.thumbnail
  Description: out.items[0].volumeInfo.description
*/

  i=0
  var loadedBooks = []
  while (i < Object.keys(out.items).length){
    if (out.items[i].volumeInfo.industryIdentifiers[0].identifier.length > 10){
      loadedBooks.push(out.items[i].volumeInfo.industryIdentifiers[0].identifier)
    } else {
      loadedBooks.push(out.items[i].volumeInfo.industryIdentifiers[1].identifier)
    }
    i++;
  }

  Array.prototype.diff = function(a) {
    if (this.filter(function(i) {return a.indexOf(i) < 0;}).length !== 0){
      console.log(this.filter(function(i) {return a.indexOf(i) < 0;}))
    }else{
      return "All books indexed";
    }
  };

  console.log(isbnslist.diff(loadedBooks))
})
.catch(err => { console.log({{ isbn.isbn }}); throw err });