Background

There has been some chatter on the micro.blog timeline about adding some better tools for book covers when sharing book suggestions, etc. @michaelfransen started off asking about a gallery of images similar to the way the Photos page works. As the conversation went on @jsonbecker brought up the idea of ISBN to cover image inside of a Hugo data page.

There are a few services that have APIs that should get the image, Goodreads (Amazon) and Google Books are the biggest. Since Hugo can grab JSON when building pages it is possible to just use a short-code to build out what you'd need for a naive implementation.


Drawbacks

While looking into this I noticed that both the Goodreads and Google APIs have their problems. On the Google side of things not every result is going to have larger image sizes:

"imageLinks": {
"smallThumbnail": "http://books.google.com/books/content?id=DRlhnQEACAAJ&printsec=frontcover&img=1&zoom=5&source=gbs_api",
"thumbnail": "http://books.google.com/books/content?id=DRlhnQEACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api"
},

You can change the “zoom” but that doesn't result in an image sadly. Other than that the Google API works pretty much as we'd need. Search for an ISBN and get an object back with results! Oh but wait… there are a bunch of ISBNs that return no cover image.

Not to worry, Goodreads can help us out there. After all, they have the Amazon library of images along with crowd sourced covers. Except in order to look up books on Goodreads you need the ID - and the API call to look up the ID by ISBN doesn't work. And, from what I've read, isn't very reliable when it does. Also, it responds with XML so, that won't help.

The final draw back would be that the images aren't “download” they would be linked from the Goodreads or Google hosting where they are stored. That may not seem like a drawback but at some point they will stop allowing the images to be linked from certain referring domains.


Overcoming Obstacles

Since Google can't be relied on we going to move them to backup status. We'll use Goodreads as the primary source and fallback as needed.

That still presents us with the problem of the Goodreads API not working. Well, it turns out the workaround for that is pretty easy. I noticed that if you searched for an ISBN in on the main page you would get a single response with what I was looking for.

Taking a look in Chrome console I was pleased to discover that the search GET request returns a nice JSON object.

[
  {
    "imageUrl": "https://i.gr-assets.com/images/S/compressed.photo.goodreads.com/books/1388494987i/38379._SY75_.jpg",
    "bookId": "38379",
    "workId": "1921329",
    "bookUrl": "/book/show/38379.Ring",
    "from_search": true,
    "qid": "Xoh78SXtTz",
    "rank": 1,
    "title": "Ring (Ring, #1)",
    "bookTitleBare": "Ring",
    "numPages": 282,
    "avgRating": "3.82",
    "ratingsCount": 13682,
    "author": {
      "id": 14465,
      "name": "Kōji Suzuki",
      "isGoodreadsAuthor": false,
      "profileUrl": "https://www.goodreads.com/author/show/14465.K_ji_Suzuki",
      "worksListUrl": "https://www.goodreads.com/author/list/14465.K_ji_Suzuki"
    },
    "kcrPreviewUrl": null,
    "description": {
      "html": "A mysterious videotape warns that the viewer will die in one week unless a certain, unspecified act is performed. Exactly one week after watching the tape, four teenagers die…",
      "truncated": true,
      "fullContentUrl": "https://www.goodreads.com/book/show/38379.Ring"
    }
  }
]

What's more, we have an image URL right there in the response!

Ring cover

Almost! If only this image was the full cover. It turns out that it is, sort of. If we drop the ._SY75_ we'll see the “full size image”.

Ring cover

SY75 seems to be some sort of scaling, maybe. If you play around with that you can get back different size images up to what is likely the maximum size. Now we are getting somewhere and we have enough to create our basic short-code!

But first, let's talk about the third drawback - the eventual loss of images. The way the short-code would work is to build out an <img> link, Hugo can't download the images. This means the image data remains on a remote server. I can envision one way to work on getting out from under that which would not require attempting to mirror all the images. A simple proxy which looked locally for downloaded images before passing the request upstream would work. This would be “easy” to setup I think - but it's beyond what we are trying to do right now.


Shortcode

