@chrisburnell/eleventy-cache-webmentions 2.3.2 → 2.3.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/README.md +25 -4
- package/eleventy-cache-webmentions.cjs +4 -688
- package/eleventy-cache-webmentions.js +40 -675
- package/package.json +8 -6
- package/src/core.cjs +708 -0
- package/.cache/eleventy-fetch-68a403136c61abd104ade94e69ba62 +0 -1
- package/.cache/eleventy-fetch-68a403136c61abd104ade94e69ba62.json +0 -1
- package/eleventy-cache-webmentions.min.cjs +0 -1
- package/eleventy-cache-webmentions.min.js +0 -1
- package/eslint.config.js +0 -42
package/src/core.cjs
ADDED
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} AllowedHTML
|
|
3
|
+
* @property {Array<string>} [allowedTags]
|
|
4
|
+
* @property {{[attribute: string]: Array<string>}} [allowedAttributes]
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} OptionsDefaults
|
|
9
|
+
* @property {boolean} refresh
|
|
10
|
+
* @property {string} duration
|
|
11
|
+
* @property {string} uniqueKey
|
|
12
|
+
* @property {string} [cacheDirectory]
|
|
13
|
+
* @property {AllowedHTML} allowedHTML
|
|
14
|
+
* @property {Array<string>} allowlist
|
|
15
|
+
* @property {Array<string>} blocklist
|
|
16
|
+
* @property {{[key: string]: string}} urlReplacements
|
|
17
|
+
* @property {number} maximumHtmlLength
|
|
18
|
+
* @property {string} maximumHtmlText
|
|
19
|
+
* @property {boolean} paginate
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} OptionsUserInput
|
|
24
|
+
* @property {string} domain
|
|
25
|
+
* @property {string} feed
|
|
26
|
+
* @property {string} key
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {OptionsDefaults & OptionsUserInput} Options
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {object} Webmention
|
|
35
|
+
* @property {string} [source]
|
|
36
|
+
* @property {string} [url]
|
|
37
|
+
* @property {string} [target]
|
|
38
|
+
* @property {string} [published]
|
|
39
|
+
* @property {string} [contentSanitized]
|
|
40
|
+
* @property {object} [content]
|
|
41
|
+
* @property {string} [content.html]
|
|
42
|
+
* @property {string} [content.value]
|
|
43
|
+
* @property {object} [data]
|
|
44
|
+
* @property {string} [data.title]
|
|
45
|
+
* @property {string} [data.url]
|
|
46
|
+
* @property {string} [data.published]
|
|
47
|
+
* @property {string} [data.content]
|
|
48
|
+
* @property {string} [type]
|
|
49
|
+
* @property {object} [activity]
|
|
50
|
+
* @property {string} [activity.type]
|
|
51
|
+
* @property {boolean} [verified]
|
|
52
|
+
* @property {string} ["wm-property"]
|
|
53
|
+
* @property {string} ["wm-received"]
|
|
54
|
+
* @property {string} ["wm-source"]
|
|
55
|
+
* @property {string} ["wm-target"]
|
|
56
|
+
* @property {string} ["verified_date"]
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {"bookmark-of"|"like-of"|"repost-of"|"mention-of"|"in-reply-to"} WebmentionType
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {object} deps
|
|
65
|
+
* @param {typeof import("@11ty/eleventy-fetch").AssetCache} deps.AssetCache
|
|
66
|
+
* @param {typeof import("node:util").styleText} deps.styleText
|
|
67
|
+
* @param {typeof import("sanitize-html")} deps.sanitizeHTML
|
|
68
|
+
* @returns {object}
|
|
69
|
+
*/
|
|
70
|
+
module.exports = ({ AssetCache, styleText, sanitizeHTML }) => {
|
|
71
|
+
/**
|
|
72
|
+
* @type {OptionsDefaults}
|
|
73
|
+
*/
|
|
74
|
+
const defaults = {
|
|
75
|
+
refresh: false,
|
|
76
|
+
duration: "1d",
|
|
77
|
+
uniqueKey: "webmentions",
|
|
78
|
+
allowedHTML: {
|
|
79
|
+
allowedTags: ["a", "b", "em", "i", "strong"],
|
|
80
|
+
allowedAttributes: {
|
|
81
|
+
a: ["href"],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
allowlist: [],
|
|
85
|
+
blocklist: [],
|
|
86
|
+
urlReplacements: {},
|
|
87
|
+
maximumHtmlLength: 1000,
|
|
88
|
+
maximumHtmlText: "mentioned this in",
|
|
89
|
+
paginate: false,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string} url
|
|
94
|
+
* @param {string} domain
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
const absoluteURL = (url, domain) => {
|
|
98
|
+
try {
|
|
99
|
+
return new URL(url, domain).toString();
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error(
|
|
102
|
+
`Trying to convert ${styleText(
|
|
103
|
+
"bold",
|
|
104
|
+
url,
|
|
105
|
+
)} to be an absolute url with base ${styleText(
|
|
106
|
+
"bold",
|
|
107
|
+
domain,
|
|
108
|
+
)} and failed.`,
|
|
109
|
+
error,
|
|
110
|
+
);
|
|
111
|
+
return url;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {string} url
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
const baseURL = (url) => {
|
|
120
|
+
let hashSplit = url.split("#");
|
|
121
|
+
let queryparamSplit = hashSplit[0].split("?");
|
|
122
|
+
return queryparamSplit[0];
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} url
|
|
127
|
+
* @param {{[key: string]: string}} [urlReplacements]
|
|
128
|
+
* @returns {string}
|
|
129
|
+
*/
|
|
130
|
+
const fixURL = (url, urlReplacements) => {
|
|
131
|
+
return Object.entries(urlReplacements).reduce(
|
|
132
|
+
(accumulator, [key, value]) => accumulator.replaceAll(key, value),
|
|
133
|
+
url,
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {string} url
|
|
139
|
+
* @returns {string}
|
|
140
|
+
*/
|
|
141
|
+
const hostname = (url) => {
|
|
142
|
+
if (typeof url === "string" && url.includes("//")) {
|
|
143
|
+
const urlObject = new URL(url);
|
|
144
|
+
return urlObject.hostname;
|
|
145
|
+
}
|
|
146
|
+
return url;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {string|number|Date} date
|
|
151
|
+
* @returns {number}
|
|
152
|
+
*/
|
|
153
|
+
const epoch = (date) => {
|
|
154
|
+
return new Date(date).getTime();
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {Array<Webmention>} webmentions
|
|
159
|
+
* @returns {Array<Webmention>}
|
|
160
|
+
*/
|
|
161
|
+
const removeDuplicates = (webmentions) => {
|
|
162
|
+
return [
|
|
163
|
+
...webmentions
|
|
164
|
+
.reduce((map, webmention) => {
|
|
165
|
+
const key =
|
|
166
|
+
webmention === null || webmention === undefined
|
|
167
|
+
? webmention
|
|
168
|
+
: getUniqueKey(webmention);
|
|
169
|
+
if (!map.has(key)) {
|
|
170
|
+
map.set(key, webmention);
|
|
171
|
+
}
|
|
172
|
+
return map;
|
|
173
|
+
}, new Map())
|
|
174
|
+
.values(),
|
|
175
|
+
];
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {Webmention} webmention
|
|
180
|
+
* @returns {string|undefined}
|
|
181
|
+
*/
|
|
182
|
+
const getPublished = (webmention) => {
|
|
183
|
+
return (
|
|
184
|
+
webmention?.["data"]?.["published"] ||
|
|
185
|
+
webmention["published"] ||
|
|
186
|
+
webmention["wm-received"] ||
|
|
187
|
+
webmention["verified_date"]
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
const getWebmentionPublished = getPublished;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @param {Webmention} webmention
|
|
194
|
+
* @returns {string|undefined}
|
|
195
|
+
*/
|
|
196
|
+
const getReceived = (webmention) => {
|
|
197
|
+
return (
|
|
198
|
+
webmention["wm-received"] ||
|
|
199
|
+
webmention["verified_date"] ||
|
|
200
|
+
webmention["published"] ||
|
|
201
|
+
webmention?.["data"]?.["published"]
|
|
202
|
+
);
|
|
203
|
+
};
|
|
204
|
+
const getWebmentionReceived = getReceived;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @param {Webmention} webmention
|
|
208
|
+
* @returns {string}
|
|
209
|
+
*/
|
|
210
|
+
const getContent = (webmention) => {
|
|
211
|
+
return (
|
|
212
|
+
webmention?.["contentSanitized"] ||
|
|
213
|
+
webmention?.["content"]?.["html"] ||
|
|
214
|
+
webmention?.["content"]?.["value"] ||
|
|
215
|
+
webmention?.["content"] ||
|
|
216
|
+
webmention?.["data"]?.["content"] ||
|
|
217
|
+
""
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
const getWebmentionContent = getContent;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @param {Webmention} webmention
|
|
224
|
+
* @returns {string|undefined}
|
|
225
|
+
*/
|
|
226
|
+
const getSource = (webmention) => {
|
|
227
|
+
return (
|
|
228
|
+
webmention["wm-source"] ||
|
|
229
|
+
webmention["source"] ||
|
|
230
|
+
webmention?.["data"]?.["url"] ||
|
|
231
|
+
webmention["url"]
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
const getWebmentionSource = getSource;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* @param {Webmention} webmention
|
|
238
|
+
* @returns {string|undefined}
|
|
239
|
+
*/
|
|
240
|
+
const getURL = (webmention) => {
|
|
241
|
+
return (
|
|
242
|
+
webmention?.["data"]?.["url"] ||
|
|
243
|
+
webmention["url"] ||
|
|
244
|
+
webmention["wm-source"] ||
|
|
245
|
+
webmention["source"]
|
|
246
|
+
);
|
|
247
|
+
};
|
|
248
|
+
const getWebmentionURL = getURL;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @param {Webmention} webmention
|
|
252
|
+
* @returns {string|undefined}
|
|
253
|
+
*/
|
|
254
|
+
const getTarget = (webmention) => {
|
|
255
|
+
return webmention["wm-target"] || webmention["target"];
|
|
256
|
+
};
|
|
257
|
+
const getWebmentionTarget = getTarget;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* @param {Webmention} webmention
|
|
261
|
+
* @returns {string}
|
|
262
|
+
*/
|
|
263
|
+
const getUniqueKey = (webmention) => {
|
|
264
|
+
return JSON.stringify([getSource(webmention), getTarget(webmention)]);
|
|
265
|
+
};
|
|
266
|
+
const getWebmentionUniqueKey = getUniqueKey;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* @param {Webmention} webmention
|
|
270
|
+
* @returns {string|undefined}
|
|
271
|
+
*/
|
|
272
|
+
const getType = (webmention) => {
|
|
273
|
+
return (
|
|
274
|
+
webmention["wm-property"] ||
|
|
275
|
+
webmention?.["activity"]?.["type"] ||
|
|
276
|
+
webmention["type"]
|
|
277
|
+
);
|
|
278
|
+
};
|
|
279
|
+
const getWebmentionType = getType;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* @param {Array<Webmention>} webmentions
|
|
283
|
+
* @param {WebmentionType|Array<WebmentionType>} types
|
|
284
|
+
* @returns {Array<Webmention>}
|
|
285
|
+
*/
|
|
286
|
+
const getByTypes = (webmentions, types) => {
|
|
287
|
+
return webmentions.filter((webmention) => {
|
|
288
|
+
if (typeof types === "string") {
|
|
289
|
+
return types === getType(webmention);
|
|
290
|
+
}
|
|
291
|
+
return types.includes(getType(webmention));
|
|
292
|
+
});
|
|
293
|
+
};
|
|
294
|
+
const getByType = getByTypes;
|
|
295
|
+
const getWebmentionsByTypes = getByTypes;
|
|
296
|
+
const getWebmentionsByType = getByTypes;
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* @param {Array<Webmention>} webmentions
|
|
300
|
+
* @param {Array<string>} blocklist
|
|
301
|
+
* @returns {Array<Webmention>}
|
|
302
|
+
*/
|
|
303
|
+
const processBlocklist = (webmentions, blocklist) => {
|
|
304
|
+
return webmentions.filter((webmention) => {
|
|
305
|
+
let url = getSource(webmention);
|
|
306
|
+
let source = getSource(webmention);
|
|
307
|
+
for (let blocklistURL of blocklist) {
|
|
308
|
+
if (
|
|
309
|
+
url.includes(blocklistURL.replace(/\/?$/, "/")) ||
|
|
310
|
+
source.includes(blocklistURL.replace(/\/?$/, "/"))
|
|
311
|
+
) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return true;
|
|
316
|
+
});
|
|
317
|
+
};
|
|
318
|
+
const processWebmentionBlocklist = processBlocklist;
|
|
319
|
+
const processWebmentionsBlocklist = processBlocklist;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* @param {Array<Webmention>} webmentions
|
|
323
|
+
* @param {Array<string>} allowlist
|
|
324
|
+
* @returns {Array<Webmention>}
|
|
325
|
+
*/
|
|
326
|
+
const processAllowlist = (webmentions, allowlist) => {
|
|
327
|
+
return webmentions.filter((webmention) => {
|
|
328
|
+
let url = getSource(webmention);
|
|
329
|
+
let source = getSource(webmention);
|
|
330
|
+
for (let allowlistURL of allowlist) {
|
|
331
|
+
if (
|
|
332
|
+
url.includes(allowlistURL.replace(/\/?$/, "/")) ||
|
|
333
|
+
source.includes(allowlistURL.replace(/\/?$/, "/"))
|
|
334
|
+
) {
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
});
|
|
340
|
+
};
|
|
341
|
+
const processWebmentionAllowlist = processAllowlist;
|
|
342
|
+
const processWebmentionsAllowlist = processAllowlist;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* @param {Options} options
|
|
346
|
+
* @param {Array<Webmention>} webmentions
|
|
347
|
+
* @param {string} url
|
|
348
|
+
* @returns {Promise<{found: number, webmentions: Array<Webmention>}>}
|
|
349
|
+
*/
|
|
350
|
+
const fetchWebmentions = async (options, webmentions, url) => {
|
|
351
|
+
return await fetch(url)
|
|
352
|
+
.then(async (response) => {
|
|
353
|
+
if (!response.ok) {
|
|
354
|
+
return Promise.reject(response);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const feed = await response.json();
|
|
358
|
+
|
|
359
|
+
if (!(options.key in feed)) {
|
|
360
|
+
console.log(
|
|
361
|
+
`${styleText("gray", `[${hostname(options.domain)}]`)} ${
|
|
362
|
+
options.key
|
|
363
|
+
} was not found as a key in the response from ${styleText(
|
|
364
|
+
"bold",
|
|
365
|
+
hostname(options.feed),
|
|
366
|
+
)}!`,
|
|
367
|
+
);
|
|
368
|
+
return Promise.reject(response);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Fetched Webmentions replace cached ones with the same
|
|
372
|
+
// source / target pair
|
|
373
|
+
const fetchedKeys = new Set(feed[options.key].map(getUniqueKey));
|
|
374
|
+
webmentions = [
|
|
375
|
+
...feed[options.key],
|
|
376
|
+
...webmentions.filter((wm) => !fetchedKeys.has(getUniqueKey(wm))),
|
|
377
|
+
];
|
|
378
|
+
// Remove any remaining duplicates by source / target pair
|
|
379
|
+
webmentions = removeDuplicates(webmentions);
|
|
380
|
+
// Process the blocklist, if it has any entries
|
|
381
|
+
if (options.blocklist.length) {
|
|
382
|
+
webmentions = processBlocklist(webmentions, options.blocklist);
|
|
383
|
+
}
|
|
384
|
+
// Process the allowlist, if it has any entries
|
|
385
|
+
if (options.allowlist.length) {
|
|
386
|
+
webmentions = processAllowlist(webmentions, options.allowlist);
|
|
387
|
+
}
|
|
388
|
+
// Sort webmentions by received date for getting most recent Webmention on subsequent requests
|
|
389
|
+
webmentions = webmentions.sort((a, b) => {
|
|
390
|
+
return epoch(getReceived(b)) - epoch(getReceived(a));
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
found: feed[options.key].length,
|
|
395
|
+
webmentions: webmentions,
|
|
396
|
+
};
|
|
397
|
+
})
|
|
398
|
+
.catch((error) => {
|
|
399
|
+
console.warn(
|
|
400
|
+
`${styleText(
|
|
401
|
+
"gray",
|
|
402
|
+
`[${hostname(options.domain)}]`,
|
|
403
|
+
)} Something went wrong with your Webmention request to ${styleText(
|
|
404
|
+
"bold",
|
|
405
|
+
hostname(options.feed),
|
|
406
|
+
)}!`,
|
|
407
|
+
);
|
|
408
|
+
console.warn(error instanceof Error ? error.message : error);
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
found: 0,
|
|
412
|
+
webmentions: webmentions,
|
|
413
|
+
};
|
|
414
|
+
});
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* @param {Options} options
|
|
419
|
+
* @returns {Promise<Array<Webmention>>}
|
|
420
|
+
*/
|
|
421
|
+
const retrieveWebmentions = async (options) => {
|
|
422
|
+
if (!options.domain) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
"`domain` is a required field when attempting to retrieve Webmentions. See https://www.npmjs.com/package/@chrisburnell/eleventy-cache-webmentions#installation for more information.",
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!options.feed) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
"`feed` is a required field when attempting to retrieve Webmentions. See https://www.npmjs.com/package/@chrisburnell/eleventy-cache-webmentions#installation for more information.",
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (!options.key) {
|
|
435
|
+
throw new Error(
|
|
436
|
+
"`key` is a required field when attempting to retrieve Webmentions. See https://www.npmjs.com/package/@chrisburnell/eleventy-cache-webmentions#installation for more information.",
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
let asset = new AssetCache(
|
|
441
|
+
options.uniqueKey || `webmentions-${hostname(options.domain)}`,
|
|
442
|
+
options.cacheDirectory,
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
let webmentions = [];
|
|
446
|
+
|
|
447
|
+
// Unless specifically getting fresh Webmentions, if there is a cached file
|
|
448
|
+
// at all, grab its contents now
|
|
449
|
+
if (asset.isCacheValid("*") && !options.refresh) {
|
|
450
|
+
webmentions = await asset.getCachedValue();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Get the number of cached Webmentions for diffing against fetched
|
|
454
|
+
// Webmentions later
|
|
455
|
+
const webmentionsCachedLength = webmentions.length;
|
|
456
|
+
|
|
457
|
+
// If there is a cached file but it is outside of expiry, fetch fresh
|
|
458
|
+
// results since the most recent Webmention
|
|
459
|
+
if (!asset.isCacheValid(options.refresh ? "0s" : options.duration)) {
|
|
460
|
+
const performanceStart = process.hrtime();
|
|
461
|
+
// Get the received date of the most recent Webmention, if it exists
|
|
462
|
+
const since = webmentions.length ? getReceived(webmentions[0]) : false;
|
|
463
|
+
// Build the URL for the fetch request
|
|
464
|
+
const url = `${options.feed}${
|
|
465
|
+
since ? `${options.feed.includes("?") ? "&" : "?"}since=${since}` : ""
|
|
466
|
+
}`;
|
|
467
|
+
|
|
468
|
+
// If using webmention.io or paginate or per-page in URL
|
|
469
|
+
const urlObject = new URL(url);
|
|
470
|
+
if (
|
|
471
|
+
url.includes("https://webmention.io") ||
|
|
472
|
+
options.paginate ||
|
|
473
|
+
urlObject.searchParams.has("per-page")
|
|
474
|
+
) {
|
|
475
|
+
const perPage = Number(urlObject.searchParams.get("per-page")) || 1000;
|
|
476
|
+
urlObject.searchParams.delete("per-page");
|
|
477
|
+
// Start on page 0, to increment per subsequent request
|
|
478
|
+
let page = 0;
|
|
479
|
+
// Loop until a break condition is hit
|
|
480
|
+
while (true) {
|
|
481
|
+
const urlPaginated =
|
|
482
|
+
urlObject.href + `&per-page=${perPage}&page=${page}`;
|
|
483
|
+
const fetched = await fetchWebmentions(
|
|
484
|
+
options,
|
|
485
|
+
webmentions,
|
|
486
|
+
urlPaginated,
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
// Page has no Webmentions or an error occurred → break
|
|
490
|
+
if (fetched.found === 0) {
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
webmentions = fetched.webmentions;
|
|
495
|
+
|
|
496
|
+
// If there are less Webmentions found than should be in each
|
|
497
|
+
// page → break
|
|
498
|
+
if (fetched.found < perPage) {
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Increment page
|
|
503
|
+
page += 1;
|
|
504
|
+
// Throttle next request
|
|
505
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
506
|
+
}
|
|
507
|
+
} else {
|
|
508
|
+
const fetched = await fetchWebmentions(options, webmentions, url);
|
|
509
|
+
webmentions = fetched.webmentions;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Process the blocklist, if it has any entries
|
|
513
|
+
if (options.blocklist.length) {
|
|
514
|
+
webmentions = processBlocklist(webmentions, options.blocklist);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Process the allowlist, if it has any entries
|
|
518
|
+
if (options.allowlist.length) {
|
|
519
|
+
webmentions = processAllowlist(webmentions, options.allowlist);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
await asset.save(webmentions, "json");
|
|
523
|
+
|
|
524
|
+
const performance = process.hrtime(performanceStart);
|
|
525
|
+
|
|
526
|
+
// Add a console message with the number of fetched and processed Webmentions, if any
|
|
527
|
+
if (webmentionsCachedLength < webmentions.length) {
|
|
528
|
+
console.log(
|
|
529
|
+
`${styleText(
|
|
530
|
+
"gray",
|
|
531
|
+
`[${hostname(options.domain)}]`,
|
|
532
|
+
)} ${styleText(
|
|
533
|
+
"bold",
|
|
534
|
+
String(webmentions.length - webmentionsCachedLength),
|
|
535
|
+
)} new Webmentions fetched into cache in ${styleText(
|
|
536
|
+
"bold",
|
|
537
|
+
(performance[0] + performance[1] / 1e9).toFixed(3) + " seconds",
|
|
538
|
+
)}.`,
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return webmentions;
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const webmentionsByURLCache = new Map();
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* @param {Options} options
|
|
550
|
+
* @returns {Promise<{[key: string]: Array<Webmention>}>}
|
|
551
|
+
*/
|
|
552
|
+
const webmentionsByURL = async (options) => {
|
|
553
|
+
const cacheKey = JSON.stringify([
|
|
554
|
+
options.domain,
|
|
555
|
+
options.feed,
|
|
556
|
+
options.key,
|
|
557
|
+
options.uniqueKey,
|
|
558
|
+
options.refresh,
|
|
559
|
+
]);
|
|
560
|
+
|
|
561
|
+
if (webmentionsByURLCache.has(cacheKey)) {
|
|
562
|
+
return webmentionsByURLCache.get(cacheKey);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const promise = retrieveWebmentions(options).then((rawWebmentions) => {
|
|
566
|
+
// Fix local URLs based on urlReplacements and sort Webmentions into
|
|
567
|
+
// groups by target base URL
|
|
568
|
+
const webmentions = {};
|
|
569
|
+
rawWebmentions.forEach((webmention) => {
|
|
570
|
+
const target = getTarget(webmention);
|
|
571
|
+
if (!target) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
let url = baseURL(
|
|
576
|
+
fixURL(target.replace(/\/?$/, "/"), options.urlReplacements),
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
if (!webmentions[url]) {
|
|
580
|
+
webmentions[url] = [];
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
webmentions[url].push(webmention);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
return webmentions;
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
webmentionsByURLCache.set(cacheKey, promise);
|
|
590
|
+
|
|
591
|
+
return promise;
|
|
592
|
+
};
|
|
593
|
+
const webmentionsByUrl = webmentionsByURL;
|
|
594
|
+
const filteredWebmentions = webmentionsByURL;
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* @param {Options} options
|
|
598
|
+
* @param {string} url
|
|
599
|
+
* @param {WebmentionType|Array<WebmentionType>} [types]
|
|
600
|
+
* @returns {Promise<Array<Webmention>>}
|
|
601
|
+
*/
|
|
602
|
+
const getWebmentions = async (options, url, types = []) => {
|
|
603
|
+
const webmentions = await webmentionsByURL(options);
|
|
604
|
+
url = absoluteURL(url, options.domain);
|
|
605
|
+
|
|
606
|
+
if (!url || !webmentions || !webmentions[url]) {
|
|
607
|
+
return [];
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return webmentions[url]
|
|
611
|
+
// Filter webmentions by allowed response post types
|
|
612
|
+
.filter((entry) => {
|
|
613
|
+
return typeof types === "object" && Object.keys(types).length
|
|
614
|
+
? types.includes(getType(entry))
|
|
615
|
+
: typeof types === "string"
|
|
616
|
+
? types === getType(entry)
|
|
617
|
+
: true;
|
|
618
|
+
})
|
|
619
|
+
// Sanitize content of webmentions against HTML limit
|
|
620
|
+
.map((entry) => {
|
|
621
|
+
const html = getContent(entry);
|
|
622
|
+
|
|
623
|
+
if (html.length) {
|
|
624
|
+
entry.contentSanitized = sanitizeHTML(html, options.allowedHTML);
|
|
625
|
+
if (html.length > options.maximumHtmlLength) {
|
|
626
|
+
entry.contentSanitized = `${
|
|
627
|
+
options.maximumHtmlText
|
|
628
|
+
} <a href="${getSource(entry)}">${getSource(entry)}</a>`;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return entry;
|
|
633
|
+
})
|
|
634
|
+
// Sort by published
|
|
635
|
+
.sort((a, b) => {
|
|
636
|
+
return epoch(getPublished(a)) - epoch(getPublished(b));
|
|
637
|
+
});
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* @param {object} eleventyConfig
|
|
642
|
+
* @param {Options} [options]
|
|
643
|
+
*/
|
|
644
|
+
const eleventyCacheWebmentions = async (eleventyConfig, options = {}) => {
|
|
645
|
+
options = Object.assign({}, defaults, options);
|
|
646
|
+
|
|
647
|
+
const byURL = await webmentionsByURL(options);
|
|
648
|
+
const all = Object.values(byURL).reduce(
|
|
649
|
+
(array, webmentions) => [...array, ...webmentions],
|
|
650
|
+
[],
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
// Global Data
|
|
654
|
+
eleventyConfig.addGlobalData("webmentionsDefaults", defaults);
|
|
655
|
+
eleventyConfig.addGlobalData("webmentionsOptions", options);
|
|
656
|
+
eleventyConfig.addGlobalData("webmentionsByURL", byURL);
|
|
657
|
+
eleventyConfig.addGlobalData("webmentionsByUrl", byURL);
|
|
658
|
+
eleventyConfig.addGlobalData("webmentionsAll", all);
|
|
659
|
+
|
|
660
|
+
// Universal Filters
|
|
661
|
+
eleventyConfig.addFilter("getWebmentionsByType", getByTypes);
|
|
662
|
+
eleventyConfig.addFilter("getWebmentionsByTypes", getByTypes);
|
|
663
|
+
eleventyConfig.addFilter("getWebmentionPublished", getPublished);
|
|
664
|
+
eleventyConfig.addFilter("getWebmentionReceived", getReceived);
|
|
665
|
+
eleventyConfig.addFilter("getWebmentionContent", getContent);
|
|
666
|
+
eleventyConfig.addFilter("getWebmentionSource", getSource);
|
|
667
|
+
eleventyConfig.addFilter("getWebmentionURL", getURL);
|
|
668
|
+
eleventyConfig.addFilter("getWebmentionTarget", getTarget);
|
|
669
|
+
eleventyConfig.addFilter("getWebmentionType", getType);
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
return {
|
|
673
|
+
defaults,
|
|
674
|
+
getPublished,
|
|
675
|
+
getWebmentionPublished,
|
|
676
|
+
getReceived,
|
|
677
|
+
getWebmentionReceived,
|
|
678
|
+
getContent,
|
|
679
|
+
getWebmentionContent,
|
|
680
|
+
getSource,
|
|
681
|
+
getWebmentionSource,
|
|
682
|
+
getURL,
|
|
683
|
+
getWebmentionURL,
|
|
684
|
+
getTarget,
|
|
685
|
+
getWebmentionTarget,
|
|
686
|
+
getUniqueKey,
|
|
687
|
+
getWebmentionUniqueKey,
|
|
688
|
+
getType,
|
|
689
|
+
getWebmentionType,
|
|
690
|
+
getByTypes,
|
|
691
|
+
getByType,
|
|
692
|
+
getWebmentionsByTypes,
|
|
693
|
+
getWebmentionsByType,
|
|
694
|
+
processBlocklist,
|
|
695
|
+
processWebmentionBlocklist,
|
|
696
|
+
processWebmentionsBlocklist,
|
|
697
|
+
processAllowlist,
|
|
698
|
+
processWebmentionAllowlist,
|
|
699
|
+
processWebmentionsAllowlist,
|
|
700
|
+
fetchWebmentions,
|
|
701
|
+
retrieveWebmentions,
|
|
702
|
+
webmentionsByURL,
|
|
703
|
+
webmentionsByUrl,
|
|
704
|
+
filteredWebmentions,
|
|
705
|
+
getWebmentions,
|
|
706
|
+
eleventyCacheWebmentions,
|
|
707
|
+
};
|
|
708
|
+
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"cachedAt":1784559047504,"type":"json","metadata":{}}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[{"type":"entry","author":{"type":"card","name":"Jane Doe","url":"https://example.com"},"url":"https://example.com/post1/","published":"2024-01-01T12:00:00Z","wm-received":"2024-01-01T12:00:00Z","wm-id":123456,"wm-source":"https://example.com/post1/","wm-target":"https://example.com/page1/","wm-protocol":"webmention","name":"Example Post","content":{"content-type":"text/html","value":"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>","html":"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit."},"mention-of":"https://example.com/page1/","wm-property":"mention-of","wm-private":false},{"type":"entry","author":{"type":"card","name":"Jane Doe","url":"https://example.com"},"url":"https://example.com/post2/","published":"2024-01-01T12:00:00Z","wm-received":"2024-01-01T12:00:00Z","wm-id":234567,"wm-source":"https://example.com/post2/","wm-target":"https://example.com/page2/","wm-protocol":"webmention","name":"Example Post","content":{"content-type":"text/html","value":"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>","html":"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit."},"in-reply-to":"https://example.com/page2/","wm-property":"in-reply-to","wm-private":false},{"type":"entry","author":{"type":"card","name":"Jane Doe","url":"https://example.com"},"url":"https://example.com/post3/","published":"2024-01-01T12:00:00Z","wm-received":"2024-01-01T12:00:00Z","wm-id":345678,"wm-source":"https://example.com/post3/","wm-target":"https://example.com/page2/","wm-protocol":"webmention","name":"Example Post","content":{"content-type":"text/html","value":"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>","html":"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>","text":"Lorem ipsum dolor sit amet, consectetur adipiscing elit."},"mention-of":"https://example.com/page2/","wm-property":"mention-of","wm-private":false}]
|