Goodreads no Longer Supports RSS

Nov 10, 2026

There is a workaround 😉

Goodreads has turned off their rss feature. This is what I, and others, used to access their Goodreads data via an api request. This was the api request that powers that books portion of my about page. The post claims this was due to a security issue, normally I would dismiss such a claim, "what could be so harmful about book data", I would think, but this is 2026, the year when every other post on Hacker News is about some security loophole discovered where some archaic tech, *cough* xml *cough*, gets used as an exploit for something much more horrible.

Anyway, the workaround as the subtitle promised…

First is in

Customize the widget to display the information you would like, on the bottom there is a <script> tag, the src attribute is what we need.

Mine looks like this:

CURRENTLY_READING = "https://www.goodreads.com/review/custom_widget/147397213.Lucas's%20bookshelf:%20currently-reading?cover_position=left&cover_size=small&num_books=5&order=a&shelf=currently-reading&show_author=1&show_cover=1&show_rating=1&show_review=1&show_tags=1&show_title=1&sort=date_added&widget_bg_color=FFFFFF&widget_bg_transparent=&widget_border_width=1&widget_id=1793723772&widget_text_color=000000&widget_title_size=medium&widget_width=medium"

From here, we can fetch this information like by directly querying the URL.

class BookDict(TypedDict):
    type: str
    img: str
    title: str
    link: str
    subtitle: str


def fetch_goodreads(url: str) -> list[BookDict]:
    js = urlopen(url).read().decode("utf-8")
    match = re.search(r"var widget_code = '(.*?)'\n", js, re.DOTALL)
    if not match:
        raise ValueError("Goodreads widget_code not found")
    widget = match.group(1).replace("\\n", "\n").replace('\\"', '"').replace("\\/", "/")
    blocks = widget.split('class="gr_custom_each_container_')[1:]
    return [parse_goodreads(block) for block in blocks]

def parse_goodreads(block: str) -> BookDict:

    title = find(r'<a title="([^"]*)"', block)
    author = " ".join(find(r'/author/show/[^>]*>([^<]*)</a>', block).split())

    # Drop the "._SY75_" size suffix to get the full-size cover.
    image = re.sub(r"\._S[XY]\d+_(?=\.jpg)", "", find(r'<img alt="[^"]*" border="0" src="([^"]*)"', block))
    # shortcut: book id is parsed from the cover filename (undocumented), fix if links break.
    book_id = find(r"/(\d+)(?:\.[^/]*)?\.jpg$", image)

    link = f"https://www.goodreads.com/book/show/{book_id}"

    book = BookDict(
        type="art",
        img=image,
        link=link,
        title=title,
        subtitle=author,
    )

    return book


  

This, fetch_goodreads(CURRENTLY_READING), fetches the widget source code, and looks through it with regex. Stuff like .gr_custom_each_container_ and widget_code can be replaced at any notice, if you notice the /about page working well, you can assume this script currently works.

https://overlandandseas.dev/posts/atom.xml