RSS feed reader endpoint for Indiekit. Aggregates multiple feeds, caches in MongoDB, displays on frontend.
  • JavaScript 87.7%
  • Nunjucks 8.9%
  • CSS 3.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Ricardo 13dd9c4b41 fix: the content field prefilled the template that strips formatting
The publish form hardcoded "{{description}}" as the content field's
fallback, whatever the post type. That placeholder runs the item through
stripHtml, so an article feed saved straight from the form published its
items as one flat block of text: links, lists and code spans all gone,
and markdown then applied smart quotes to what used to be code.

jf2-builder already knows the right default per post type -
defaultContent() returns {{content}} for an article - and
applyFeedSettings already falls back to it when content is empty, using
the post type actually submitted. The form was overriding that with a
value the user never chose.

The field is now empty by default and shows the post type's default as
a placeholder, so the server-side default applies for every type.

Existing feeds keep whatever they have stored; set the field to
{{content}} to switch one over.
2026-09-06 12:07:15 +02:00
assets fix: nest the publish disclosure in the feed's own column 2026-09-05 22:13:55 +02:00
lib fix: the content field prefilled the template that strips formatting 2026-09-06 12:07:15 +02:00
locales feat: richer content, a post date choice, and a way to start over 2026-09-05 23:53:36 +02:00
test fix: the content field prefilled the template that strips formatting 2026-09-06 12:07:15 +02:00
views fix: the content field prefilled the template that strips formatting 2026-09-06 12:07:15 +02:00
.gitignore fix: never prune the newest items of a feed 2026-09-05 16:29:53 +02:00
CLAUDE.md chore: release 1.1.0 2026-09-05 21:01:14 +02:00
index.js feat: space out the posts in a publish batch 2026-09-06 01:15:03 +02:00
package.json fix: the content field prefilled the template that strips formatting 2026-09-06 12:07:15 +02:00
README.md chore: release 1.1.0 2026-09-05 21:01:14 +02:00
renovate.json chore: enable Renovate with grouped, review-only updates 2026-09-04 16:42:22 +02:00

@rmdes/indiekit-endpoint-rss

RSS feed reader endpoint for Indiekit. Aggregates multiple feeds, caches in MongoDB, displays on frontend.

Features

  • Multi-Feed Aggregation - Subscribe to unlimited RSS/Atom/JSON feeds
  • Background Sync - Automatic updates every 15 minutes (configurable)
  • MongoDB Caching - Fast access to cached feed items
  • Admin Dashboard - Add/remove feeds, view recent items, trigger manual sync
  • Public JSON API - Read-only endpoints for frontend display
  • Feed Management - Enable/disable feeds, view sync status and errors
  • Automatic Cleanup - Prunes items older than 30 days (configurable)
  • Format Support - RSS 2.0, Atom, JSON Feed, Google Reader API (FreshRSS)
  • Concurrency Control - Fetches 3 feeds in parallel to avoid overwhelming servers
  • HTML Sanitization - Strips dangerous tags, keeps basic formatting

Installation

npm install @rmdes/indiekit-endpoint-rss

Configuration

Add to your indiekit.config.js:

import RssEndpoint from "@rmdes/indiekit-endpoint-rss";

export default {
  plugins: [
    new RssEndpoint({
      mountPath: "/rssapi",          // Default: /rssapi
      syncInterval: 900_000,         // 15 minutes (in milliseconds)
      maxItemsPerFeed: 50,           // Max items per feed to cache
      fetchTimeout: 10_000,          // 10 second timeout per feed
      maxConcurrentFetches: 3,       // Parallel feed fetches
      retentionDays: 30,             // Days to keep items
      minItemsPerFeed: 10,           // Newest items always kept, whatever their age
      maxPostsPerCycle: 10           // Max posts created per sync cycle
    })
  ],
  // MongoDB database is REQUIRED
  mongodbUrl: process.env.MONGODB_URL
};

Usage

Admin Dashboard

Navigate to /rssapi (or your configured mountPath) to access the admin dashboard.

Features:

  • View all subscribed feeds
  • Add new feeds by URL
  • Enable/disable feeds
  • Remove feeds
  • View recent items
  • Manual sync trigger
  • Clear cache and re-sync

Adding Feeds

Via Admin UI:

  1. Go to /rssapi
  2. Enter feed URL in the "Add Feed" form
  3. Click "Add Feed"

Via API:

curl -X POST https://yoursite.com/rssapi/api/feeds \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"url": "https://example.com/feed.xml"}'

Public API Endpoints

List All Feeds:

GET /rssapi/api/feeds

List Feed Items (Paginated):

GET /rssapi/api/items?page=1&limit=20&feedId=FEED_ID&includeContent=true

Get Single Item:

GET /rssapi/api/items/:id

Sync Status:

GET /rssapi/api/status

Example: Display Items in Eleventy

