@rmdes/indiekit-endpoint-microsub 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/styles.css CHANGED
@@ -679,43 +679,6 @@
679
679
  text-align: right;
680
680
  }
681
681
 
682
- .compose__syndication {
683
- background: var(--color-offset);
684
- border: none;
685
- border-radius: var(--border-radius);
686
- margin: var(--space-m) 0;
687
- padding: var(--space-m);
688
- }
689
-
690
- .compose__syndication legend {
691
- font-weight: 600;
692
- margin-bottom: var(--space-xs);
693
- }
694
-
695
- .compose__syndication .hint {
696
- color: var(--color-text-muted);
697
- font-size: var(--font-size-small);
698
- margin-bottom: var(--space-s);
699
- }
700
-
701
- .syndication-target {
702
- align-items: center;
703
- cursor: pointer;
704
- display: flex;
705
- gap: var(--space-s);
706
- padding: var(--space-xs) 0;
707
- }
708
-
709
- .syndication-target input[type="checkbox"] {
710
- flex-shrink: 0;
711
- height: 1.25rem;
712
- width: 1.25rem;
713
- }
714
-
715
- .syndication-target__name {
716
- flex: 1;
717
- }
718
-
719
682
  /* ==========================================================================
720
683
  Settings
721
684
  ========================================================================== */