I am still new to working with Hugo templating so there may be better methods to accomplish this but it works. In my theme I have a file at layouts/shortcodes/book.html. Here we'll take a closer look at what is in it, the complete code can be found a bit further down. I think I may need to make a short-code repo on GitHub as well. I'll add a link here if/when I do.

First, we'll declare a variable and assign the URL to an image that says “image not available”. We'll use this as our image if nothing comes back from the API.

{{ $fin := "https://books.google.com/books/content?id=DRlhnQEACAAJ&printsec=frontcover&img=1&zoom=2&source=gbs_api" }}

Next we will set our API URLs. We'll also put together our query parameters. This sets us up to use getJSON.

{{ $grUrlPre := "https://www.goodreads.com/book/auto_complete" }}
{{ $googUrlPre := "https://www.googleapis.com/books/v1/volumes" }}

{{ $grIsbn := printf "%s%s" "?format=json&q=" .Params.isbn }}
{{ $googIsbn := printf "%s%s" "?q=isbn:" .Params.isbn }}

We make two calls out to the APIs. I thought about checking one and then the other only if needed. In the end though I think it keeps the snippet much easier to read. After all what's one more GET request? Anyway, we store the resulting object in a variable.

{{ $grJ := getJSON $grUrlPre $grIsbn }}
{{ $googJ := getJSON $googUrlPre $googIsbn }}

Since we are concerned with Goodreads we'll check that first to make sure that the length of the object is larger then 0. If it is then we next assign the first object within the response to a variable. Within that object we'll pull out the imageUrl value and using regular expression select and replace the “scaling” tag, ._SY75_. with just a .. That's the end so just write out or final HTML.

{{ if gt (len $grJ) 0 }}
  {{ $i := index $grJ 0 }}
  {{ $fin := replaceRE "._.*_." "." $i.imageUrl }}
  <img src="{{ $fin }}" class="book_cover"/>

But, what if?! Well, we can carry on over to the Google API response if needed. We start of the same, checking that the response has data. If it does have data again we assume that the first portion of the object is what we want. In this example, because we can't be sure if a larger image exists I am using volumeInfo.imageLinks.thumbnail.

{{ else if gt ($googJ.totalItems) 0 }}
  {{ $i := index $googJ.items 0 }}
  <img src="{{ $i.volumeInfo.imageLinks.thumbnail }}" class="book_cover"/>

And if all that went off the rails? The we default to displaying our image not available image.

{{ else}}
  <img src="{{ $fin }} class="book_cover""/>
{{ end }}

layouts/shortcodes/book.html

{{ $fin := "https://books.google.com/books/content?id=DRlhnQEACAAJ&printsec=frontcover&img=1&zoom=2&source=gbs_api" }}

{{ $grUrlPre := "https://www.goodreads.com/book/auto_complete" }}
{{ $googUrlPre := "https://www.googleapis.com/books/v1/volumes" }}

{{ $grIsbn := printf "%s%s" "?format=json&q=" .Params.isbn }}
{{ $googIsbn := printf "%s%s" "?q=isbn:" .Params.isbn }}

{{ $grJ := getJSON $grUrlPre $grIsbn }}
{{ $googJ := getJSON $googUrlPre $googIsbn }}

  {{ if gt (len $grJ) 0 }}
    {{ $i := index $grJ 0 }}
    {{ $fin := replaceRE "._.*_." "." $i.imageUrl }}
    <img src="{{ $fin }}" class="book_cover"/>
  {{ else if gt ($googJ.totalItems) 0 }}
    {{ $i := index $googJ.items 0 }}
    <img src="{{ $i.volumeInfo.imageLinks.thumbnail }}" class="book_cover"/>
  {{ else}}
    <img src="{{ $fin }}" class="book_cover"/>
  {{ end }}

Usage

That's great but how does it work? Next time you are writing a post add:

{{&lt; book isbn="9781493415465" &gt;}}

Note, that Hugo was having a heck of a time not actually using this as a short-code. You may need to covert the great-than and less-than signs back to symbols

If all goes well you should see your cover image. Note that we have done no styling so you could end up with an image that's to larger (or even too small).

Over the next little while I'll see if we can get our covers to display as a gallery, like the photos do. Until next time…


Enjoy this post?
How about buying me a coffee?