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