@rmdes/indiekit-endpoint-microsub 1.0.0-beta.7 → 1.0.0-beta.9

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/index.js CHANGED
@@ -87,6 +87,9 @@ export default class MicrosubEndpoint {
87
87
  readerRouter.get("/item/:id", readerController.item);
88
88
  readerRouter.get("/compose", readerController.compose);
89
89
  readerRouter.post("/compose", readerController.submitCompose);
90
+ readerRouter.get("/search", readerController.searchPage);
91
+ readerRouter.post("/search", readerController.searchFeeds);
92
+ readerRouter.post("/subscribe", readerController.subscribe);
90
93
  router.use("/reader", readerRouter);
91
94
 
92
95
  return router;
@@ -114,6 +117,8 @@ export default class MicrosubEndpoint {
114
117
  * @param {object} indiekit - Indiekit instance
115
118
  */
116
119
  init(indiekit) {
120
+ console.info("[Microsub] Initializing endpoint-microsub plugin");
121
+
117
122
  // Register MongoDB collections
118
123
  indiekit.addCollection("microsub_channels");
119
124
  indiekit.addCollection("microsub_feeds");
@@ -122,6 +127,8 @@ export default class MicrosubEndpoint {
122
127
  indiekit.addCollection("microsub_muted");
123
128
  indiekit.addCollection("microsub_blocked");
124
129
 
130
+ console.info("[Microsub] Registered MongoDB collections");
131
+
125
132
  // Register endpoint
126
133
  indiekit.addEndpoint(this);
127
134
 
@@ -133,7 +140,12 @@ export default class MicrosubEndpoint {
133
140
  // Start feed polling scheduler when server starts
134
141
  // This will be called after the server is ready
135
142
  if (indiekit.database) {
143
+ console.info("[Microsub] Database available, starting scheduler");
136
144
  startScheduler(indiekit);
145
+ } else {
146
+ console.warn(
147
+ "[Microsub] Database not available at init, scheduler not started",
148
+ );
137
149
  }
138
150
  }
139
151
 
@@ -3,6 +3,7 @@
3
3
  * @module controllers/reader
4
4
  */
5
5
 
6
+ import { discoverFeedsFromUrl } from "../feeds/fetcher.js";
6
7
  import { refreshFeedNow } from "../polling/scheduler.js";
7
8
  import {
8
9
  getChannels,
@@ -302,6 +303,89 @@ export async function submitCompose(request, response) {
302
303
  response.redirect(`${request.baseUrl}/channels`);
303
304
  }
304
305
 
306
+ /**
307
+ * Search/discover feeds page
308
+ * @param {object} request - Express request
309
+ * @param {object} response - Express response
310
+ * @returns {Promise<void>}
311
+ */
312
+ export async function searchPage(request, response) {
313
+ const { application } = request.app.locals;
314
+ const userId = request.session?.userId;
315
+
316
+ const channelList = await getChannels(application, userId);
317
+
318
+ response.render("search", {
319
+ title: request.__("microsub.search.title"),
320
+ channels: channelList,
321
+ baseUrl: request.baseUrl,
322
+ });
323
+ }
324
+
325
+ /**
326
+ * Search for feeds from URL
327
+ * @param {object} request - Express request
328
+ * @param {object} response - Express response
329
+ * @returns {Promise<void>}
330
+ */
331
+ export async function searchFeeds(request, response) {
332
+ const { application } = request.app.locals;
333
+ const userId = request.session?.userId;
334
+ const { query } = request.body;
335
+
336
+ const channelList = await getChannels(application, userId);
337
+
338
+ let results = [];
339
+ if (query) {
340
+ try {
341
+ results = await discoverFeedsFromUrl(query);
342
+ } catch {
343
+ // Ignore discovery errors
344
+ }
345
+ }
346
+
347
+ response.render("search", {
348
+ title: request.__("microsub.search.title"),
349
+ channels: channelList,
350
+ query,
351
+ results,
352
+ searched: true,
353
+ baseUrl: request.baseUrl,
354
+ });
355
+ }
356
+
357
+ /**
358
+ * Subscribe to a feed from search results
359
+ * @param {object} request - Express request
360
+ * @param {object} response - Express response
361
+ * @returns {Promise<void>}
362
+ */
363
+ export async function subscribe(request, response) {
364
+ const { application } = request.app.locals;
365
+ const userId = request.session?.userId;
366
+ const { url, channel: channelUid } = request.body;
367
+
368
+ const channelDocument = await getChannel(application, channelUid, userId);
369
+ if (!channelDocument) {
370
+ return response.status(404).render("404");
371
+ }
372
+
373
+ // Create feed subscription
374
+ const feed = await createFeed(application, {
375
+ channelId: channelDocument._id,
376
+ url,
377
+ title: undefined,
378
+ photo: undefined,
379
+ });
380
+
381
+ // Trigger immediate fetch in background
382
+ refreshFeedNow(application, feed._id).catch((error) => {
383
+ console.error(`[Microsub] Error fetching new feed ${url}:`, error.message);
384
+ });
385
+
386
+ response.redirect(`${request.baseUrl}/channels/${channelUid}/feeds`);
387
+ }
388
+
305
389
  export const readerController = {
306
390
  index,
307
391
  channels,
@@ -316,4 +400,7 @@ export const readerController = {
316
400
  item,
317
401
  compose,
318
402
  submitCompose,
403
+ searchPage,
404
+ searchFeeds,
405
+ subscribe,
319
406
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-microsub",
3
- "version": "1.0.0-beta.7",
3
+ "version": "1.0.0-beta.9",
4
4
  "description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.",
5
5
  "keywords": [
6
6
  "indiekit",
package/views/reader.njk CHANGED
@@ -21,6 +21,9 @@
21
21
  {% endfor %}
22
22
  </ul>
23
23
  <p class="reader__actions">
24
+ <a href="{{ baseUrl }}/search" class="button button--primary">
25
+ {{ icon("syndicate") }} {{ __("microsub.feeds.follow") }}
26
+ </a>
24
27
  <a href="{{ baseUrl }}/channels/new" class="button button--secondary">
25
28
  {{ icon("createPost") }} {{ __("microsub.channels.new") }}
26
29
  </a>
@@ -0,0 +1,58 @@
1
+ {% extends "document.njk" %}
2
+
3
+ {% block content %}
4
+ <div class="search">
5
+ <a href="{{ baseUrl }}/channels" class="back-link">
6
+ {{ icon("previous") }} {{ __("microsub.channels.title") }}
7
+ </a>
8
+
9
+ <h2>{{ __("microsub.search.title") }}</h2>
10
+
11
+ <form method="post" action="{{ baseUrl }}/search" class="search__form">
12
+ {{ input({
13
+ id: "query",
14
+ name: "query",
15
+ label: __("microsub.search.placeholder"),
16
+ type: "url",
17
+ required: true,
18
+ placeholder: "https://example.com",
19
+ autocomplete: "off",
20
+ value: query,
21
+ attributes: { autofocus: true }
22
+ }) }}
23
+ <div class="button-group">
24
+ {{ button({ text: __("microsub.search.submit") }) }}
25
+ </div>
26
+ </form>
27
+
28
+ {% if results and results.length > 0 %}
29
+ <div class="search__results">
30
+ <h3>{{ __("microsub.search.title") }}</h3>
31
+ <ul class="search__list">
32
+ {% for result in results %}
33
+ <li class="search__item">
34
+ <div class="search__feed">
35
+ <span class="search__url">{{ result.url }}</span>
36
+ </div>
37
+ <form method="post" action="{{ baseUrl }}/subscribe" class="search__subscribe">
38
+ <input type="hidden" name="url" value="{{ result.url }}">
39
+ <label for="channel-{{ loop.index }}" class="visually-hidden">{{ __("microsub.channels.title") }}</label>
40
+ <select name="channel" id="channel-{{ loop.index }}" class="select select--small">
41
+ {% for channel in channels %}
42
+ <option value="{{ channel.uid }}">{{ channel.name }}</option>
43
+ {% endfor %}
44
+ </select>
45
+ {{ button({
46
+ text: __("microsub.feeds.follow"),
47
+ classes: "button--small"
48
+ }) }}
49
+ </form>
50
+ </li>
51
+ {% endfor %}
52
+ </ul>
53
+ </div>
54
+ {% elif searched %}
55
+ {{ prose({ text: __("microsub.search.noResults") }) }}
56
+ {% endif %}
57
+ </div>
58
+ {% endblock %}