Introducing Alexandria: Faster, Smoother, Smarter

Adam FortunaAvatar for Adam Fortuna

By Adam Fortuna

8 min read

Back in September I started looking into ways of speeding up the Hardcover website, iOS and Android apps while also lowering our hosting costs. The main site was running on Next.js (app dir) and was hosting was starting to get expensive. It grew to $600/month on Vercel, then lowered when we moved to Google Cloud Run, then grew again over time as more readers joined.

Throughout that time, I tried a bunch of things to speed up the site, but to solve the real problems would require a LOT of code. The problem was that Hardcover was originally engineered to run entirely in the browser with no backend. When we switched to running on the server (Next.js 14 w/RSC), things sped up a little, but each request still needed to go through a bunch of layers (Next.js, Hasura, Rails, Postgres).

After a bunch of research, and a lot of soul searching, I decided it was time for a major change: we’d switch from Next.js to Ruby on Rails for the entire front-end. This wasn’t a decision I made lightly. I knew it would be a TON of work, but if successful the site would be faster, easier to maintain and cost less to host – wins all around.

So we started the migration – a plan to move to Inertia.js and Ruby on Rails. Since our backend was already Rails, and our front-end was React, the big shift in release was changing how data is loaded and sent to the browser when you load a page.

I always liked organization in release names, like Androids alphabetical dessert themed names. I thought we’d do something similar using libraries and literary themes. The Library of Alexandria seemed a perfect theme to start with. 😂

Previously, Next.js would hit a GraphQL server which would hit either Postgres or Rails, then return the contents and render out the page from there. Some parts of that were cached – at the Postgres, GraphQL and Next.js levels – but much more needed to be cached in Next.js using Redis.

Now Rails will check the cache (Solid Cache with Postgres) for the content and render the page immediately. If it’s not cached, it’ll use an existing database connection to load data, cache it and return it. This heavy emphasis on caching will be a HUGE speed improvement. Uncached pages will still be fast, but they won’t be instant fast.

What’s Changed?

Ok, that was all very technical. I’m going to try to list out everything that’s included in this release, but I’ll be a little brief.

Compare Your Reading

On the profile page, you’ll now see a little teaser for “Shared Reads”.

Clicking into the page will show every book that both you and that reader have read. We’ve divided it up into four groups: Books you both rated 4.5+ ⭐, ones you rated 2 ⭐ higher, ones they rated 2 ⭐ higher, and all books.

You can try this out with anyone right now:

If you’re a content creator on BookTok, Bookstagram or BookTube, please reach out to me (adam at hardcover.app)! I’d love to feature you here in the future.

Advanced Goals

A few years ago, before Hardcover, I used to do write ups about my past year in books on my blog. One year I realized I’d read 100 books, but only 10 were by women! The following year I intentionally read more books by women authors and saw that percentage grow to about 40% of my books from the year.

I only realized that because I was tracking and had the data. For others who are also data driven, we’ve improved Hardcover Goals to now support and celebrate more diversity in your reading.

This allows you to create goals that use any (or all) of the following:

  • By Reading Format – Read, Listen or Both
  • By Book Categorization – Book, Graphic Novel, Novella, Short Story, etc
  • By author gender – Male, Female or Other, including gender non-conforming
  • By author background – BIPOC or non-BIPOC
  • By author sexual orientation – LGBTQIA+ or Cis/Het authors

The author demographics are only as good as our database. If you spot an author not listed in a goal, or listed incorrectly, you can help out by updating that author or filing a report for us to update them.

Custom Profile Headers for Supporters

Supporters can now upload a profile header, or use one of the ones we provide. Down the line we’ll also allow Supporters to set custom headers for Lists.

Publisher and Character Librarian Tools

Librarians can now edit and mark both of these as duplicates. Publishers can also have a “parent” publisher – which can be used for imprints and alternative names.

Letterbooks Everywhere

Last year we released what we called Letterbooks, an improved way to showcase a list of books inspired by Letterboxd. These allow for changing the view (table, card or shelf), and sorting by a number of options.

Letterbooks are now available almost everywhere we show a list of books. That includes:

  • Your Library Pages – All, Want to Read, Currently Reading, Read, Stopped
  • Your Lists
  • Your Goals
  • Prompts
  • Genres, Moods and Tags
  • Author Books
  • Publisher Books
  • Series Books
  • Character Books

In each of these, you’ll be able to sort by Match Percentage if you’re a Supporter. We’re planning to add filtering and bulk edit mode to each of these areas soon too!

Genres, Moods and Tags

Speaking of Genres, Moods and Tags – they’re back! We disabled these pages because of how slow they were due to how we were generating them. Now they’re much faster.

We’ve also changed the sorting of them. Previously they were sorted by book popularity within that tag. Unfortunately, because anyone can tag a book, that meant that most popular books ended up being at the top of every genre. 😱

