docs: add CLAUDE.md and README.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ricardo
2026-02-13 18:22:45 +01:00
parent 02bfff7ce9
commit e5ffc5b8a1
2 changed files with 904 additions and 0 deletions

658
CLAUDE.md Normal file
View File

@@ -0,0 +1,658 @@
# CLAUDE.md - indiekit-endpoint-microsub
## Package Overview
`@rmdes/indiekit-endpoint-microsub` is a comprehensive Microsub social reader plugin for Indiekit. It implements the Microsub protocol for subscribing to feeds, organizing them into channels, and reading posts in a unified timeline interface. The plugin provides both a Microsub API endpoint (for compatible clients) and a built-in web-based reader UI.
**Package Name:** `@rmdes/indiekit-endpoint-microsub`
**Version:** 1.0.28
**Type:** ESM module
**Entry Point:** `index.js`
## Core Features
- **Microsub Protocol Implementation**: Full Microsub API (channels, timeline, follow/unfollow, mute/block, search, preview)
- **Web Reader UI**: Built-in Nunjucks-based reader interface with channel navigation, timeline view, and composition
- **Multi-Format Feed Support**: RSS, Atom, JSON Feed, h-feed (microformats), with fallback feed discovery
- **Real-Time Updates**: WebSub (PubSubHubbub) support for instant notifications
- **Adaptive Polling**: Tiered polling system (2 minutes to 17+ hours) based on feed update frequency
- **Read State Management**: Per-user read tracking with automatic cleanup (keeps last 30 read items per channel)
- **Feed Discovery**: Automatic discovery of feeds from websites (RSS/Atom link tags, JSON Feed, h-feed)
- **Webmention Receiving**: Accepts webmentions for posts in the timeline
- **Media Proxy**: Proxies external images through local endpoint for privacy and caching
- **Blogroll Integration**: Optionally syncs feed subscriptions with `@rmdes/indiekit-endpoint-blogroll`
- **Compose UI**: Post replies, likes, reposts, and bookmarks via Micropub
## Architecture
### Data Flow
```
┌──────────────────────────────────────────────────────────────┐
│ FEED INGESTION │
├──────────────────────────────────────────────────────────────┤
│ Scheduler (60s interval) │
│ ↓ │
│ getFeedsToFetch() → processFeedBatch() │
│ ↓ │
│ fetchFeed() → parseFeed() → normalizeItems() │
│ ↓ │
│ addItem() → MongoDB (dedup by uid) │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ READER UI │
├──────────────────────────────────────────────────────────────┤
│ /microsub/reader/channels → List channels │
│ /microsub/reader/channels/:uid → Channel timeline │
│ /microsub/reader/channels/:uid/feeds → Manage subscriptions │
│ /microsub/reader/compose → Post via Micropub │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ MICROSUB API │
├──────────────────────────────────────────────────────────────┤
│ GET/POST /microsub?action=channels → Channel list │
│ GET/POST /microsub?action=timeline → Timeline items │
│ POST /microsub?action=follow → Subscribe to feed │
│ POST /microsub?action=unfollow → Unsubscribe │
│ POST /microsub?action=mute/block → Filter content │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ REAL-TIME UPDATES │
├──────────────────────────────────────────────────────────────┤
│ WebSub Hub → POST /microsub/websub/:id → processWebsubUpdate│
│ Webmention → POST /microsub/webmention → addNotification │
└──────────────────────────────────────────────────────────────┘
```
## MongoDB Collections
### `microsub_channels`
Stores user channels for organizing feeds.
```javascript
{
_id: ObjectId,
uid: "unique-short-id", // Generated 8-char alphanumeric
name: "Technology",
userId: "user-id", // For multi-user support
order: 0, // Display order
settings: {
excludeTypes: ["repost"], // Filter by post type
excludeRegex: "/spam|ads/i" // Filter by regex
},
createdAt: "2026-02-13T...",
updatedAt: "2026-02-13T..."
}
```
**Special Channel**: `uid: "notifications"` (order: -1, always first) receives webmentions and mentions.
**Indexes:**
- `{ uid: 1 }` - Unique channel lookup
- `{ userId: 1, order: 1 }` - Sorted channel list per user
### `microsub_feeds`
Stores feed subscriptions and polling metadata.
```javascript
{
_id: ObjectId,
channelId: ObjectId, // References microsub_channels
url: "https://example.com/feed",
title: "Example Blog",
photo: "https://example.com/icon.png",
tier: 1, // Polling tier (0-10)
unmodified: 0, // Consecutive unchanged fetches
nextFetchAt: Date, // When to poll next (kept as Date for query)
lastFetchedAt: "2026-02-13T...", // ISO string
status: "active" | "error",
lastError: "HTTP 404",
lastErrorAt: "2026-02-13T...",
consecutiveErrors: 0,
itemCount: 42,
websub: {
hub: "https://hub.example/",
topic: "https://example.com/feed",
secret: "random-secret",
leaseSeconds: 432000,
expiresAt: Date
},
createdAt: "2026-02-13T...",
updatedAt: "2026-02-13T..."
}
```
**Polling Tiers:**
- Tier 0: 1 minute
- Tier 1: 2 minutes
- Tier 2: 4 minutes
- Tier 3: 8 minutes
- ...
- Tier 10: 1024 minutes (~17 hours)
**Tier Adjustment:**
- Content changed: tier - 1 (faster polling)
- Unchanged 2x: tier + 1 (slower polling)
**Indexes:**
- `{ channelId: 1, url: 1 }` - Prevent duplicate subscriptions
- `{ nextFetchAt: 1 }` - Scheduler query
### `microsub_items`
Stores timeline items (posts/entries).
```javascript
{
_id: ObjectId,
channelId: ObjectId,
feedId: ObjectId,
uid: "https://example.com/post/123", // Canonical URL or GUID
type: "entry" | "event" | "review",
url: "https://example.com/post/123",
name: "Post Title",
content: {
text: "Plain text...",
html: "<p>HTML content...</p>"
},
summary: "Short description",
published: Date, // Kept as Date for sorting
updated: Date,
author: {
name: "Author Name",
url: "https://author.example/",
photo: "https://author.example/photo.jpg"
},
category: ["tag1", "tag2"],
photo: ["https://example.com/img.jpg"],
video: ["https://example.com/vid.mp4"],
audio: ["https://example.com/aud.mp3"],
likeOf: ["https://liked-post.example/"],
repostOf: ["https://repost.example/"],
bookmarkOf: ["https://bookmark.example/"],
inReplyTo: ["https://reply-to.example/"],
source: { // Metadata about feed source
title: "Example Blog",
url: "https://example.com"
},
readBy: ["user-id"], // Array of user IDs who read this
createdAt: "2026-02-13T..."
}
```
**Read State:** Items are marked read by adding userId to `readBy` array. Old read items are auto-deleted (keeps last 30 per channel).
**Indexes:**
- `{ channelId: 1, uid: 1 }` - Unique (prevents duplicates)
- `{ channelId: 1, published: -1 }` - Timeline queries
- `{ feedId: 1 }` - Feed-specific queries
- `{ channelId: 1, url: 1 }` - URL-based mark_read operations
- Text index on `name`, `content.text`, `content.html`, `summary`, `author.name`
### `microsub_notifications`
Special items collection for notifications channel (webmentions, mentions).
**Same schema as `microsub_items`**, stored in the notifications channel.
### `microsub_muted`
Muted URLs (hide posts from specific URLs).
```javascript
{
_id: ObjectId,
userId: "user-id",
url: "https://muted-site.example/",
createdAt: "2026-02-13T..."
}
```
### `microsub_blocked`
Blocked authors (delete all posts from author URL).
```javascript
{
_id: ObjectId,
userId: "user-id",
authorUrl: "https://blocked-author.example/",
createdAt: "2026-02-13T..."
}
```
## Key Files and Modules
### Core Entry Point
**`index.js`**
- Exports `MicrosubEndpoint` class
- Defines routes, navigation items, mount path
- Initializes MongoDB collections, scheduler, indexes, cleanup
- Registers public routes (WebSub, webmention, media proxy)
### Controllers
**`lib/controllers/microsub.js`**
- Main Microsub API dispatcher
- Routes GET/POST requests by `action` parameter
- Calls specialized controllers (channels, timeline, follow, mute, block, search, preview, events)
**`lib/controllers/reader.js`**
- Web UI controller for reader interface
- Channel management (list, create, delete, settings)
- Feed management (add, remove, edit, rediscover, refresh)
- Timeline rendering (pagination, read/unread filtering)
- Compose form (reply, like, repost, bookmark via Micropub)
- Search and discovery UI
**`lib/controllers/channels.js`**
- Microsub API: `action=channels`
- List, create, update, delete, reorder channels
**`lib/controllers/timeline.js`**
- Microsub API: `action=timeline`
- Get timeline items (paginated)
- Mark read/unread, remove items
**`lib/controllers/follow.js`**
- Microsub API: `action=follow`, `action=unfollow`
- Subscribe to feeds, unsubscribe
- Notifies blogroll plugin via `blogroll-notify.js`
**`lib/controllers/mute.js` / `block.js`**
- Microsub API: `action=mute`, `action=unmute`, `action=block`, `action=unblock`
- Mute URLs, block authors
**`lib/controllers/search.js`**
- Microsub API: `action=search`
- Feed discovery from URL
**`lib/controllers/preview.js`**
- Microsub API: `action=preview`
- Preview feed before subscribing
**`lib/controllers/events.js`**
- Microsub API: `action=events`
- Server-Sent Events (SSE) stream for real-time updates
**`lib/controllers/opml.js`**
- Export subscriptions as OPML
### Storage Layer
**`lib/storage/channels.js`**
- `createChannel()`, `getChannels()`, `getChannel()`, `updateChannel()`, `deleteChannel()`
- `reorderChannels()`, `updateChannelSettings()`
- `ensureNotificationsChannel()` - Auto-creates notifications channel
**`lib/storage/feeds.js`**
- `createFeed()`, `getFeedsForChannel()`, `getFeedById()`, `updateFeed()`, `deleteFeed()`
- `getFeedsToFetch()` - Returns feeds where `nextFetchAt <= now`
- `updateFeedAfterFetch()` - Adjusts tier based on content changes
- `updateFeedWebsub()` - Stores WebSub subscription data
- `updateFeedStatus()` - Tracks errors and health
- `getFeedsWithErrors()` - Admin diagnostics
**`lib/storage/items.js`**
- `addItem()` - Inserts item (dedup by `channelId + uid`)
- `getTimelineItems()` - Paginated timeline with before/after cursors
- `getItemById()`, `getItemsByUids()`
- `markItemsRead()`, `markItemsUnread()` - Per-user read state
- `removeItems()` - Delete items by ID/UID/URL
- `cleanupAllReadItems()` - Startup cleanup, keeps last 30 read per channel
- `createIndexes()` - Creates MongoDB indexes
**`lib/storage/filters.js`**
- `getMutedUrls()`, `addMutedUrl()`, `removeMutedUrl()`
- `getBlockedAuthors()`, `addBlockedAuthor()`, `removeBlockedAuthor()`
**`lib/storage/read-state.js`**
- `getReadState()`, `markRead()`, `markUnread()`
- Wraps `items.js` read operations
### Feed Processing
**`lib/feeds/parser.js`**
- `detectFeedType()` - Sniffs RSS/Atom/JSON Feed/h-feed from content
- `parseFeed()` - Dispatcher to format-specific parsers
**`lib/feeds/rss.js`**
- `parseRss()` - Parses RSS 2.0 and RSS 1.0 (RDF) using `feedparser`
**`lib/feeds/atom.js`**
- `parseAtom()` - Parses Atom feeds using `feedparser`
**`lib/feeds/jsonfeed.js`**
- `parseJsonFeed()` - Parses JSON Feed 1.x
**`lib/feeds/hfeed.js`**
- `parseHfeed()` - Parses h-feed microformats using `microformats-parser`
**`lib/feeds/normalizer.js`**
- `normalizeItem()` - Converts parsed items to jf2 format
**`lib/feeds/fetcher.js`**
- `fetchFeed()` - HTTP fetch with User-Agent, timeout, redirect handling
**`lib/feeds/discovery.js`**
- `discoverFeeds()` - Parses HTML `<link>` tags for RSS/Atom/JSON Feed
- `discoverAndValidateFeeds()` - Discovery + validation
- `getBestFeed()` - Prefers Atom > RSS > JSON Feed > h-feed
**`lib/feeds/validator.js`**
- `validateFeedUrl()` - Fetches and parses feed to ensure it's valid
- Detects comments feeds (WordPress/Mastodon post replies)
### Polling System
**`lib/polling/scheduler.js`**
- `startScheduler()` - Runs every 60 seconds, calls `runSchedulerCycle()`
- `stopScheduler()` - Cleanup on shutdown
- `refreshFeedNow()` - Manual feed refresh
**`lib/polling/processor.js`**
- `processFeed()` - Fetch, parse, add items for one feed
- `processFeedBatch()` - Concurrent processing (default 5 feeds at once)
**`lib/polling/tier.js`**
- `getTierInterval()` - Maps tier (0-10) to polling interval
- `adjustTier()` - Increases/decreases tier based on update frequency
### Real-Time Updates
**`lib/websub/discovery.js`**
- `discoverWebsubHub()` - Parses feed for `<link rel="hub">` or `<atom:link rel="hub">`
**`lib/websub/subscriber.js`**
- `subscribeToHub()` - Sends WebSub subscribe request to hub
**`lib/websub/handler.js`**
- `verify()` - Handles hub verification (GET /microsub/websub/:id)
- `receive()` - Handles content distribution (POST /microsub/websub/:id)
**`lib/webmention/receiver.js`**
- `receive()` - Accepts webmentions (POST /microsub/webmention)
- Adds to notifications channel
**`lib/webmention/verifier.js`**
- `verifyWebmention()` - Fetches source URL and confirms link to target
**`lib/webmention/processor.js`**
- `processWebmention()` - Parses source as h-entry, adds to notifications
### Media and Utilities
**`lib/media/proxy.js`**
- `handleMediaProxy()` - GET /microsub/media/:hash
- Fetches and caches external images, serves with correct Content-Type
- Hash is base64url(url)
**`lib/utils/auth.js`**
- `getUserId()` - Extracts user ID from session (defaults to "default" for single-user)
**`lib/utils/jf2.js`**
- `generateChannelUid()` - Random 8-char alphanumeric
- `convertToJf2()` - Transforms various formats to jf2
**`lib/utils/pagination.js`**
- `buildPaginationQuery()` - Cursor-based pagination (before/after)
- `generatePagingCursors()` - Returns `before` and `after` cursor strings
**`lib/utils/validation.js`**
- `validateChannelName()`, `validateAction()`, `validateExcludeTypes()`, `validateExcludeRegex()`
**`lib/utils/blogroll-notify.js`**
- `notifyBlogroll()` - Fire-and-forget notification to `@rmdes/indiekit-endpoint-blogroll`
- On follow: upserts blog entry with `source: "microsub"`
- On unfollow: soft-deletes blog entry
**`lib/cache/redis.js`**
- Optional Redis caching (not currently used in core)
**`lib/search/indexer.js` / `query.js`**
- Full-text search on items (uses MongoDB text index)
**`lib/realtime/broker.js`**
- SSE (Server-Sent Events) broker for real-time notifications
## Configuration
```javascript
import MicrosubEndpoint from "@rmdes/indiekit-endpoint-microsub";
export default {
plugins: [
new MicrosubEndpoint({
mountPath: "/microsub", // Default
}),
],
};
```
## Routes
### Protected (require auth)
| Method | Path | Description |
|--------|------|-------------|
| GET/POST | `/microsub` | Microsub API endpoint (action parameter) |
| GET | `/microsub/reader` | Reader UI (redirects to channels) |
| GET | `/microsub/reader/channels` | List channels |
| GET | `/microsub/reader/channels/new` | New channel form |
| POST | `/microsub/reader/channels/new` | Create channel |
| GET | `/microsub/reader/channels/:uid` | Channel timeline |
| GET | `/microsub/reader/channels/:uid/settings` | Channel settings form |
| POST | `/microsub/reader/channels/:uid/settings` | Update settings |
| POST | `/microsub/reader/channels/:uid/delete` | Delete channel |
| GET | `/microsub/reader/channels/:uid/feeds` | List feeds in channel |
| POST | `/microsub/reader/channels/:uid/feeds` | Add feed to channel |
| POST | `/microsub/reader/channels/:uid/feeds/remove` | Remove feed |
| GET | `/microsub/reader/channels/:uid/feeds/:feedId/edit` | Edit feed form |
| POST | `/microsub/reader/channels/:uid/feeds/:feedId/edit` | Update feed URL |
| POST | `/microsub/reader/channels/:uid/feeds/:feedId/rediscover` | Run feed discovery |
| POST | `/microsub/reader/channels/:uid/feeds/:feedId/refresh` | Force refresh |
| GET | `/microsub/reader/item/:id` | Single item view |
| GET | `/microsub/reader/compose` | Compose form |
| POST | `/microsub/reader/compose` | Submit post via Micropub |
| GET | `/microsub/reader/search` | Search/discover feeds page |
| POST | `/microsub/reader/search` | Search feeds |
| POST | `/microsub/reader/subscribe` | Subscribe from search results |
| POST | `/microsub/reader/api/mark-read` | Mark all items read |
| GET | `/microsub/reader/opml` | Export OPML |
### Public (no auth)
| Method | Path | Description |
|--------|------|-------------|
| GET | `/microsub/websub/:id` | WebSub verification |
| POST | `/microsub/websub/:id` | WebSub content distribution |
| POST | `/microsub/webmention` | Webmention receiver |
| GET | `/microsub/media/:hash` | Media proxy |
## Integration with Other Plugins
### Blogroll Plugin
When subscribing/unsubscribing to feeds, Microsub optionally notifies `@rmdes/indiekit-endpoint-blogroll`:
```javascript
// On follow
notifyBlogroll(application, "follow", {
url: feedUrl,
title: feedTitle,
channelName: channel.name,
feedId: feed._id,
channelId: channel._id,
});
// On unfollow
notifyBlogroll(application, "unfollow", { url: feedUrl });
```
Blogroll stores feeds with `source: "microsub"` and soft-deletes on unfollow. If user explicitly deletes from blogroll, Microsub won't re-add.
### Micropub Plugin
Compose form posts via Micropub:
```javascript
// Fetch syndication targets from Micropub config
const micropubUrl = `${application.micropubEndpoint}?q=config`;
const config = await fetch(micropubUrl, {
headers: { Authorization: `Bearer ${token}` }
});
const syndicationTargets = config["syndicate-to"];
```
Posts replies, likes, reposts, bookmarks:
```javascript
micropubData.append("h", "entry");
micropubData.append("in-reply-to", replyToUrl);
micropubData.append("content", content);
```
## Known Gotchas
### Date Handling
**Rule**: Always store dates as ISO strings (`new Date().toISOString()`), EXCEPT `published` and `updated` in `microsub_items`, and `nextFetchAt` in `microsub_feeds`, which are kept as `Date` objects for MongoDB query compatibility.
```javascript
// CORRECT - stored as Date for query
{ published: new Date(timestamp) }
// CORRECT - converted to ISO string when sending to client
published: item.published?.toISOString()
// CORRECT - other timestamps as ISO strings
{ createdAt: new Date().toISOString() }
```
Templates use `| date("PPp")` filter which requires ISO strings, so `transformToJf2()` converts `published` Date to ISO before sending to templates.
### Read State Cleanup
Only the last 30 read items per channel are kept. Cleanup runs:
- On startup: `cleanupAllReadItems()`
- After marking items read: `cleanupOldReadItems()`
This prevents database bloat. Unread items are never deleted by cleanup.
### Feed Discovery Gotchas
- **ActivityPub JSON**: If a URL returns ActivityPub JSON (e.g., Mastodon profile), discovery throws an error suggesting the direct feed URL (e.g., `/feed/`)
- **Comments Feeds**: WordPress post comment feeds are detected and allowed but warned about (usually not what users want)
- **HTML Feeds**: h-feed discovery requires microformats2 markup
### Polling and WebSub
- Feeds with WebSub subscriptions are still polled (but less frequently)
- WebSub expires after `leaseSeconds` - plugin should re-subscribe (TODO: check if implemented)
- Tier adjustment only happens on successful fetch - errors don't change tier
### Media Proxy
Images are proxied through `/microsub/media/:hash` where hash is base64url(imageUrl). This:
- Hides user IP from origin servers
- Caches images locally
- Works around CORS and mixed-content issues
### Blogroll Integration
If a feed was explicitly deleted from blogroll (`status: "deleted"`), Microsub won't re-add it on follow. Delete and re-subscribe to override.
### Concurrent Processing
Scheduler processes 5 feeds concurrently by default. Increase `BATCH_CONCURRENCY` in `scheduler.js` for faster syncing (but watch memory/network usage).
## Dependencies
**Core:**
- `express` - Routing
- `feedparser` - RSS/Atom parsing
- `microformats-parser` - h-feed parsing
- `htmlparser2` - HTML parsing
- `sanitize-html` - XSS prevention
- `luxon` - Date handling
**Indiekit:**
- `@indiekit/error` - Error handling
- `@indiekit/frontend` - UI components
- `@indiekit/util` - Utilities (formatDate, etc.)
**Optional:**
- `ioredis` - Redis caching (not currently used)
- `debug` - Debug logging
## Testing and Debugging
**Enable debug logging:**
```bash
DEBUG=microsub:* npm start
```
**Check scheduler status:**
Scheduler runs every 60 seconds. Check logs for `[Microsub] Processing N feeds due for refresh`.
**Inspect feed errors:**
```javascript
const feeds = await getFeedsWithErrors(application, 3);
console.log(feeds.map(f => ({ url: f.url, error: f.lastError })));
```
**Manual feed refresh:**
```bash
POST /microsub/reader/channels/:uid/feeds/:feedId/refresh
```
**Clear read items:**
```javascript
await cleanupAllReadItems(application);
```
**Check WebSub subscriptions:**
```javascript
const feeds = await collection.find({ "websub.hub": { $exists: true } }).toArray();
```
## Common Issues
**Q: Feeds not updating?**
- Check `nextFetchAt` in `microsub_feeds` - may be in far future due to high tier
- Force refresh or rediscover feed from UI
**Q: Items disappearing after marking read?**
- Normal behavior - only last 30 read items kept per channel
- Adjust `MAX_READ_ITEMS` in `storage/items.js` if needed
**Q: "Unable to detect feed type" error?**
- Feed may be behind login wall
- Check if URL returns HTML instead of XML/JSON
- Try feed discovery from homepage URL
**Q: Duplicate items showing up?**
- Dedup is by `channelId + uid` - ensure feed provides stable GUIDs
- Check if feed URL changed (different feedId → new items)
**Q: WebSub not working?**
- Check hub discovery in feed XML: `<link rel="hub" href="..."/>`
- Verify callback URL is publicly accessible
- Check logs for hub verification failures
## Future Improvements
- WebSub lease renewal (currently expires after `leaseSeconds`)
- Redis caching for items (reduce MongoDB load)
- Full-text search UI (backend already implemented)
- SSE events stream UI (backend already implemented)
- OPML import (export already works)
- Microsub client compatibility testing (Indigenous, Monocle, etc.)
- Feed health dashboard (show error counts, last fetch times)
- Batch mark-read from timeline UI (currently channel-wide only)