// _data/rssItems.js
export default async function () {
  const response = await fetch("https://yoursite.com/rssapi/api/items?limit=10");
  const data = await response.json();
  return data.items;
}
<!-- index.njk -->
<h2>Latest News</h2>
{% for item in rssItems %}
  <article>
    <h3><a href="{{ item.link }}">{{ item.title }}</a></h3>
    <p class="meta">{{ item.feedTitle }} - {{ item.pubDate | date("PPp") }}</p>
    <p>{{ item.description }}</p>
    {% if item.imageUrl %}
      <img src="{{ item.imageUrl }}" alt="">
    {% endif %}
  </article>
{% endfor %}

Supported Feed Formats

  • RSS 2.0 - Standard blog feeds
  • Atom - GitHub releases, podcasts, etc.
  • JSON Feed - jsonfeed.org spec (v1.0, v1.1)
  • Google Reader API - FreshRSS, NewsBlur, Miniflux (use ?f=json or ?f=greader)

FreshRSS Integration

This plugin has special support for FreshRSS (and other Google Reader API compatible aggregators):

  1. Get your FreshRSS API feed URL (Settings → Authentication → API feed URL)
  2. Add ?f=json or ?f=greader to the URL
  3. Add the URL to Indiekit RSS endpoint

Example:

https://freshrss.example.com/api/greader.php/reader/api/0/stream/contents/reading-list?f=json&n=50&user=admin&apiKey=YOUR_API_KEY

Features:

  • Preserves original feed source (via origin object)
  • Filters out internal FreshRSS categories
  • Extracts images from media content
  • Handles FreshRSS timestamps (Unix seconds, microseconds)

API Reference

GET /api/feeds

List all feeds.

Response:

{
  "feeds": [
    {
      "id": "507f1f77bcf86cd799439011",
      "url": "https://example.com/feed.xml",
      "title": "Example Blog",
      "siteUrl": "https://example.com",
      "description": "A blog about things",
      "imageUrl": "https://example.com/icon.png",
      "enabled": true,
      "addedAt": "2025-02-01T12:00:00.000Z",
      "lastFetchedAt": "2025-02-13T14:30:00.000Z",
      "lastError": null,
      "itemCount": 42
    }
  ],
  "total": 1
}

GET /api/items

List feed items with pagination.

Query Parameters:

  • page (number, default: 1) - Page number
  • limit (number, default: 20, max: 100) - Items per page
  • feedId (string, optional) - Filter by feed ID
  • includeContent (boolean, default: false) - Include full HTML content

Response:

{
  "items": [
    {
      "id": "507f1f77bcf86cd799439012",
      "feedId": "507f1f77bcf86cd799439011",
      "feedTitle": "Example Blog",
      "guid": "https://example.com/post-1",
      "title": "Hello World",
      "link": "https://example.com/post-1",
      "description": "A short summary...",
      "author": "John Doe",
      "pubDate": "2025-02-13T12:00:00.000Z",
      "imageUrl": "https://example.com/image.jpg",
      "categories": ["tech", "web"],
      "fetchedAt": "2025-02-13T14:30:00.000Z",
      "sourceTitle": "Original Feed Title",
      "sourceUrl": "https://original-source.com"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 100,
    "totalPages": 5,
    "hasNext": true,
    "hasPrev": false
  }
}

GET /api/items/:id

Get a single item by ID.

Response:

{
  "item": {
    "id": "507f1f77bcf86cd799439012",
    "feedId": "507f1f77bcf86cd799439011",
    "feedTitle": "Example Blog",
    "title": "Hello World",
    "link": "https://example.com/post-1",
    "description": "A short summary...",
    "content": "<p>Full sanitized HTML content...</p>",
    "author": "John Doe",
    "pubDate": "2025-02-13T12:00:00.000Z",
    "imageUrl": "https://example.com/image.jpg",
    "categories": ["tech", "web"],
    "enclosure": {
      "url": "https://example.com/podcast.mp3",
      "type": "audio/mpeg",
      "length": 12345678
    },
    "fetchedAt": "2025-02-13T14:30:00.000Z"
  }
}

GET /api/status

Get sync status and statistics.

Response:

{
  "status": "idle",
  "lastSync": "2025-02-13T14:30:00.000Z",
  "nextSync": "2025-02-13T14:45:00.000Z",
  "lastError": null,
  "stats": {
    "feedsCount": 5,
    "enabledFeedsCount": 4,
    "itemsCount": 200,
    "lastFeedsProcessed": 4,
    "lastItemsAdded": 12
  },
  "config": {
    "syncInterval": 900000,
    "maxItemsPerFeed": 50
  }
}

Requirements

  • Indiekit >= 1.0.0-beta.25
  • MongoDB database
  • Node.js >= 20

License

MIT

Author

Ricardo Mendes - https://rmendes.net

Repository

https://github.com/rmdes/indiekit-endpoint-rss