Book tracker !

My Books by Year v.2

Every book I read is organized by year using Goodreads.

The first version of this project relied on manually importing a CSV file containing all the books saved on Goodreads into the hardcover.app website. I wasn’t satisfied with that approach because of the manual step in the middle. After a sudden spark of inspiration, I realized I could use Goodreads’ RSS functionality to fetch information about the books I’ve read and build a cleaner, more streamlined version—where the only manual action required is updating my Goodreads shelves.

The original version is still available, but I decided not to maintain it anymore. You can still take a look at it through this link.

Each list corresponds to a specific year.

You might be wondering what the point of all these trackers is… well, I just enjoy them.

Loading...

Technical Details

This page works thanks to a small front-end script + a Cloudflare Worker acting as a proxy/parser for Goodreads RSS feeds.

Architecture Overview

flowchart TD A["Browser / Front-end\nBooks tracker page"] -->|fetch JSON| B["Cloudflare Worker\ngoodreads-rss-parser"] B -->|fetch RSS XML| C["Goodreads RSS Feed\nlist_rss/USER_ID"] C -->|RSS XML| B B -->|Parsed JSON| A

Front-end Workflow (UI + Fetch)

When the Book Tracker page loads, the JavaScript running in the browser first determines the current year using new Date().getFullYear() and generates an array of years from the defined START_YEAR up to the current year, reversing the order so that the most recent years appear first. This array is then passed to the generateMenu() function, which dynamically constructs the navigation menu in the user interface, creating a clickable link for each year. After generating the menu, the script simulates a click on the first year in the list—usually the most recent one—and calls the loadYear() function to fetch and load the books for that year. During this process, the book data is retrieved and the books grid in the UI is populated with cards for each book, displaying the cover image, title, and a link to Goodreads, allowing users to immediately browse the books read in the selected year.

sequenceDiagram participant U as User participant FE as Front-end (Browser) participant UI as DOM (Menu + Grid) U->>FE: Load page FE->>FE: currentYear = new Date().getFullYear() FE->>FE: goodreadsLists = [START_YEAR .. currentYear] FE->>FE: goodreadsLists.reverse() FE->>UI: generateMenu(goodreadsLists) U->>FE: Click on first year FE->>FE: loadYear(goodreadsLists[0]) FE->>UI: Render books grid for selected year

Backend Workflow (Worker RSS Parser)

When the front-end requests book data for a specific Goodreads shelf, it sends a GET request to the Cloudflare Worker with the rssUrl parameter. The Worker first validates that the URL is well-formed and that the hostname includes goodreads.com to prevent invalid or malicious requests. Once validated, the Worker fetches the RSS feed from Goodreads, receiving the XML containing multiple <item> blocks, each representing a book. The Worker then parses each <item> block, extracting relevant information such as the book title, author name, cover image URL (falling back to medium or small images if necessary), Goodreads link, and the description. Optionally, it also parses the user’s completion date if available. After processing all items, the Worker returns a clean JSON object to the front-end containing the total count of books and an array of book objects with all the extracted metadata, ready to be rendered in the UI.

sequenceDiagram participant FE as Front-end participant W as Cloudflare Worker participant GR as Goodreads RSS FE->>W: GET /?rssUrl="goodreads_rss_url" W->>W: Validate rssUrl (hostname must include goodreads.com) W->>GR: fetch(rssUrl) GR-->>W: RSS XML (items) W->>W: Parse "item" blocks W->>W: Extract title/author/cover/link/description W-->>FE: JSON { count, books[] }

Data Extraction (from RSS )

Each RSS is converted into a normalized book object:

flowchart LR A["item ... /item"] --> B["title\n(CDATA or plain text)"] A --> C["author_name"] A --> D["book_*_image_url\n(large/medium/small fallback)"] A --> E["description HTML"] E --> F["Extract Goodreads book URL"] A --> G["book_description"] G --> H["stripHtml + decodeHtmlEntities"] A --> I["user_read_at"] I --> J["parseGoodreadsDate"] B --> K["Book JSON"] C --> K D --> K F --> K H --> K J --> K

Resulting object example:

{
  "title": "Example Title",
  "author": "Example Author",
  "cover": "https://...",
  "goodreadsUrl": "https://www.goodreads.com/book/show/...",
  "description": "Clean description text",
  "readAt": "2024-01-03T08:00:00.000Z"
}

Why the Worker is Needed

Goodreads RSS is served as XML and cannot be easily consumed directly in the browser as structured data. The Worker solves this by:

  • acting as a CORS-friendly proxy

  • converting RSS XML → JSON

  • providing a consistent output format for the UI

flowchart LR A[Goodreads RSS XML] -->|Hard to use in Front End| X[Browser] A -->|Worker parses| B[Worker JSON API] B -->|Easy fetch + render| X

Summary

A personal book tracking page that organizes everything I read by year. Using Goodreads data via RSS, this tracker displays my reading history in an interactive grid.

What's New in This Version

- Migrated book data source from Hardcover API to Goodreads RSS via Cloudflare Worker.
- Added dynamic year-based navigation menu (from START_YEAR to current year).
- Implemented JSON parsing of RSS feed with proper handling of CDATA and plain text titles.
- Added filtering by year of completion (`user_read_at`) on the Worker.
- Updated front-end to fetch parsed JSON from Worker and render books grid per year.
- Added detailed documentation and Mermaid diagrams for front-end and back-end workflow.
- CORS support added to Worker for cross-origin requests.

Notes mentioning this note

There are no notes linking to this note.


Here are all the notes on this site, along with their links, visualized as a graph.