246
README.md Normal file
View File

@@ -0,0 +1,246 @@
# @rmdes/indiekit-endpoint-microsub
A comprehensive Microsub social reader plugin for Indiekit. Subscribe to feeds (RSS, Atom, JSON Feed, h-feed), organize them into channels, and read posts in a unified timeline interface with a built-in web reader UI.
## Features
- **Microsub Protocol**: Full implementation of the [Microsub spec](https://indieweb.org/Microsub)
- **Multi-Format Feeds**: RSS, Atom, JSON Feed, h-feed (microformats)
- **Smart Polling**: Adaptive tiered polling (2 minutes to 17+ hours) based on update frequency
- **Real-Time Updates**: WebSub (PubSubHubbub) support for instant notifications
- **Web Reader UI**: Built-in reader interface with channel navigation and timeline view
- **Feed Discovery**: Automatic discovery of feeds from website URLs
- **Read State**: Per-user read tracking with automatic cleanup
- **Compose Interface**: Post replies, likes, reposts, and bookmarks via Micropub
- **Webmention Support**: Receive webmentions in your notifications channel
- **Media Proxy**: Privacy-friendly image proxying
- **OPML Export**: Export your subscriptions as OPML
## Installation
```bash
npm install @rmdes/indiekit-endpoint-microsub
```
## Configuration
Add to your Indiekit config:
```javascript
import MicrosubEndpoint from "@rmdes/indiekit-endpoint-microsub";
export default {
plugins: [
new MicrosubEndpoint({
mountPath: "/microsub", // Default mount path
}),
],
};
```
## Usage
### Web Reader UI
Navigate to `/microsub/reader` in your Indiekit installation to access the web interface.
**Channels**: Organize feeds into channels (Technology, News, Friends, etc.)
- Create new channels
- Configure content filters (exclude types, regex patterns)
- Reorder channels
**Feeds**: Manage subscriptions within each channel
- Subscribe to feeds by URL
- Search and discover feeds from websites
- Edit or rediscover feed URLs
- Force refresh feeds
- View feed health status
**Timeline**: Read posts from subscribed feeds
- Paginated timeline view
- Mark individual items or all items as read
- View read items separately
- Click through to original posts
**Compose**: Create posts via Micropub
- Reply to posts
- Like posts
- Repost posts
- Bookmark posts
- Include syndication targets
### Microsub API
Compatible with Microsub clients like [Indigenous](https://indigenous.realize.be/) and [Monocle](https://monocle.p3k.io/).
**Endpoint:** Your Indiekit URL + `/microsub`
**Supported Actions:**
- `channels` - List, create, update, delete, reorder channels
- `timeline` - Get timeline items (paginated)
- `follow` - Subscribe to a feed
- `unfollow` - Unsubscribe from a feed
- `mute` - Mute URLs
- `unmute` - Unmute URLs
- `block` - Block authors
- `unblock` - Unblock authors
- `search` - Discover feeds from URL
- `preview` - Preview feed before subscribing
**Example:**
```bash
# List channels
curl "https://your-site.example/microsub?action=channels" \
-H "Authorization: Bearer YOUR_TOKEN"
# Get timeline for channel
curl "https://your-site.example/microsub?action=timeline&channel=CHANNEL_UID" \
-H "Authorization: Bearer YOUR_TOKEN"
# Subscribe to feed
curl "https://your-site.example/microsub" \
-X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-d "action=follow&channel=CHANNEL_UID&url=https://example.com/feed"
```
## Feed Polling
Feeds are polled using an adaptive tiered system:
- **Tier 0**: 1 minute (very active feeds)
- **Tier 1**: 2 minutes (active feeds)
- **Tier 2**: 4 minutes
- **Tier 3**: 8 minutes
- ...
- **Tier 10**: ~17 hours (inactive feeds)
Tiers adjust automatically:
- Feed updates → decrease tier (faster polling)
- No changes for 2+ fetches → increase tier (slower polling)
WebSub-enabled feeds receive instant updates when available.
## Read State Management
Read items are tracked per user. To prevent database bloat, only the last 30 read items per channel are kept. Unread items are never deleted.
Cleanup runs automatically:
- On server startup
- After marking items read
## Integration with Other Plugins
### Blogroll Plugin
If `@rmdes/indiekit-endpoint-blogroll` is installed, Microsub will automatically sync feed subscriptions:
- Subscribe to feed → adds to blogroll
- Unsubscribe → soft-deletes from blogroll
### Micropub Plugin
The compose interface posts via Micropub. Ensure `@indiekit/endpoint-micropub` is configured.
## OPML Export
Export your subscriptions:
```
GET /microsub/reader/opml
```
Returns OPML XML with all subscribed feeds organized by channel.
## Webmentions
The plugin accepts webmentions at `/microsub/webmention`. Received webmentions appear in the special "Notifications" channel.
To advertise your webmention endpoint, add to your site's `<head>`:
```html
<link rel="webmention" href="https://your-site.example/microsub/webmention" />
```
## Media Proxy
External images are proxied through `/microsub/media/:hash` for privacy and caching. This prevents your IP address from being sent to third-party image hosts.
## API Response Format
All API responses follow the Microsub spec. Timeline items use the [jf2 format](https://jf2.spec.indieweb.org/).
**Example timeline response:**
```json
{
"items": [
{
"type": "entry",
"uid": "https://example.com/post/123",
"url": "https://example.com/post/123",
"published": "2026-02-13T12:00:00.000Z",
"name": "Post Title",
"content": {
"text": "Plain text content",
"html": "<p>HTML content</p>"
},
"author": {
"name": "Author Name",
"url": "https://author.example/",
"photo": "https://author.example/photo.jpg"
},
"_id": "507f1f77bcf86cd799439011",
"_is_read": false
}
],
"paging": {
"after": "cursor-string"
}
}
```
## Database Collections
The plugin creates these MongoDB collections:
- `microsub_channels` - User channels
- `microsub_feeds` - Feed subscriptions with polling metadata
- `microsub_items` - Timeline items (posts)
- `microsub_notifications` - Notifications channel items
- `microsub_muted` - Muted URLs
- `microsub_blocked` - Blocked authors
## Troubleshooting
### Feeds not updating
- Check the feed's `nextFetchAt` time in the admin UI
- Use "Force Refresh" button to poll immediately
- Try "Rediscover" to find the correct feed URL
### "Unable to detect feed type" error
- The URL may not be a valid feed
- Try using the search feature to discover feeds from the homepage
- Check if the feed requires authentication
### Items disappearing after marking read
This is normal behavior - only the last 30 read items per channel are kept to prevent database bloat. Unread items are never deleted.
### Duplicate items
Deduplication is based on the feed's GUID/URL. If a feed doesn't provide stable GUIDs, duplicates may appear.
## Contributing
Issues and pull requests welcome at [github.com/rmdes/indiekit-endpoint-microsub](https://github.com/rmdes/indiekit-endpoint-microsub)
## License
MIT
## Credits
Built by [Ricardo Mendes](https://rmendes.net) for the IndieWeb community.