npm package: @rmdes/indiekit-endpoint-blogroll
Description: Blogroll endpoint for Indiekit. Aggregates blog feeds from OPML, JSON feeds, or manual entry.
Author: Ricardo Mendes
Latest version: 1.0.24

# Blogroll Endpoint for Indiekit

An Indiekit plugin that provides a comprehensive blogroll management system with feed aggregation, admin UI, and public API.

## Features

- **Multiple Source Types:** Import blogs from OPML files/URLs, Microsub subscriptions, or add manually
- **Background Feed Fetching:** Automatically syncs blogs and caches recent items
- **Microsub Integration:** Mirror your Microsub subscriptions as a blogroll (zero duplication)
- **Admin UI:** Manage sources, blogs, and view recent activity
- **Public JSON API:** Read-only endpoints for frontend integration
- **OPML Export:** Export your blogroll as OPML (all or by category)
- **Feed Discovery:** Auto-discover feeds from website URLs
- **Item Retention:** Automatic cleanup of old items (encourages fresh content discovery)

## Installation

```bash
npm install @rmdes/indiekit-endpoint-blogroll
```

## Configuration

Add to your `indiekit.config.js`:

```javascript
import BlogrollEndpoint from "@rmdes/indiekit-endpoint-blogroll";

export default {
  plugins: [
    new BlogrollEndpoint({
      mountPath: "/blogrollapi",    // Admin UI and API base path
      syncInterval: 3600000,         // 1 hour (in milliseconds)
      maxItemsPerBlog: 50,           // Items to fetch per blog
      maxItemAge: 7,                 // Days - older items auto-deleted
      fetchTimeout: 15000            // 15 seconds per feed fetch
    })
  ]
};
```

## Requirements

- **Indiekit:** `>=1.0.0-beta.25`
- **MongoDB:** Required for data storage
- **Optional:** `@rmdes/indiekit-endpoint-microsub` for Microsub integration

## Usage

### Admin UI

Navigate to `/blogrollapi` in your Indiekit instance to access:

- **Dashboard:** View sync status, blog counts, recent activity
- **Sources:** Manage OPML and Microsub sources
- **Blogs:** Add/edit/delete individual blogs, refresh feeds
- **Manual Sync:** Trigger immediate sync or clear and resync

### Source Types

1. **OPML URL:** Point to a public OPML file (e.g., your feed reader's export)
2. **OPML File:** Paste OPML XML directly into the form
3. **Microsub:** Import subscriptions from your Microsub channels
4. **Manual:** Add individual blog feeds one at a time

### Public API

All API endpoints return JSON (except OPML export which returns XML).

**List Blogs**
```
GET /blogrollapi/api/blogs?category=Tech&limit=100&offset=0
```

**Get Blog with Recent Items**
```
GET /blogrollapi/api/blogs/:id
```

**List Items Across All Blogs**
```
GET /blogrollapi/api/items?blog=<id>&category=Tech&limit=50&offset=0
```

**List Categories**
```
GET /blogrollapi/api/categories
```

**Sync Status**
```
GET /blogrollapi/api/status
```

**Export OPML**
```
GET /blogrollapi/api/opml                  (all blogs)
GET /blogrollapi/api/opml/:category        (specific category)
```

### Example Response

**GET /blogrollapi/api/blogs**
```json
{
  "items": [
    {
      "id": "507f1f77bcf86cd799439011",
      "title": "Example Blog",
      "description": "A great blog about tech",
      "feedUrl": "https://example.com/feed",
      "siteUrl": "https://example.com",
      "feedType": "rss",
      "category": "Tech",
      "tags": ["programming", "web"],
      "photo": "https://example.com/icon.png",
      "status": "active",
      "itemCount": 25,
      "pinned": false,
      "lastFetchAt": "2026-02-13T10:30:00.000Z"
    }
  ],
  "total": 42,
  "hasMore": true
}
```

**GET /blogrollapi/api/items**
```json
{
  "items": [
    {
      "id": "507f1f77bcf86cd799439011",
      "url": "https://example.com/post/hello",
      "title": "Hello World",
      "summary": "My first blog post...",
      "published": "2026-02-13T10:00:00.000Z",
      "isFuture": false,
      "author": { "name": "Jane Doe" },
      "photo": ["https://example.com/image.jpg"],
      "categories": ["announcement"],
      "blog": {
        "id": "507f1f77bcf86cd799439011",
        "title": "Example Blog",
        "siteUrl": "https://example.com",
        "category": "Tech",
        "photo": "https://example.com/icon.png"
      }
    }
  ],
  "hasMore": false
}
```

## Microsub Integration

If you have `@rmdes/indiekit-endpoint-microsub` installed, the blogroll can mirror your subscriptions:

1. Create a Microsub source in the admin UI
2. Select specific channels or sync all channels
3. Add a category prefix (optional) to distinguish Microsub blogs
4. Blogs and items are referenced, not duplicated

**Benefits:**
- Zero data duplication - items are served directly from Microsub
- Automatic orphan cleanup when feeds are unsubscribed
- Webhook support for real-time updates

## Background Sync

The plugin automatically syncs in the background:

1. **Initial Sync:** Runs 15 seconds after server startup
2. **Periodic Sync:** Runs every `syncInterval` milliseconds (default 1 hour)
3. **What it Does:**
   - Syncs enabled sources (OPML/Microsub)
   - Fetches new items from active blogs
   - Deletes items older than `maxItemAge` days
   - Updates sync statistics

**Manual Sync:**
- Trigger from the dashboard
- Use `POST /blogrollapi/sync` (protected endpoint)
- Use `POST /blogrollapi/clear-resync` to clear and resync all

## Feed Discovery

The plugin includes auto-discovery for finding feeds from website URLs:

```javascript
// In the admin UI, when adding a blog, paste a website URL
// The plugin will:
// 1. Check <link rel="alternate"> tags in HTML
// 2. Try common feed paths (/feed, /rss, /atom.xml, etc.)
// 3. Suggest discovered feeds
```

## Item Retention

By default, items older than 7 days are automatically deleted during sync. This encourages discovery of fresh content rather than archiving everything.

**To Change Retention:**
```javascript
new BlogrollEndpoint({
  maxItemAge: 30  // Keep items for 30 days instead
})
```

## Blog Status

- **active:** Blog is working, fetching items normally
- **error:** Last fetch failed (see `lastError` for details)
- **deleted:** Soft-deleted, won't be recreated by sync

## Navigation

The plugin adds itself to Indiekit's navigation:

- **Menu Item:** "Blogroll" (requires database)
- **Shortcut:** Bookmark icon in admin dashboard

## Security

- **Protected Routes:** Admin UI and management endpoints require authentication
- **Public Routes:** Read-only API endpoints are publicly accessible
- **XSS Prevention:** Feed content is sanitized with `sanitize-html`
- **Feed Discovery:** Protected to prevent abuse (requires authentication)

## Supported Feed Formats

- RSS 2.0
- Atom 1.0
- JSON Feed 1.0

## Contributing

Report issues at: https://github.com/rmdes/indiekit-endpoint-blogroll/issues

## License

MIT