@rmdes/indiekit-endpoint-blogroll 1.0.3 → 1.0.4
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 +3 -0
- package/lib/controllers/api.js +22 -0
- package/lib/utils/feed-discovery.js +164 -0
- package/locales/en.json +11 -0
- package/package.json +1 -1
- package/views/blogroll-blog-edit.njk +227 -0
package/index.js
CHANGED
|
@@ -86,6 +86,9 @@ export default class BlogrollEndpoint {
|
|
|
86
86
|
protectedRouter.post("/blogs/:id/delete", blogsController.remove);
|
|
87
87
|
protectedRouter.post("/blogs/:id/refresh", blogsController.refresh);
|
|
88
88
|
|
|
89
|
+
// Feed discovery (protected to prevent abuse)
|
|
90
|
+
protectedRouter.get("/api/discover", apiController.discover);
|
|
91
|
+
|
|
89
92
|
return protectedRouter;
|
|
90
93
|
}
|
|
91
94
|
|
package/lib/controllers/api.js
CHANGED
|
@@ -8,6 +8,7 @@ import { getBlogs, countBlogs, getBlog, getCategories } from "../storage/blogs.j
|
|
|
8
8
|
import { getItems, getItemsForBlog } from "../storage/items.js";
|
|
9
9
|
import { getSyncStatus } from "../sync/scheduler.js";
|
|
10
10
|
import { generateOpml } from "../sync/opml.js";
|
|
11
|
+
import { discoverFeeds } from "../utils/feed-discovery.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* List blogs with optional filtering
|
|
@@ -185,6 +186,26 @@ async function exportOpmlCategory(request, response) {
|
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Discover feeds from a website URL
|
|
191
|
+
* GET /api/discover?url=...
|
|
192
|
+
*/
|
|
193
|
+
async function discover(request, response) {
|
|
194
|
+
const { url } = request.query;
|
|
195
|
+
|
|
196
|
+
if (!url) {
|
|
197
|
+
return response.status(400).json({ error: "URL parameter required" });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const result = await discoverFeeds(url);
|
|
202
|
+
response.json(result);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error("[Blogroll API] discover error:", error);
|
|
205
|
+
response.status(500).json({ error: "Failed to discover feeds" });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
188
209
|
// Helper functions
|
|
189
210
|
|
|
190
211
|
/**
|
|
@@ -237,4 +258,5 @@ export const apiController = {
|
|
|
237
258
|
status,
|
|
238
259
|
exportOpml,
|
|
239
260
|
exportOpmlCategory,
|
|
261
|
+
discover,
|
|
240
262
|
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RSS/Atom feed discovery from website URLs
|
|
3
|
+
* @module utils/feed-discovery
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Discover RSS/Atom feeds from a website URL
|
|
8
|
+
* @param {string} websiteUrl - The website URL to check
|
|
9
|
+
* @param {number} timeout - Fetch timeout in ms
|
|
10
|
+
* @returns {Promise<object>} Discovery result with feeds array
|
|
11
|
+
*/
|
|
12
|
+
export async function discoverFeeds(websiteUrl, timeout = 10000) {
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// Normalize URL
|
|
18
|
+
let url = websiteUrl.trim();
|
|
19
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
20
|
+
url = "https://" + url;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const response = await fetch(url, {
|
|
24
|
+
signal: controller.signal,
|
|
25
|
+
headers: {
|
|
26
|
+
"User-Agent": "Indiekit-Blogroll/1.0 (Feed Discovery)",
|
|
27
|
+
Accept: "text/html,application/xhtml+xml",
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
return { success: false, error: `HTTP ${response.status}`, feeds: [] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const html = await response.text();
|
|
36
|
+
const feeds = [];
|
|
37
|
+
const baseUrl = new URL(url);
|
|
38
|
+
|
|
39
|
+
// Find <link rel="alternate"> feeds in HTML
|
|
40
|
+
const linkRegex =
|
|
41
|
+
/<link[^>]+rel=["']alternate["'][^>]*>/gi;
|
|
42
|
+
const typeRegex = /type=["']([^"']+)["']/i;
|
|
43
|
+
const hrefRegex = /href=["']([^"']+)["']/i;
|
|
44
|
+
const titleRegex = /title=["']([^"']+)["']/i;
|
|
45
|
+
|
|
46
|
+
const feedTypes = [
|
|
47
|
+
"application/rss+xml",
|
|
48
|
+
"application/atom+xml",
|
|
49
|
+
"application/feed+json",
|
|
50
|
+
"application/json",
|
|
51
|
+
"text/xml",
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
let match;
|
|
55
|
+
while ((match = linkRegex.exec(html)) !== null) {
|
|
56
|
+
const linkTag = match[0];
|
|
57
|
+
const typeMatch = typeRegex.exec(linkTag);
|
|
58
|
+
const hrefMatch = hrefRegex.exec(linkTag);
|
|
59
|
+
|
|
60
|
+
if (hrefMatch) {
|
|
61
|
+
const type = typeMatch ? typeMatch[1].toLowerCase() : "";
|
|
62
|
+
const href = hrefMatch[1];
|
|
63
|
+
const titleMatch = titleRegex.exec(linkTag);
|
|
64
|
+
const title = titleMatch ? titleMatch[1] : null;
|
|
65
|
+
|
|
66
|
+
// Check if it's a feed type
|
|
67
|
+
if (feedTypes.some((ft) => type.includes(ft.split("/")[1]))) {
|
|
68
|
+
// Resolve relative URLs
|
|
69
|
+
const feedUrl = new URL(href, baseUrl).href;
|
|
70
|
+
|
|
71
|
+
feeds.push({
|
|
72
|
+
url: feedUrl,
|
|
73
|
+
type: type.includes("atom")
|
|
74
|
+
? "atom"
|
|
75
|
+
: type.includes("json")
|
|
76
|
+
? "json"
|
|
77
|
+
: "rss",
|
|
78
|
+
title,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Also check common feed paths if no feeds found in HTML
|
|
85
|
+
if (feeds.length === 0) {
|
|
86
|
+
const commonPaths = [
|
|
87
|
+
"/feed",
|
|
88
|
+
"/feed.xml",
|
|
89
|
+
"/rss",
|
|
90
|
+
"/rss.xml",
|
|
91
|
+
"/atom.xml",
|
|
92
|
+
"/feed/atom",
|
|
93
|
+
"/feed/rss",
|
|
94
|
+
"/index.xml",
|
|
95
|
+
"/blog/feed",
|
|
96
|
+
"/blog/rss",
|
|
97
|
+
"/.rss",
|
|
98
|
+
"/feed.json",
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
for (const path of commonPaths) {
|
|
102
|
+
try {
|
|
103
|
+
const feedUrl = new URL(path, baseUrl).href;
|
|
104
|
+
const feedResponse = await fetch(feedUrl, {
|
|
105
|
+
method: "HEAD",
|
|
106
|
+
signal: controller.signal,
|
|
107
|
+
headers: {
|
|
108
|
+
"User-Agent": "Indiekit-Blogroll/1.0 (Feed Discovery)",
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (feedResponse.ok) {
|
|
113
|
+
const contentType = feedResponse.headers.get("content-type") || "";
|
|
114
|
+
if (
|
|
115
|
+
contentType.includes("xml") ||
|
|
116
|
+
contentType.includes("rss") ||
|
|
117
|
+
contentType.includes("atom") ||
|
|
118
|
+
contentType.includes("json")
|
|
119
|
+
) {
|
|
120
|
+
feeds.push({
|
|
121
|
+
url: feedUrl,
|
|
122
|
+
type: contentType.includes("atom")
|
|
123
|
+
? "atom"
|
|
124
|
+
: contentType.includes("json")
|
|
125
|
+
? "json"
|
|
126
|
+
: "rss",
|
|
127
|
+
title: null,
|
|
128
|
+
});
|
|
129
|
+
break; // Found one, stop checking
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
// Ignore individual path errors
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Try to extract page title for blog name
|
|
139
|
+
let pageTitle = null;
|
|
140
|
+
const titleTagMatch = /<title[^>]*>([^<]+)<\/title>/i.exec(html);
|
|
141
|
+
if (titleTagMatch) {
|
|
142
|
+
pageTitle = titleTagMatch[1].trim();
|
|
143
|
+
// Clean up common suffixes
|
|
144
|
+
pageTitle = pageTitle
|
|
145
|
+
.replace(/\s*[-|–—]\s*.*$/, "")
|
|
146
|
+
.replace(/\s*:\s*Home.*$/i, "")
|
|
147
|
+
.trim();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
success: true,
|
|
152
|
+
feeds,
|
|
153
|
+
pageTitle,
|
|
154
|
+
siteUrl: baseUrl.origin,
|
|
155
|
+
};
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error.name === "AbortError") {
|
|
158
|
+
return { success: false, error: "Request timed out", feeds: [] };
|
|
159
|
+
}
|
|
160
|
+
return { success: false, error: error.message, feeds: [] };
|
|
161
|
+
} finally {
|
|
162
|
+
clearTimeout(timeoutId);
|
|
163
|
+
}
|
|
164
|
+
}
|
package/locales/en.json
CHANGED
|
@@ -91,6 +91,17 @@
|
|
|
91
91
|
"deleted": "Blog deleted successfully.",
|
|
92
92
|
"refreshed": "Blog refreshed. Added %{items} new items.",
|
|
93
93
|
"form": {
|
|
94
|
+
"discoverUrl": "Website URL",
|
|
95
|
+
"discover": "Discover Feed",
|
|
96
|
+
"discoverHint": "Enter a website URL to auto-discover its RSS/Atom feed",
|
|
97
|
+
"discoverNoUrl": "Please enter a website URL",
|
|
98
|
+
"discovering": "Discovering...",
|
|
99
|
+
"discoveringHint": "Checking for RSS/Atom feeds...",
|
|
100
|
+
"discoverFailed": "Failed to discover feeds",
|
|
101
|
+
"discoverNoFeeds": "No feeds found on this website",
|
|
102
|
+
"discoverFoundOne": "Found feed:",
|
|
103
|
+
"discoverFoundMultiple": "Multiple feeds found. Click one to select:",
|
|
104
|
+
"discoverSelected": "Selected feed:",
|
|
94
105
|
"feedUrl": "Feed URL",
|
|
95
106
|
"feedUrlHint": "RSS, Atom, or JSON Feed URL",
|
|
96
107
|
"title": "Title",
|
package/package.json
CHANGED
|
@@ -107,6 +107,93 @@
|
|
|
107
107
|
text-align: center;
|
|
108
108
|
padding: var(--space-m, 1rem);
|
|
109
109
|
}
|
|
110
|
+
|
|
111
|
+
.br-discover-section {
|
|
112
|
+
background: var(--color-offset, #f5f5f5);
|
|
113
|
+
border-radius: var(--border-radius-small, 0.5rem);
|
|
114
|
+
padding: var(--space-m, 1rem);
|
|
115
|
+
margin-block-end: var(--space-m, 1rem);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.br-discover-section .br-field {
|
|
119
|
+
margin-block-end: var(--space-s, 0.75rem);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.br-discover-input {
|
|
123
|
+
display: flex;
|
|
124
|
+
gap: var(--space-s, 0.75rem);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.br-discover-input input {
|
|
128
|
+
flex: 1;
|
|
129
|
+
appearance: none;
|
|
130
|
+
background-color: var(--color-background, #fff);
|
|
131
|
+
border: 1px solid var(--color-outline-variant, #ccc);
|
|
132
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
133
|
+
font: var(--font-body, 0.875rem/1.4 sans-serif);
|
|
134
|
+
padding: calc(var(--space-s, 0.75rem) / 2) var(--space-s, 0.75rem);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.br-discover-result {
|
|
138
|
+
margin-block-start: var(--space-s, 0.75rem);
|
|
139
|
+
padding: var(--space-s, 0.75rem);
|
|
140
|
+
background: var(--color-background, #fff);
|
|
141
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
142
|
+
font: var(--font-caption, 0.875rem/1.4 sans-serif);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.br-discover-result.br-discover-result--error {
|
|
146
|
+
color: var(--color-error, #dc3545);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.br-discover-result.br-discover-result--success {
|
|
150
|
+
color: var(--color-success, #28a745);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.br-discover-feeds {
|
|
154
|
+
list-style: none;
|
|
155
|
+
padding: 0;
|
|
156
|
+
margin: var(--space-xs, 0.5rem) 0 0 0;
|
|
157
|
+
display: flex;
|
|
158
|
+
flex-direction: column;
|
|
159
|
+
gap: var(--space-xs, 0.5rem);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.br-discover-feed {
|
|
163
|
+
display: flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
gap: var(--space-s, 0.75rem);
|
|
166
|
+
padding: var(--space-xs, 0.5rem);
|
|
167
|
+
background: var(--color-offset, #f5f5f5);
|
|
168
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
169
|
+
cursor: pointer;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.br-discover-feed:hover {
|
|
173
|
+
background: var(--color-primary-offset, #e6f0ff);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.br-discover-feed-url {
|
|
177
|
+
flex: 1;
|
|
178
|
+
font-family: monospace;
|
|
179
|
+
font-size: 0.75rem;
|
|
180
|
+
word-break: break-all;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.br-discover-feed-type {
|
|
184
|
+
background: var(--color-primary, #0066cc);
|
|
185
|
+
color: white;
|
|
186
|
+
padding: 0.125rem 0.5rem;
|
|
187
|
+
border-radius: 0.25rem;
|
|
188
|
+
font-size: 0.625rem;
|
|
189
|
+
text-transform: uppercase;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.br-divider {
|
|
193
|
+
border: none;
|
|
194
|
+
border-block-start: 1px solid var(--color-outline-variant, #ddd);
|
|
195
|
+
margin: var(--space-m, 1rem) 0;
|
|
196
|
+
}
|
|
110
197
|
</style>
|
|
111
198
|
|
|
112
199
|
<header class="page-header">
|
|
@@ -121,6 +208,23 @@
|
|
|
121
208
|
{% endfor %}
|
|
122
209
|
|
|
123
210
|
<form method="post" action="{% if isNew %}{{ baseUrl }}/blogs{% else %}{{ baseUrl }}/blogs/{{ blog._id }}{% endif %}" class="br-form">
|
|
211
|
+
{% if isNew %}
|
|
212
|
+
<div class="br-discover-section">
|
|
213
|
+
<div class="br-field">
|
|
214
|
+
<label for="discoverUrl">{{ __("blogroll.blogs.form.discoverUrl") }}</label>
|
|
215
|
+
<div class="br-discover-input">
|
|
216
|
+
<input type="url" id="discoverUrl" placeholder="https://tantek.com">
|
|
217
|
+
<button type="button" id="discoverBtn" class="button button--secondary">
|
|
218
|
+
{{ __("blogroll.blogs.form.discover") }}
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
221
|
+
<span class="br-field-hint">{{ __("blogroll.blogs.form.discoverHint") }}</span>
|
|
222
|
+
</div>
|
|
223
|
+
<div id="discoverResult" class="br-discover-result" style="display: none;"></div>
|
|
224
|
+
</div>
|
|
225
|
+
<hr class="br-divider">
|
|
226
|
+
{% endif %}
|
|
227
|
+
|
|
124
228
|
<div class="br-field">
|
|
125
229
|
<label for="feedUrl">{{ __("blogroll.blogs.form.feedUrl") }}</label>
|
|
126
230
|
<input type="url" id="feedUrl" name="feedUrl" value="{{ blog.feedUrl if blog else '' }}" required placeholder="https://example.com/feed.xml">
|
|
@@ -196,4 +300,127 @@
|
|
|
196
300
|
{% endif %}
|
|
197
301
|
</div>
|
|
198
302
|
{% endif %}
|
|
303
|
+
|
|
304
|
+
{% if isNew %}
|
|
305
|
+
<script>
|
|
306
|
+
(function() {
|
|
307
|
+
const discoverBtn = document.getElementById('discoverBtn');
|
|
308
|
+
const discoverUrl = document.getElementById('discoverUrl');
|
|
309
|
+
const discoverResult = document.getElementById('discoverResult');
|
|
310
|
+
const feedUrlInput = document.getElementById('feedUrl');
|
|
311
|
+
const titleInput = document.getElementById('title');
|
|
312
|
+
const siteUrlInput = document.getElementById('siteUrl');
|
|
313
|
+
|
|
314
|
+
function showResult(message, isError, isSuccess) {
|
|
315
|
+
discoverResult.style.display = 'block';
|
|
316
|
+
discoverResult.className = 'br-discover-result' +
|
|
317
|
+
(isError ? ' br-discover-result--error' : '') +
|
|
318
|
+
(isSuccess ? ' br-discover-result--success' : '');
|
|
319
|
+
discoverResult.textContent = '';
|
|
320
|
+
|
|
321
|
+
const span = document.createElement('span');
|
|
322
|
+
span.textContent = message;
|
|
323
|
+
discoverResult.appendChild(span);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function showFeedUrl(message, url) {
|
|
327
|
+
discoverResult.style.display = 'block';
|
|
328
|
+
discoverResult.className = 'br-discover-result br-discover-result--success';
|
|
329
|
+
discoverResult.textContent = '';
|
|
330
|
+
|
|
331
|
+
const span = document.createElement('span');
|
|
332
|
+
span.textContent = message + ' ';
|
|
333
|
+
discoverResult.appendChild(span);
|
|
334
|
+
|
|
335
|
+
const code = document.createElement('code');
|
|
336
|
+
code.textContent = url;
|
|
337
|
+
discoverResult.appendChild(code);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
discoverBtn.addEventListener('click', async function() {
|
|
341
|
+
const url = discoverUrl.value.trim();
|
|
342
|
+
if (!url) {
|
|
343
|
+
showResult('{{ __("blogroll.blogs.form.discoverNoUrl") }}', true, false);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
discoverBtn.disabled = true;
|
|
348
|
+
discoverBtn.textContent = '{{ __("blogroll.blogs.form.discovering") }}';
|
|
349
|
+
showResult('{{ __("blogroll.blogs.form.discoveringHint") }}', false, false);
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
const response = await fetch('{{ baseUrl }}/api/discover?url=' + encodeURIComponent(url));
|
|
353
|
+
const data = await response.json();
|
|
354
|
+
|
|
355
|
+
if (!data.success) {
|
|
356
|
+
showResult(data.error || '{{ __("blogroll.blogs.form.discoverFailed") }}', true, false);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (data.feeds.length === 0) {
|
|
361
|
+
showResult('{{ __("blogroll.blogs.form.discoverNoFeeds") }}', true, false);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Auto-fill siteUrl and title if available
|
|
366
|
+
if (data.siteUrl) {
|
|
367
|
+
siteUrlInput.value = data.siteUrl;
|
|
368
|
+
}
|
|
369
|
+
if (data.pageTitle && !titleInput.value) {
|
|
370
|
+
titleInput.value = data.pageTitle;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// If only one feed, auto-select it
|
|
374
|
+
if (data.feeds.length === 1) {
|
|
375
|
+
feedUrlInput.value = data.feeds[0].url;
|
|
376
|
+
showFeedUrl('{{ __("blogroll.blogs.form.discoverFoundOne") }}', data.feeds[0].url);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Multiple feeds - let user choose
|
|
381
|
+
showResult('{{ __("blogroll.blogs.form.discoverFoundMultiple") }}', false, true);
|
|
382
|
+
|
|
383
|
+
const feedList = document.createElement('ul');
|
|
384
|
+
feedList.className = 'br-discover-feeds';
|
|
385
|
+
|
|
386
|
+
data.feeds.forEach(function(feed) {
|
|
387
|
+
const li = document.createElement('li');
|
|
388
|
+
li.className = 'br-discover-feed';
|
|
389
|
+
|
|
390
|
+
const typeSpan = document.createElement('span');
|
|
391
|
+
typeSpan.className = 'br-discover-feed-type';
|
|
392
|
+
typeSpan.textContent = feed.type;
|
|
393
|
+
li.appendChild(typeSpan);
|
|
394
|
+
|
|
395
|
+
const urlSpan = document.createElement('span');
|
|
396
|
+
urlSpan.className = 'br-discover-feed-url';
|
|
397
|
+
urlSpan.textContent = feed.url;
|
|
398
|
+
li.appendChild(urlSpan);
|
|
399
|
+
|
|
400
|
+
li.addEventListener('click', function() {
|
|
401
|
+
feedUrlInput.value = feed.url;
|
|
402
|
+
showFeedUrl('{{ __("blogroll.blogs.form.discoverSelected") }}', feed.url);
|
|
403
|
+
});
|
|
404
|
+
feedList.appendChild(li);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
discoverResult.appendChild(feedList);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
showResult(error.message || '{{ __("blogroll.blogs.form.discoverFailed") }}', true, false);
|
|
410
|
+
} finally {
|
|
411
|
+
discoverBtn.disabled = false;
|
|
412
|
+
discoverBtn.textContent = '{{ __("blogroll.blogs.form.discover") }}';
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Allow pressing Enter in the URL field
|
|
417
|
+
discoverUrl.addEventListener('keypress', function(e) {
|
|
418
|
+
if (e.key === 'Enter') {
|
|
419
|
+
e.preventDefault();
|
|
420
|
+
discoverBtn.click();
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
})();
|
|
424
|
+
</script>
|
|
425
|
+
{% endif %}
|
|
199
426
|
{% endblock %}
|