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

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;
@@ -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.8",
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 %}