package/index.js CHANGED
@@ -96,7 +96,6 @@ export default class MicrosubEndpoint {
96
96
  readerRouter.get("/search", readerController.searchPage);
97
97
  readerRouter.post("/search", readerController.searchFeeds);
98
98
  readerRouter.post("/subscribe", readerController.subscribe);
99
- readerRouter.post("/api/mark-read", readerController.markAllRead);
100
99
  router.use("/reader", readerRouter);
101
100
 
102
101
  return router;
@@ -17,12 +17,7 @@ import {
17
17
  createFeed,
18
18
  deleteFeed,
19
19
  } from "../storage/feeds.js";
20
- import {
21
- getTimelineItems,
22
- getItemById,
23
- markItemsRead,
24
- countReadItems,
25
- } from "../storage/items.js";
20
+ import { getTimelineItems, getItemById } from "../storage/items.js";
26
21
  import { getUserId } from "../utils/auth.js";
27
22
  import {
28
23
  validateChannelName,
@@ -96,37 +91,24 @@ export async function channel(request, response) {
96
91
  const { application } = request.app.locals;
97
92
  const userId = getUserId(request);
98
93
  const { uid } = request.params;
99
- const { before, after, showRead } = request.query;
94
+ const { before, after } = request.query;
100
95
 
101
96
  const channelDocument = await getChannel(application, uid, userId);
102
97
  if (!channelDocument) {
103
98
  return response.status(404).render("404");
104
99
  }
105
100
 
106
- // Check if showing read items
107
- const showReadItems = showRead === "true";
108
-
109
101
  const timeline = await getTimelineItems(application, channelDocument._id, {
110
102
  before,
111
103
  after,
112
104
  userId,
113
- showRead: showReadItems,
114
105
  });
115
106
 
116
- // Count read items to show "View read items" button
117
- const readCount = await countReadItems(
118
- application,
119
- channelDocument._id,
120
- userId,
121
- );
122
-
123
107
  response.render("channel", {
124
108
  title: channelDocument.name,
125
109
  channel: channelDocument,
126
110
  items: timeline.items,
127
111
  paging: timeline.paging,
128
- readCount,
129
- showRead: showReadItems,
130
112
  baseUrl: request.baseUrl,
131
113
  });
132
114
  }
@@ -191,33 +173,6 @@ export async function updateSettings(request, response) {
191
173
  response.redirect(`${request.baseUrl}/channels/${uid}`);
192
174
  }
193
175
 
194
- /**
195
- * Mark all items in channel as read
196
- * @param {object} request - Express request
197
- * @param {object} response - Express response
198
- * @returns {Promise<void>}
199
- */
200
- export async function markAllRead(request, response) {
201
- const { application } = request.app.locals;
202
- const userId = getUserId(request);
203
- const { channel: channelUid } = request.body;
204
-
205
- const channelDocument = await getChannel(application, channelUid, userId);
206
- if (!channelDocument) {
207
- return response.status(404).render("404");
208
- }
209
-
210
- // Mark all items as read using the special "last-read-entry" value
211
- await markItemsRead(
212
- application,
213
- channelDocument._id,
214
- ["last-read-entry"],
215
- userId,
216
- );
217
-
218
- response.redirect(`${request.baseUrl}/channels/${channelUid}`);
219
- }
220
-
221
176
  /**
222
177
  * Delete channel
223
178
  * @param {object} request - Express request
@@ -360,38 +315,6 @@ function ensureString(value) {
360
315
  return String(value);
361
316
  }
362
317
 
363
- /**
364
- * Fetch syndication targets from Micropub config
365
- * @param {object} application - Indiekit application
366
- * @param {string} token - Auth token
367
- * @returns {Promise<Array>} Syndication targets
368
- */
369
- async function getSyndicationTargets(application, token) {
370
- try {
371
- const micropubEndpoint = application.micropubEndpoint;
372
- if (!micropubEndpoint) return [];
373
-
374
- const micropubUrl = micropubEndpoint.startsWith("http")
375
- ? micropubEndpoint
376
- : new URL(micropubEndpoint, application.url).href;
377
-
378
- const configUrl = `${micropubUrl}?q=config`;
379
- const configResponse = await fetch(configUrl, {
380
- headers: {
381
- Authorization: `Bearer ${token}`,
382
- Accept: "application/json",
383
- },
384
- });
385
-
386
- if (!configResponse.ok) return [];
387
-
388
- const config = await configResponse.json();
389
- return config["syndicate-to"] || [];
390
- } catch {
391
- return [];
392
- }
393
- }
394
-
395
318
  /**
396
319
  * Compose response form
397
320
  * @param {object} request - Express request
@@ -399,8 +322,6 @@ async function getSyndicationTargets(application, token) {
399
322
  * @returns {Promise<void>}
400
323
  */
401
324
  export async function compose(request, response) {
402
- const { application } = request.app.locals;
403
-
404
325
  // Support both long-form (replyTo) and short-form (reply) query params
405
326
  const {
406
327
  replyTo,
@@ -413,19 +334,12 @@ export async function compose(request, response) {
413
334
  bookmark,
414
335
  } = request.query;
415
336
 
416
- // Fetch syndication targets if user is authenticated
417
- const token = request.session?.access_token;
418
- const syndicationTargets = token
419
- ? await getSyndicationTargets(application, token)
420
- : [];
421
-
422
337
  response.render("compose", {
423
338
  title: request.__("microsub.compose.title"),
424
339
  replyTo: ensureString(replyTo || reply),
425
340
  likeOf: ensureString(likeOf || like),
426
341
  repostOf: ensureString(repostOf || repost),
427
342
  bookmarkOf: ensureString(bookmarkOf || bookmark),
428
- syndicationTargets,
429
343
  baseUrl: request.baseUrl,
430
344
  });
431
345
  }
@@ -443,7 +357,6 @@ export async function submitCompose(request, response) {
443
357
  const likeOf = request.body["like-of"];
444
358
  const repostOf = request.body["repost-of"];
445
359
  const bookmarkOf = request.body["bookmark-of"];
446
- const syndicateTo = request.body["mp-syndicate-to"];
447
360
 
448
361
  // Debug logging
449
362
  console.info(
@@ -456,7 +369,6 @@ export async function submitCompose(request, response) {
456
369
  likeOf,
457
370
  repostOf,
458
371
  bookmarkOf,
459
- syndicateTo,
460
372
  });
461
373
 
462
374
  // Get Micropub endpoint
@@ -484,22 +396,16 @@ export async function submitCompose(request, response) {
484
396
  micropubData.append("h", "entry");
485
397
 
486
398
  if (likeOf) {
487
- // Like post - content is optional comment
399
+ // Like post (no content needed)
488
400
  micropubData.append("like-of", likeOf);
489
- if (content && content.trim()) {
490
- micropubData.append("content", content.trim());
491
- }
492
401
  } else if (repostOf) {
493
- // Repost - content is optional comment
402
+ // Repost (no content needed)
494
403
  micropubData.append("repost-of", repostOf);
495
- if (content && content.trim()) {
496
- micropubData.append("content", content.trim());
497
- }
498
404
  } else if (bookmarkOf) {
499
- // Bookmark - content is optional comment
405
+ // Bookmark (content optional)
500
406
  micropubData.append("bookmark-of", bookmarkOf);
501
- if (content && content.trim()) {
502
- micropubData.append("content", content.trim());
407
+ if (content) {
408
+ micropubData.append("content", content);
503
409
  }
504
410
  } else if (inReplyTo) {
505
411
  // Reply
@@ -510,14 +416,6 @@ export async function submitCompose(request, response) {
510
416
  micropubData.append("content", content || "");
511
417
  }
512
418
 
513
- // Add syndication targets
514
- if (syndicateTo) {
515
- const targets = Array.isArray(syndicateTo) ? syndicateTo : [syndicateTo];
516
- for (const target of targets) {
517
- micropubData.append("mp-syndicate-to", target);
518
- }
519
- }
520
-
521
419
  // Debug: log what we're sending
522
420
  console.info("[Microsub] Sending to Micropub:", {
523
421
  url: micropubUrl,
@@ -675,7 +573,6 @@ export const readerController = {
675
573
  channel,
676
574
  settings,
677
575
  updateSettings,
678
- markAllRead,
679
576
  deleteChannel: deleteChannelAction,
680
577
  feeds,
681
578
  addFeed,
@@ -78,7 +78,6 @@ export async function addItem(application, { channelId, feedId, uid, item }) {
78
78
  * @param {string} [options.after] - After cursor
79
79
  * @param {number} [options.limit] - Items per page
80
80
  * @param {string} [options.userId] - User ID for read state
81
- * @param {boolean} [options.showRead] - Whether to show read items (default: false)
82
81
  * @returns {Promise<object>} Timeline with items and paging
83
82
  */
84
83
  export async function getTimelineItems(application, channelId, options = {}) {
@@ -87,12 +86,7 @@ export async function getTimelineItems(application, channelId, options = {}) {
87
86
  typeof channelId === "string" ? new ObjectId(channelId) : channelId;
88
87
  const limit = parseLimit(options.limit);
89
88
 
90
- // Base query - filter out read items unless showRead is true
91
89
  const baseQuery = { channelId: objectId };
92
- if (options.userId && !options.showRead) {
93
- baseQuery.readBy = { $ne: options.userId };
94
- }
95
-
96
90
  const query = buildPaginationQuery({
97
91
  before: options.before,
98
92
  after: options.after,
@@ -262,24 +256,6 @@ export async function getItemsByUids(application, uids, userId) {
262
256
  return items.map((item) => transformToJf2(item, userId));
263
257
  }
264
258
 
265
- /**
266
- * Count read items in a channel for a user
267
- * @param {object} application - Indiekit application
268
- * @param {ObjectId|string} channelId - Channel ObjectId
269
- * @param {string} userId - User ID
270
- * @returns {Promise<number>} Count of read items
271
- */
272
- export async function countReadItems(application, channelId, userId) {
273
- const collection = getCollection(application);
274
- const objectId =
275
- typeof channelId === "string" ? new ObjectId(channelId) : channelId;
276
-
277
- return collection.countDocuments({
278
- channelId: objectId,
279
- readBy: userId,
280
- });
281
- }
282
-
283
259
  /**
284
260
  * Mark items as read
285
261
  * @param {object} application - Indiekit application
package/locales/en.json CHANGED
@@ -4,9 +4,6 @@
4
4
  "title": "Reader",
5
5
  "empty": "No items to display",
6
6
  "markAllRead": "Mark all as read",
7
- "showRead": "Show read ({{count}})",
8
- "hideRead": "Hide read items",
9
- "allRead": "All caught up!",
10
7
  "newer": "Newer",
11
8
  "older": "Older"
12
9
  },
@@ -46,16 +43,12 @@
46
43
  "compose": {
47
44
  "title": "Compose",
48
45
  "content": "What's on your mind?",
49
- "comment": "Add a comment (optional)",
50
- "commentHint": "Your comment will be included with the syndicated post",
51
46
  "submit": "Post",
52
47
  "cancel": "Cancel",
53
48
  "replyTo": "Replying to",
54
49
  "likeOf": "Liking",
55
50
  "repostOf": "Reposting",
56
- "bookmarkOf": "Bookmarking",
57
- "syndicateTo": "Syndicate to",
58
- "syndicateHint": "Also share this to your connected accounts"
51
+ "bookmarkOf": "Bookmarking"
59
52
  },
60
53
  "settings": {
61
54
  "title": "{{channel}} settings",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-microsub",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -10,7 +10,7 @@
10
10
  "reader",
11
11
  "social-reader"
12
12
  ],
13
- "homepage": "https://github.com/rmdes/indiekit",
13
+ "homepage": "https://github.com/rmdes/indiekit-endpoint-microsub",
14
14
  "author": {
15
15
  "name": "Ricardo Mendes",
16
16
  "url": "https://rmendes.net"
@@ -21,9 +21,6 @@
21
21
  },
22
22
  "type": "module",
23
23
  "main": "index.js",
24
- "scripts": {
25
- "test": "node --test test/unit/*.js"
26
- },
27
24
  "files": [
28
25
  "assets",
29
26
  "lib",
@@ -32,12 +29,11 @@
32
29
  "index.js"
33
30
  ],
34
31
  "bugs": {
35
- "url": "https://github.com/rmdes/indiekit/issues"
32
+ "url": "https://github.com/rmdes/indiekit-endpoint-microsub/issues"
36
33
  },
37
34
  "repository": {
38
35
  "type": "git",
39
- "url": "https://github.com/rmdes/indiekit.git",
40
- "directory": "packages/endpoint-microsub"
36
+ "url": "https://github.com/rmdes/indiekit-endpoint-microsub.git"
41
37
  },
42
38
  "dependencies": {
43
39
  "@indiekit/error": "^1.0.0-beta.25",
package/views/channel.njk CHANGED
@@ -7,7 +7,6 @@
7
7
  {{ icon("previous") }} {{ __("microsub.channels.title") }}
8
8
  </a>
9
9
  <div class="channel__actions">
10
- {% if not showRead and items.length > 0 %}
11
10
  <form action="{{ baseUrl }}/api/mark-read" method="POST" style="display: inline;">
12
11
  <input type="hidden" name="channel" value="{{ channel.uid }}">
13
12
  <input type="hidden" name="entry" value="last-read-entry">
@@ -15,16 +14,6 @@
15
14
  {{ icon("checkboxChecked") }} {{ __("microsub.reader.markAllRead") }}
16
15
  </button>
17
16
  </form>
18
- {% endif %}
19
- {% if showRead %}
20
- <a href="{{ baseUrl }}/channels/{{ channel.uid }}" class="button button--secondary button--small">
21
- {{ icon("hide") }} {{ __("microsub.reader.hideRead") }}
22
- </a>
23
- {% elif readCount > 0 %}
24
- <a href="{{ baseUrl }}/channels/{{ channel.uid }}?showRead=true" class="button button--secondary button--small">
25
- {{ icon("show") }} {{ __("microsub.reader.showRead", { count: readCount }) }}
26
- </a>
27
- {% endif %}
28
17
  <a href="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="button button--secondary button--small">
29
18
  {{ icon("syndicate") }} {{ __("microsub.feeds.title") }}
30
19
  </a>
@@ -59,19 +48,11 @@
59
48
  {% endif %}
60
49
  {% else %}
61
50
  <div class="reader__empty">
62
- {% if readCount > 0 and not showRead %}
63
- {{ icon("checkboxChecked") }}
64
- <p>{{ __("microsub.reader.allRead") }}</p>
65
- <a href="{{ baseUrl }}/channels/{{ channel.uid }}?showRead=true" class="button button--secondary">
66
- {{ icon("show") }} {{ __("microsub.reader.showRead", { count: readCount }) }}
67
- </a>
68
- {% else %}
69
51
  {{ icon("syndicate") }}
70
52
  <p>{{ __("microsub.timeline.empty") }}</p>
71
53
  <a href="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="button button--primary">
72
54
  {{ __("microsub.feeds.subscribe") }}
73
55
  </a>
74
- {% endif %}
75
56
  </div>
76
57
  {% endif %}
77
58
  </div>
package/views/compose.njk CHANGED
@@ -71,32 +71,6 @@
71
71
  <div class="compose__counter">
72
72
  <span id="char-count">0</span> characters
73
73
  </div>
74
- {% else %}
75
- {# Comment field for likes/reposts/bookmarks #}
76
- {{ textarea({
77
- label: __("microsub.compose.comment"),
78
- id: "content",
79
- name: "content",
80
- rows: 3,
81
- hint: __("microsub.compose.commentHint")
82
- }) }}
83
- <div class="compose__counter">
84
- <span id="char-count">0</span> characters
85
- </div>
86
- {% endif %}
87
-
88
- {# Syndication targets #}
89
- {% if syndicationTargets and syndicationTargets.length %}
90
- <fieldset class="compose__syndication">
91
- <legend>{{ __("microsub.compose.syndicateTo") }}</legend>
92
- <p class="hint">{{ __("microsub.compose.syndicateHint") }}</p>
93
- {% for target in syndicationTargets %}
94
- <label class="syndication-target">
95
- <input type="checkbox" name="mp-syndicate-to" value="{{ target.uid }}"{% if target.checked %} checked{% endif %}>
96
- <span class="syndication-target__name">{{ target.name }}</span>
97
- </label>
98
- {% endfor %}
99
- </fieldset>
100
74
  {% endif %}
101
75
 
102
76
  <div class="button-group">
@@ -110,6 +84,7 @@
110
84
  </form>
111
85
  </div>
112
86
 
87
+ {% if not isAction %}
113
88
  <script type="module">
114
89
  const textarea = document.getElementById('content');
115
90
  const counter = document.getElementById('char-count');
@@ -119,4 +94,5 @@
119
94
  });
120
95
  }
121
96
  </script>
97
+ {% endif %}
122
98
  {% endblock %}
package/README.md DELETED
@@ -1,111 +0,0 @@
1
- # @indiekit/endpoint-microsub
2
-
3
- Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the [Microsub protocol](https://indieweb.org/Microsub).
4
-
5
- ## Features
6
-
7
- - **Full Microsub API** - Channels, timeline, follow/unfollow, search, preview, mute, block
8
- - **Multiple feed formats** - RSS 1.0/2.0, Atom, JSON Feed, h-feed (Microformats)
9
- - **External client support** - Works with Monocle, Together, IndiePass
10
- - **Built-in reader UI** - Social reading experience in Indiekit admin
11
- - **Real-time updates** - Server-Sent Events (SSE) and WebSub integration
12
- - **Adaptive polling** - Tier-based feed fetching inspired by Ekster
13
- - **Direct webmention receiving** - Notifications channel for mentions
14
-
15
- ## Installation
16
-
17
- `npm install @indiekit/endpoint-microsub`
18
-
19
- ## Usage
20
-
21
- Add `@indiekit/endpoint-microsub` to the list of plugins in your configuration:
22
-
23
- ```js
24
- export default {
25
- plugins: ["@indiekit/endpoint-microsub"],
26
- };
27
- ```
28
-
29
- ## Options
30
-
31
- | Option | Type | Description |
32
- | :----------- | :------- | :--------------------------------------------------------------- |
33
- | `mountPath` | `string` | Path to mount Microsub API. _Optional_, defaults to `/microsub`. |
34
- | `readerPath` | `string` | Path to mount reader UI. _Optional_, defaults to `/reader`. |
35
-
36
- ## Endpoints
37
-
38
- ### Microsub API
39
-
40
- The main Microsub endpoint is mounted at `/microsub` (configurable).
41
-
42
- **Discovery**: Add this to your site's `<head>`:
43
-
44
- ```html
45
- <link rel="microsub" href="https://yoursite.com/microsub" />
46
- ```
47
-
48
- ### Supported actions
49
-
50
- | Action | GET | POST | Description |
51
- | :--------- | :-- | :--- | :--------------------------------------------- |
52
- | `channels` | ✓ | ✓ | List, create, update, delete, reorder channels |
53
- | `timeline` | ✓ | ✓ | Get timeline, mark read/unread, remove items |
54
- | `follow` | ✓ | ✓ | List followed feeds, subscribe to new feeds |
55
- | `unfollow` | - | ✓ | Unsubscribe from feeds |
56
- | `search` | ✓ | ✓ | Feed discovery and full-text search |
57
- | `preview` | ✓ | ✓ | Preview feed before subscribing |
58
- | `mute` | ✓ | ✓ | List muted URLs, mute/unmute |
59
- | `block` | ✓ | ✓ | List blocked URLs, block/unblock |
60
- | `events` | ✓ | - | Server-Sent Events stream |
61
-
62
- ### Reader UI
63
-
64
- The built-in reader is mounted at `/reader` (configurable) and provides:
65
-
66
- - Channel list with unread counts
67
- - Timeline view with items
68
- - Mark as read on scroll/click
69
- - Like/reply/repost via Micropub
70
- - Channel settings (filters)
71
- - Compose modal
72
-
73
- ### WebSub callbacks
74
-
75
- WebSub hub callbacks are handled at `/microsub/websub/:id`.
76
-
77
- ### Webmention receiving
78
-
79
- Direct webmentions can be sent to `/microsub/webmention`.
80
-
81
- ## MongoDB Collections
82
-
83
- This plugin creates the following collections:
84
-
85
- - `microsub_channels` - User's feed channels
86
- - `microsub_feeds` - Subscribed feeds
87
- - `microsub_items` - Timeline entries
88
- - `microsub_notifications` - Webmention notifications
89
- - `microsub_muted` - Muted URLs
90
- - `microsub_blocked` - Blocked URLs
91
-
92
- ## Dependencies
93
-
94
- - **feedparser** - RSS/Atom parsing
95
- - **microformats-parser** - h-feed parsing
96
- - **ioredis** - Redis client (optional, for caching/pub-sub)
97
- - **sanitize-html** - XSS prevention
98
-
99
- ## External Clients
100
-
101
- This endpoint is compatible with:
102
-
103
- - [Monocle](https://monocle.p3k.io/) - Web-based reader
104
- - [Together](https://together.tpxl.io/) - Web-based reader
105
- - [IndiePass](https://indiepass.app/) - Mobile/desktop app (archived)
106
-
107
- ## References
108
-
109
- - [Microsub Specification](https://indieweb.org/Microsub-spec)
110
- - [Ekster](https://github.com/pstuifzand/ekster) - Reference implementation in Go
111
- - [Aperture](https://github.com/aaronpk/Aperture) - Popular Microsub server in PHP