<div align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/shepherdjerred/monorepo/main/packages/webring/assets/logo-dark.png">
    <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/shepherdjerred/monorepo/main/packages/webring/assets/logo-light.png">
    <img alt="webring logo" src="https://raw.githubusercontent.com/shepherdjerred/monorepo/main/packages/webring/assets/logo-light.png" height=150>
  </picture>

[![webring](https://img.shields.io/npm/v/webring.svg)](https://www.npmjs.com/package/webring)

`webring` fetches the latest updates from your favorite RSS feeds.

This project is actively maintained. If you have a feature request or need help, please [create an issue](https://github.com/shepherdjerred/monorepo/issues/new).

</div>

## Installation

```bash
npm i webring
```

## Features

- Written in TypeScript
- Caching
- HTML sanitization and truncation

## Quick Start

This library is intended to be used with a static site generator. I use it with [Astro](https://astro.build/) on my personal website — see [`packages/sjer.red/src/webring.ts`](https://github.com/shepherdjerred/monorepo/blob/main/packages/sjer.red/src/webring.ts) for a real consumer.

```typescript
import { run } from "webring";

const result = await run({
  sources: [
    {
      url: "https://drewdevault.com/blog/index.xml",
      title: "Drew DeVault",
    },
    {
      url: "https://danluu.com/atom.xml",
      title: "Dan Luu",
    },
    {
      url: "https://jakelazaroff.com/rss.xml",
      title: "Jake Lazaroff",
    },
  ],
  number: 3,
  truncate: 300,
});

console.log(result);
// [
//   {
//     title: 'A discussion of discussions on AI bias',
//     url: 'https://danluu.com/ai-bias/',
//     date: 2024-06-16T00:00:00.000Z,
//     source: { url: 'https://danluu.com/atom.xml', title: 'Dan Luu' },
//     preview: `There've been regular viral stories about ML/AI bias with LLMs and generative AI for the past couple years. One thing I find interesting about discussions of bias is how different the reaction is in the LLM and generative AI case when compared to "classical" bugs in cases where there's a clear bug. ...`
//   },
//   {
//     title: 'Writing a Unix clone in about a month',
//     url: 'https://drewdevault.com/2024/05/24/2024-05-24-Bunnix.html',
//     date: 2024-05-24T00:00:00.000Z,
//     source: {
//       url: 'https://drewdevault.com/blog/index.xml',
//       title: 'Drew DeVault'
//     },
//     preview: 'I needed a bit of a break from “real work” recently, so I started a new programming project that was low-stakes and purely recreational. On April 21st, I set out to see how much of a Unix-like operating system for x86_64 targets that I could put together in about a month. The result is Bunnix. Not i...'
//   },
//   {
//     title: 'The Web Component Success Story',
//     url: 'https://jakelazaroff.com/words/the-web-component-success-story/',
//     date: 2024-01-29T00:00:00.000Z,
//     source: { url: 'https://jakelazaroff.com/rss.xml', title: 'Jake Lazaroff' },
//     preview: "Web components won't take web development by storm, or show us the One True Way to build websites. What they will do is let us collectively build a rich ecosystem of dynamic components that work with any web stack."
//   }
// ]
```

## Configuration

`webring` is configured by passing a `Configuration` object into `run`. The
exported `Configuration` type is the Zod _output_ type, so the schema's
defaults for `number` and `truncate` are already applied in the type: those two
fields are **required** on the object you pass in, alongside `sources`. Only
`cache` and `shuffle` may be omitted.

| Key                            | Type                 | Required | Description                                                           |
| ------------------------------ | -------------------- | -------- | --------------------------------------------------------------------- |
| `sources`                      | `Source[]`           | yes      | Feeds to fetch. Each has a `url`, a `title`, and an optional `filter` |
| `number`                       | `number`             | yes      | Return the n latest updates across all sources                        |
| `truncate`                     | `number`             | yes      | Preview length in characters, applied after HTML sanitization         |
| `cache`                        | `CacheConfiguration` | no       | Enable caching by providing this object; omit it to disable caching   |
| `cache.cache_file`             | `string`             | no       | File used as the cache. Defaults to `"cache.json"`                    |
| `cache.cache_duration_minutes` | `number`             | no       | How long a cached result is reused. Defaults to `60`                  |
| `shuffle`                      | `boolean`            | no       | Randomize the output order. Omitted behaves as `false`                |

The `cache.*` defaults above are the only ones `run` fills in at runtime: it
parses the object with `CachedConfigurationSchema`, which succeeds only when
`cache` is present. Without a `cache` key the object is used as given, so
`number` and `truncate` must carry real values rather than relying on the
schema.

Full generated API documentation: [webring.sjer.red](https://webring.sjer.red) (e.g. [`Configuration`](https://webring.sjer.red/types/Configuration.html)).

## Example

An example of using this project with Astro is located in `example`. The relevant file is [`src/pages/blog/[...slug].astro`](https://github.com/shepherdjerred/monorepo/blob/main/packages/webring/example/src/pages/blog/%5B...slug%5D.astro).

```typescript
---
import { type CollectionEntry, getCollection } from "astro:content";
import BlogPost from "../../layouts/BlogPost.astro";
import { type Configuration, type Result, run } from "webring";

export async function getStaticPaths() {
  const posts = await getCollection("blog");
  return posts.map((post) => ({
    params: { slug: post.slug },
    props: post,
  }));
}
type Props = CollectionEntry<"blog">;

const post = Astro.props;
const { Content } = await post.render();

export const config: Configuration = {
  sources: [
    {
      url: "https://drewdevault.com/blog/index.xml",
      title: "Drew DeVault",
    },
    {
      url: "https://danluu.com/atom.xml",
      title: "Dan Luu",
    },
    {
      url: "https://jakelazaroff.com/rss.xml",
      title: "Jake Lazaroff",
    },
  ],
  number: 3,
  truncate: 300,
  cache: {
    cache_file: "webring.json",
    cache_duration_minutes: 60,
  },
};

export const result: Result = await run(config);
---

<BlogPost {...post.data}>
  <Content />
  <h2>Posts from blogs I read</h2>
  <ul>
    {
      result.map((post) => (
        <li>
          <a href={post.url}>{post.title}</a>
        </li>
      ))
    }
  </ul>
</BlogPost>
```

## Inspiration

- https://github.com/lukehsiao/openring-rs
- https://git.sr.ht/~sircmpwn/openring