Now we’re sorting these by the number of readers who have tagged them with that genre, mood or tag. In other words, the top “Fantasy” book is the one that the most readers on Hardcover have tagged with a genre of “Fantasy” (Spoiler: Harry Potter #1 and Mistborn at the top of the list).

Editable Reading Journal Dates

You can now edit dates for your Reading Journal! 🥳 This is only available for entries you create – not the ones created by the system.

New Stats (Preview)

We were very ambitious with our ideas for new stats. 😂 Ste and I designed these during over a month of Hardcover Live episodes you can check out to see how we landed where we did.

What we’re releasing today is the Preview of stats. Finishing this up is at the very top of the list right now, alongside any bug fixes and the release. You can give it a shot today to see what about 25% of it will look like.

Upgrade from a Monthly to a Yearly Supporter

If you signed up monthly using Stripe, we didn’t have an option to upgrade to yearly before. You can now upgrade from your Membership page.

And So Much More!

Most of the other changes are less noticeable – increasing the width of pages, improving the way a group of books looks on mobile, changing how book reviews show up and making the site faster. 🚀

What Was Removed?

We also removed a few things as part of this release.

  • Removed all LLM and AI generated content. That includes book cover placeholders, book headers, genre cards and anything else generated by LLMs and AI. Instead, you’ll see a bunch of Ste originals. 🙌 Hardcover is now AI image free – except the 2023 Year in Books (which is a time capsule of the year).
  • Bookle is no more. 🫡 It was a fun experiment. We might bring it back someday.
  • Ask Jules, our AI Librarian, is taking a break. This was a wrapper to OpenAI with a custom script. It was a fun proof of concept, but we’re removing all LLM and AI integrations across Hardcover.
  • The Series Tracker is also gone. I still love the idea of something like this, but the implementation didn’t seem right. I’d like to revisit this sometime this year.
  • Reader Similarity is no longer a Labs project, and is now part of every profile. 🥳

What Changed from a Technology Standpoint?

I’ll be posting a series of articles about this. This was a major code update that spanned thousands of files and even new services to support the move. Here’s the high level list of changes.

  • Switched from Next.js to Inertia.js and Ruby on Rails using Vite and SSR
  • Upgraded from Ruby on Rails 6 to 8
  • Upgraded from Pay gem 6 to 8
  • Added a Screenshot service that generates OpenGraph images
  • Migrated external data and version data (Papertrail) to a cache server
  • Added new DB columns for books.featured_book_series_id and books.cached_featured_series to show top series for a book (API users: use this to easily get the top series!)
  • Added id to the authors in books.cached_contributors
  • Added action_at column to reading_journals (editable by users)
  • Created new taggable_counts table which caches the number of times something has been tagged (speeds up for the genre/mood/tag listing pages!).

You can use this to get the top genres for a book (or just use the books.cached_tags column).

query MyQuery {
  books(where: {slug: {_eq: "hyperion"}}) {
    taggable_counts(order_by: {count: desc}, limit: 10, where: {tag: {tag_category: {slug: {_eq: "genre"}}}}) {
      tag {
        tag
        slug
        tag_category {
          category
        }
      }
      count
    }
  }
}Code language: JavaScript (javascript)
  • Lots of missing database indexes
  • Added reverse proxy in front of api.hardcover.app with rate limiting, access logs, IP and User-based blocking (and more later)
  • Add users.cached_cover for supporters to set a header image on their profile
  • Start integrating with Grafana and Prometheus for server data
  • Add CDN in front of CSS, JS assets for improved page speed
  • Aggressively cache data for many pages using Solid Cache and Postgres
  • Moved search from TypeSense Cloud to Digital Ocean
  • Moved analytics from Plausible paid to Plausible Community Edition on Digital Ocean

Over the next few weeks I’m going to write a few articles about this migration. Once I write these I’ll link them here:

  • Part 1: How we fell out of love with Next.js and back in love with Ruby on Rails & Inertia.js
  • Part 2: Moving from the Cloud to the Server – Google & AWS to Digital Ocean and Kamal
  • Part 3: How we use Puppeteer to generate OpenGraph images
  • Part 4: Speeding up Ruby on Rails with Solid Cache, server side rendering, Sidekiq, and Brick
  • Part 5: Securing and Speeding up our API Server

If you have any technical questions or just want to chat about that side of Hardcover, feel free to join the #development channel on Discord.

What’s Next?

Well, lots of sleep for one. 😂 This project took a while.

  • Finish Stats – We’re aiming to do this first as we work on fixing bugs related to the release.
  • Final API Updates – We’re adding a follower database which API read queries will hit. This should separate API use from impacting the main site. Also limiting all queries to at most 3 levels deep (discussion of this in #api on Discord for the last few months).
  • Bulk Edit Mode, Filtering and Dashboard! – We can finally work on a few product updates we’ve been wanting forever.
  • April 1st? – We might do something fun. 😈

← More from the blog