# `astro-loader-goodreads`

[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![License][license-src]][license-href]
[![JSDocs][jsdocs-src]][jsdocs-href]
[![bundle][bundle-src]][bundle-href]

[![Astro][astro-src]][astro-href]
[![Goodreads][goodreads-src]][goodreads-href]
[![How to use Goodreads data in Astro ][how-to-use-src]][how-to-use-href]

**Load Goodreads data for books in shelves/lists, user updates, and author blogs into Astro.**

> [!NOTE]
> `astro-loader-goodreads` uses the [Astro Content Loader API](https://docs.astro.build/en/reference/content-loader-reference/) to fetch data from Goodreads RSS feeds.
>
> See the [Usage](#usage) section below for instructions on how to use the package to load the fetched Goodreads data into [Astro Content Collections](https://docs.astro.build/en/guides/content-collections).

---

## Table of Contents

- [Features](#features)
- [Community Examples](#community-examples)
- [Installation](#installation)
- [Usage](#usage)
  - [Loader Options](#loader-options)
  - [Defining & Using Astro Content Collections](#defining--using-astro-content-collections)
    - [1. Goodreads Shelves](#1-goodreads-shelves)
    - [2. Goodreads User Updates](#2-goodreads-user-updates)
    - [3. Goodreads Author Blogs](#3-goodreads-author-blogs)
- [Data Schema](#data-schema)
  - [Overview](#overview)
  - [1. `BookSchema`](#1-bookschema)
  - [2. `UserUpdateSchema`](#2-userupdateschema)
  - [3. `AuthorBlogSchema`](#3-authorblogschema)

---

## Features

- **Load Bookshelves**: Import your Goodreads shelves to showcase your reading list, books you've read, or want to read.
- **User Updates**: Display your latest Goodreads activity including reading status updates, reviews, and likes.
- **Author Blogs**: Fetch author blogs from Goodreads to display updates from your favorite authors.
- **Astro Content Collections**: Seamlessly integrates with Astro's content collection system for type-safe data access.

## Community Examples

Below are some examples of websites that use `astro-loader-goodreads`. If you wish to add your site to this list, open a pull request!

| Site                           | Page                                       | Description                                             | Source                                  |
| ------------------------------ | ------------------------------------------ | ------------------------------------------------------- | --------------------------------------- |
| [sadman.ca](https://sadman.ca) | [sadman.ca/about](https://sadman.ca/about) | Books I'm currently reading and have recently finished. | [→](https://github.com/sadmanca/blogv2) |

---

## Installation

```sh
npm add astro-loader-goodreads
```

Supports Astro 4.14.0 and newer.

## Usage

### Loader Options

| Property              | Description                               | Required   | Default |
| --------------------- | ----------------------------------------- | ---------- | ------- |
| `url` | The URL of your Goodreads shelf, user, or author. | ✅ | - |
| `refreshIntervalDays` | Number of days to cache data before fetching again from Goodreads. | ❌ | `0` |

When `refreshIntervalDays` is set (e.g., to `7` for weekly updates), the loader will only fetch new data from Goodreads when that many days have passed since the last fetch. 
feat: Add optional loader option `refreshIntervalDays`. If not specified, no caching is done between builds (Astro's default data caching between page loads still applies).

### Defining & Using Astro Content Collections

`astro-loader-goodreads` supports loading Goodreads data from 3 types of urls:

1. **[Shelves](#1-goodreads-shelves)**: Load books from a Goodreads shelf.
2. **[User Updates](#2-goodreads-user-updates)**: Load a Goodreads user's updates feed.
3. **[Author Blogs](#3-goodreads-author-blogs)**: Load a Goodreads author's blog.

In your `content.config.ts` or `src/content/config.ts` file, you can define your content collections using each type of URL with the `goodreadsLoader` function.

> [!NOTE]
> For the full list of fields available for Astro content collections created using `astro-loader-goodreads`, see the [Data Schema](#data-schema) section below.

---

### 1. Goodreads Shelves

**To load data for books from a Goodreads shelf, use the shelf's URL** (e.g. https://www.goodreads.com/review/list/152185079-sadman-hossain?shelf=currently-reading). `astro-loader-goodreads` will convert it to the correct RSS feed URL automatically.

> [!IMPORTANT]
> **The RSS feed for a Goodreads shelf only includes the last 100 books added to that shelf.** This means that if there are more than 100 books in a shelf, `astro-loader-goodreads` will not be able to retrieve them all.
>
> You can, however, create multiple shelves (e.g. _read-2025_, _read-2026_, etc.) and then create a content collection for each shelf to get around this limitation.

```typescript
// src/content/config.ts
import { defineCollection } from "astro:content";
import { goodreadsLoader } from "astro-loader-goodreads";

const currentlyReading = defineCollection({
  loader: goodreadsLoader({
    url: "https://www.goodreads.com/review/list/152185079-sadman-hossain?shelf=currently-reading",
    refreshIntervalDays: 7, // optional parameter; set to only fetch new data once per week
  }),
});

export const collections = { currentlyReading };
```

```astro
---
// src/pages/reading.astro
import { getCollection } from "astro:content";

const books = await getCollection("currentlyReading");
---

<h1>Books I'm Currently Reading</h1>

<div class="book-grid">
  {books.map((book) => (
    <div class="book-card">
      <img src={book.data.book_large_image_url} alt={`Cover of ${book.data.title}`} />
      <h2>{book.data.title}</h2>
      <p class="author">by {book.data.author_name}</p>
      {book.data.user_rating > 0 && (
        <p class="rating">My rating: {book.data.user_rating}/5</p>
      )}
      <a href={book.data.link} target="_blank" rel="noopener noreferrer">
        View on Goodreads
      </a>
    </div>
  ))}
</div>

<style>
  .book-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 2rem;
  }
  .book-card {
    border: 1px solid #eee;
    border-radius: 0.5rem;
    padding: 1rem;
    text-align: center;
  }
  .book-card img {
    max-width: 100%;
    height: auto;
  }
</style>
```

---

### 2. Goodreads User Updates

**To load a Goodreads user's updates feed, use the user's profile URL** (e.g. https://www.goodreads.com/user/show/152185079-sadman-hossain). `astro-loader-goodreads` will convert it to the correct RSS feed URL automatically.

> [!IMPORTANT]
> **The RSS feed for a Goodreads user's updates only includes the last 10 updates by that user.** This means that  `astro-loader-goodreads` cannot retrieve more than 10 updates for any single user.

```typescript
// src/content/config.ts
import { defineCollection } from "astro:content";
import { goodreadsLoader } from "astro-loader-goodreads";

const userUpdates = defineCollection({
  loader: goodreadsLoader({
    url: "https://www.goodreads.com/user/show/152185079-sadman-hossain",
  }),
});

export const collections = { userUpdates };
```

```astro
---
// src/pages/activity.astro
import { getCollection } from "astro:content";

const updates = await getCollection("userUpdates");

// Sort updates by publication date (newest first)
const sortedUpdates = updates.sort((a, b) => 
  new Date(b.data.pubDate).getTime() - new Date(a.data.pubDate).getTime()
);
---

<h1>My Goodreads Activity</h1>

<div class="activity-feed">
  {sortedUpdates.map((update) => {
    const itemData = update.data.itemData;
    
    return (
      <div class="activity-item">
        <p class="date">{new Date(update.data.pubDate).toLocaleDateString()}</p>
        
        {itemData?.type === "ReadStatus" && (
          <div class="read-status">
            <img 
              src={itemData.bookImgUrl} 
              alt={`Cover of ${itemData.bookTitle}`} 
              width="50"
            />
            <div>
              <p>
                <strong>{itemData.readingStatus}</strong> 
                <a href={itemData.bookUrl}>{itemData.bookTitle}</a> 
                by {itemData.bookAuthor}
              </p>
            </div>
          </div>
        )}
        
        {itemData?.type === "Review" && (
          <div class="review">
            <img 
              src={itemData.bookImgUrl} 
              alt={`Cover of ${itemData.bookTitle}`} 
              width="50"
            />
            <div>
              <p>
                Rated <strong>{itemData.rating} stars</strong> for 
                <a href={itemData.bookUrl}>{itemData.bookTitle}</a> 
                by {itemData.bookAuthor}
              </p>
            </div>
          </div>
        )}
        
        {itemData?.type === "CommentReview" && (
          <div class="comment-review">
            <div>
              <p>
                Commented on <a href={itemData.reviewUrl}>{itemData.reviewUser}'s review</a> of 
                <a href={itemData.bookUrl}>{itemData.bookTitle}</a>:
              </p>
              <blockquote>"{itemData.comment}"</blockquote>
            </div>
          </div>
        )}
        
        {/* Add additional item types as needed */}
      </div>
    );
  })}
</div>
```

---

### 3. Goodreads Author Blogs

**To load Goodreads author blog posts, use the author's URL** (e.g. https://www.goodreads.com/author/show/3389.Stephen_King). `astro-loader-goodreads` will append the necessary parameters to fetch the blog RSS feed automatically.

```typescript
// src/content/config.ts
import { defineCollection } from "astro:content";
import { goodreadsLoader } from "astro-loader-goodreads";

const authorBlog = defineCollection({
  loader: goodreadsLoader({
    url: "https://www.goodreads.com/author/show/3389.Stephen_King",
  }),
});

export const collections = { authorBlog };
```

```astro
---
// src/pages/author-updates.astro
import { getCollection } from "astro:content";

const posts = await getCollection("authorBlog");
---

<h1>Latest Updates from Stephen Kingn</h1>

<div class="blog-posts">
  {posts.map((post) => (
    <article class="blog-post">
      <h2>{post.data.title}</h2>
      <p class="date">Published: {new Date(post.data.pubDate).toLocaleDateString()}</p>
      {post.data.content && (
        <div class="content" set:html={post.data.content} />
      )}
      <a href={post.data.link}>Read on Goodreads</a>
    </article>
  ))}
</div>
```

---

## Data Schema

### Overview

The astro-loader-goodreads package provides three main schemas:

1. [`BookSchema`](#1-bookschema) - For books from Goodreads shelves
2. [`UserUpdateSchema`](#2-userupdateschema) - For user updates (with various activity types)
3. [`AuthorBlogSchema`](#3-authorblogschema) - For author blog posts

### 1. `BookSchema`

This schema is used when loading data from a Goodreads shelf.

```typescript
export const BookSchema = z.object({
  id: z.coerce.string(),
  title: z.coerce.string(),
  guid: z.string(),
  pubDate: z.string(),
  link: z.string(),
  book_id: z.coerce.string(),
  book_image_url: z.string(),
  book_small_image_url: z.string(),
  book_medium_image_url: z.string(),
  book_large_image_url: z.string(),
  book_description: z.string(),
  num_pages: z.string().optional(),
  author_name: z.string(),
  isbn: z.coerce.string(),
  user_name: z.string(),
  user_rating: z.number(),
  user_read_at: z.string(),
  user_date_added: z.string(),
  user_date_created: z.string(),
  user_shelves: z.string().optional(),
  user_review: z.string().optional(),
  average_rating: z.number(),
  book_published: z.coerce.string(),
});
```

#### Book Fields

| Field | Description |
|-------|-------------|
| `id` | Unique identifier for the book |
| `title` | Book title |
| `guid` | Global unique identifier for this entry |
| `pubDate` | Publication date of this entry in the feed |
| `link` | URL to the book's Goodreads page |
| `book_id` | Goodreads ID for the book |
| `book_image_url` | URL to the book cover image |
| `book_small_image_url` | URL to small version of book cover (50×75 px) |
| `book_medium_image_url` | URL to medium version of book cover (65×98 px) |
| `book_large_image_url` | URL to large version of book cover (316×475 px) |
| `book_description` | Description/synopsis of the book |
| `num_pages` | Number of pages in the book (optional) |
| `author_name` | Name of the book's author |
| `isbn` | International Standard Book Number |
| `user_name` | Username of who added the book to their shelf |
| `user_rating` | Rating given by the user (0-5) |
| `user_read_at` | Date when the user finished reading the book |
| `user_date_added` | Date when the book was added to the user's shelf |
| `user_date_created` | Date when this entry was created |
| `user_shelves` | List of shelves the user assigned to this book (optional) |
| `user_review` | User's review of the book (optional) |
| `average_rating` | Average rating on Goodreads |
| `book_published` | Book's original publication date |

### 2. `UserUpdateSchema`

This schema is used when loading data from a Goodreads user's updates feed.

```typescript
export const UserUpdateSchema = z.object({
  id: z.string(),
  title: z.string(),
  link: z.string().optional(),
  description: z.string().optional(),
  pubDate: z.string(),
  itemType: z.string().optional(),
  itemData: ItemDataSchema.optional()
});
```

### `UserUpdateSchema` Item Types

The `itemData` field contains a discriminated union based on the `type` field:

#### `AuthorFollowing`

When a user follows an author:

```typescript
{
  type: "AuthorFollowing",
  followId: string,
  userUrl: string, 
  authorId: string
}
```

#### `UserStatus`

When a user reports progress on a book:

```typescript
{
  type: "UserStatus",
  userUrl: string,
  percentRead: string,
  bookUrl: string,
  bookTitle: string,
  bookAuthor: string,
  bookImgUrl: string
}
```

#### `ReadStatus`

When a user changes their reading status:

```typescript
{
  type: "ReadStatus",
  userUrl: string, 
  readingStatus: string, // 'started reading', 'wants to read', or 'finished reading'
  bookUrl: string,
  bookTitle: string,
  bookAuthor: string,
  bookImgUrl: string
}
```

#### `Review`

When a user posts a review:

```typescript
{
  type: "Review",
  userUrl: string,
  rating: number,
  bookUrl: string,
  bookTitle: string,
  bookAuthor: string,
  bookImgUrl: string
}
```

#### `LikeReview`

When a user likes someone's review:

```typescript
{
  type: "LikeReview",
  userUrl: string,
  reviewUrl: string,
  reviewUser: string,
  bookUrl: string,
  bookTitle: string,
  bookImgUrl: string
}
```

#### `LikeReadStatus`

When a user likes someone's read status:

```typescript
{
  type: "LikeReadStatus",
  userUrl: string,
  readStatusUser: string,
  readStatusUserImgUrl: string,
  readStatus: string,
  bookUrl: string,
  bookTitle: string
}
```

#### `CommentStatus`

When a user comments on a status:

```typescript
{
  type: "CommentStatus",
  userUrl: string,
  statusUrl: string,
  statusUser: string,
  comment: string
}
```

#### `CommentReview`

When a user comments on a review:

```typescript
{
  type: "CommentReview",
  userUrl: string,
  reviewUrl: string,
  reviewUser: string,
  bookUrl: string,
  bookTitle: string,
  bookAuthor: string,
  comment: string
}
```

### 3. `AuthorBlogSchema`

This schema is used when loading data from a Goodreads author's blog.

```typescript
export const AuthorBlogSchema = z.object({
  id: z.string(),
  title: z.string(),
  link: z.string(),
  description: z.string(),
  pubDate: z.string(),
  author: z.string().optional(),
  content: z.string().optional(),
});
```

### `AuthorBlogSchema` Fields

| Field | Description |
|-------|-------------|
| `id` | Unique identifier for the blog post |
| `title` | Blog post title |
| `link` | URL to the blog post |
| `description` | Raw HTML description of the blog post |
| `pubDate` | Publication date |
| `author` | Author's name (if available) |
| `content` | Main content of the blog post (if available) |

---

## License

`astro-loader-goodreads` is [MIT licensed](https://github.com/sadmanca/astro-loader-goodreads/blob/main/LICENSE).

---

Built with ♥ by [@sadmanca](https://github.com/sadmanca)!

[npm-version-src]: https://img.shields.io/npm/v/astro-loader-goodreads?style=flat&logo=npm&colorA=ea2039&colorB=2e2e2e
[npm-version-href]: https://npmjs.com/package/astro-loader-goodreads
[npm-downloads-src]: https://img.shields.io/npm/dm/astro-loader-goodreads?style=flat&logo=npm&colorA=ea2039&colorB=2e2e2e
[npm-downloads-href]: https://npmjs.com/package/astro-loader-goodreads
[license-src]: https://img.shields.io/badge/license-MIT-080f12?style=flat&colorA=2e2e2e&colorB=blue
[license-href]: https://github.com/sadmanca/astro-loader-goodreads/blob/main/LICENSE
[jsdocs-src]: https://img.shields.io/badge/jsdocs-astro--loader--goodreads-080f12?style=flat&colorA=2e2e2e&colorB=525252
[jsdocs-href]: https://www.jsdocs.io/package/astro-loader-goodreads
[bundle-src]: https://img.shields.io/bundlephobia/minzip/astro-loader-goodreads?style=flat&colorA=2e2e2e&colorB=1f8f00
[bundle-href]: https://bundlephobia.com/result?p=astro-loader-goodreads

[astro-src]: https://img.shields.io/badge/Astro-0690FA?style=flat&logo=astro&logoColor=ffffff&color=5e15a1
[astro-href]: https://astro.build
[goodreads-src]: https://img.shields.io/badge/Goodreads-0690FA?style=flat&logo=goodreads&logoColor=7e470f&color=e9e4d0
[goodreads-href]: https://goodreads.com
[how-to-use-src]: https://img.shields.io/badge/How_to_use_Goodreads_data_in_Astro-0690FA?style=flat&logo=amp&logoColor=6854ff&color=2e2e2e
[how-to-use-href]: https://sadman.ca/posts/how-to-use-goodreads-data-in-astro/