Building Goodreads extension for Raycast
October 1st, 2023

Often times while I'm scrolling through twitter or reading an article, I come across a book recommendation. Next thing I do, not to my amusement is open a new tab and search for the book on Goodreads. It'd be a lot nicer if I could save a few mouse clicks and key stokes and look up the book without loosing context of the current task.

If only we could get book reviews on command without having to spin up a single serving browser tab. Lucky for us, the good folks at Raycast have built just the tool and made it extensible for developers to build their own workflows on top of it.

A little bit of prototyping and a lot of googling later, I present to you the Goodreads extension for Raycast.

Peek under the hood

Raycast is a productivity tool for macOS that helps users streamline their daily tasks and workflows. Raycast extensions are custom scripts that extend the Raycast app, enabling users to perform various tasks without leaving their current context.

Raycast extensions are written in Typescript and are executed in a sandboxed environment triggered by a keyword or a command.

Bootstrapping the extension

Raycast documentation has a great onboarding guide and tooling built into the app to get started within minutes. The extension is essentially a React app, powered by built-in UI components and API contracts to be consistent with other extensions. If you're familiar with React and Typescript, you're golden.

Reading data from Goodreads

Goodreads does not have a public API anymore, so the current implementation scrapes the data from the website. The scraped data is in HTML format, so we need a parser to extract the relevant bits of data. After scouting for a few, I settled on cheerio, jQuery fans in the house, pull up!

The fun parts

It's always a joy to build something that you'd use yourself. See the idea take shape, manifest into a real product and then be useful to others; It's a great feeling.

Though most of the development was in well tread waters, exploring Cheerio's API for HTML parsing and data extraction was a good learning experience.

While scrapping web pages, thing you end up doing often is look at the HTML source and figure out the right selector to extract the data you need.
Cheerio has a nifty extract method that let's you do this declaratively.

const data = $.extract({
    red: ".red",
    links: [
        {
            selector: "a",
            value: "href",
        },
    ],
});

This would return an object with the text content of the first .red element and all the href attribute of the a element. Only catch was current published version Cheerio doesn't expose this method yet, and I had to build an in-house variant of this taking reference from the Cheerio source.

It was a good exercise in how to reason about abstractions while building a public facing API contract.

As a happy side effect of writing a custom extractor, got a chance to get into the weeds of strong typing and generics in Typescript.

In the above example, the data object has to be typed as:

{
    red: string;
    links: string[];
}

derived from the selector input passed into the extract method. You can find the source code here

While surfacing the screenshots of extension on this blog post, I got carried away, like one does, and built a variant of coverflow image carousal (seen above). It'd make for a good blog post in itself, maybe for another time.

The tricky bits

The only wrench in the works was the occasional incomplete response from Goodreads. Though Goodreads pages are server rendered, in a logged out experience, you'd often get a partial response from the web server and rest is populated on the client. This is bad news for all the web-scrapper town.

Luckily, adding a few retries usually remedies the situation. If anyone has faced this issue and has a workaround, please leave a comment.

Fin.

In conclusion, fewer new tabs were opened, webpages were scraped, data was extracted and value was delivered.

Please give Goodreads extension a try. And if you're looking for source code, you can find it here