@chrisburnell/eleventy-cache-webmentions 0.0.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/.gitattributes +14 -0
- package/LICENSE +21 -0
- package/README.md +20 -0
- package/eleventy-cache-webmentions.js +181 -0
- package/package.json +28 -0
package/.gitattributes
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# These files are text and should be normalized (convert crlf => lf)
|
|
2
|
+
*.css text
|
|
3
|
+
*.js text
|
|
4
|
+
*.html text
|
|
5
|
+
*.php text
|
|
6
|
+
*.phtml text
|
|
7
|
+
*.json text
|
|
8
|
+
|
|
9
|
+
# Images should be treated as binary
|
|
10
|
+
# (binary is a macro for -text -diff)
|
|
11
|
+
*.png binary
|
|
12
|
+
*.jpeg binary
|
|
13
|
+
*.jpg binary
|
|
14
|
+
*.gif binary
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Chris Burnell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# eleventy-cache-assets
|
|
2
|
+
|
|
3
|
+
> Work-in-progress! ⚠️ Cache webmentions using eleventy-cache-assets and make them available to use in collections, templates, pages, etc.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
- **With npm:** `npm install @chrisburnell/eleventy-cache-webmentions`
|
|
8
|
+
- **Direct download:** [https://github.com/chrisburnell/eleventy-cache-webmentions/archive/master.zip](https://github.com/chrisburnell/eleventy-cache-webmentions/archive/master.zip)
|
|
9
|
+
|
|
10
|
+
## Learn more
|
|
11
|
+
|
|
12
|
+
I wrote more about **eleventy-cache-webmentions** here: [https://chrisburnell.com/eleventy-cache-webmentions/](https://chrisburnell.com/eleventy-cache-webmentions/).
|
|
13
|
+
|
|
14
|
+
## Authors
|
|
15
|
+
|
|
16
|
+
So far, it’s just myself, [Chris Burnell](https://chrisburnell.com), but I welcome collaborators with ideas to bring to the table!
|
|
17
|
+
|
|
18
|
+
## License
|
|
19
|
+
|
|
20
|
+
This project is licensed under an MIT license.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
const fetch = require("node-fetch")
|
|
2
|
+
const sanitizeHTML = require("sanitize-html")
|
|
3
|
+
const uniqBy = require("lodash/uniqBy")
|
|
4
|
+
const { AssetCache } = require("@11ty/eleventy-cache-assets")
|
|
5
|
+
|
|
6
|
+
// Load .env variables with dotenv
|
|
7
|
+
require("dotenv").config()
|
|
8
|
+
const TOKEN = process.env.WEBMENTION_IO_TOKEN
|
|
9
|
+
|
|
10
|
+
const absoluteURL = (url, domain) => {
|
|
11
|
+
try {
|
|
12
|
+
return new URL(url, domain).toString()
|
|
13
|
+
} catch (e) {
|
|
14
|
+
console.log(`Trying to convert ${url} to be an absolute url with base ${domain} and failed.`)
|
|
15
|
+
return url
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const baseUrl = (url) => {
|
|
20
|
+
let hashSplit = url.split("#")
|
|
21
|
+
let queryparamSplit = hashSplit[0].split("?")
|
|
22
|
+
return queryparamSplit[0]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const fixUrl = (url, urlReplacements) => {
|
|
26
|
+
return Object.entries(urlReplacements).reduce((accumulator, [key, value]) => {
|
|
27
|
+
const regex = new RegExp(key, "g")
|
|
28
|
+
return accumulator.replace(regex, value)
|
|
29
|
+
}, url)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const hostname = (value) => {
|
|
33
|
+
if (typeof value === "string" && value.includes("//")) {
|
|
34
|
+
const urlObject = new URL(value)
|
|
35
|
+
return urlObject.hostname
|
|
36
|
+
}
|
|
37
|
+
return value
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const epoch = (value) => {
|
|
41
|
+
return new Date(value).getTime()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = (config, options = {}) => {
|
|
45
|
+
options = Object.assign(
|
|
46
|
+
{
|
|
47
|
+
duration: "23h",
|
|
48
|
+
key: "webmentions",
|
|
49
|
+
allowedHTML: {
|
|
50
|
+
allowedTags: ["b", "i", "em", "strong", "a"],
|
|
51
|
+
allowedAttributes: {
|
|
52
|
+
a: ["href"],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
urlReplacements: [],
|
|
56
|
+
maximumHtmlLength: 2000,
|
|
57
|
+
},
|
|
58
|
+
options
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const fetchWebmentions = async () => {
|
|
62
|
+
let asset = new AssetCache(options.key)
|
|
63
|
+
asset.ensureDir()
|
|
64
|
+
|
|
65
|
+
let webmentions = {
|
|
66
|
+
type: "feed",
|
|
67
|
+
name: "Webmentions",
|
|
68
|
+
children: [],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// If there is a cached file at all, grab its contents now
|
|
72
|
+
if (asset.isCacheValid("9001y")) {
|
|
73
|
+
webmentions = await asset.getCachedValue()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// If there is a cached file but it is outside of expiry, fetch fresh
|
|
77
|
+
// results since the most recent
|
|
78
|
+
if (!asset.isCacheValid(options.duration)) {
|
|
79
|
+
const since = asset._cache.getKey(options.key) ? asset._cache.getKey(options.key).cachedAt : false
|
|
80
|
+
const url = `https://webmention.io/api/mentions.jf2?domain=${hostname(options.domain)}&token=${TOKEN}&per-page=9001${since ? `&since=${since}` : ``}`
|
|
81
|
+
const response = await fetch(url)
|
|
82
|
+
if (response.ok) {
|
|
83
|
+
const feed = await response.json()
|
|
84
|
+
if (feed.children.length) {
|
|
85
|
+
console.log(`[${hostname(options.domain)}] ${feed.children.length} new Webmentions fetched into cache.`)
|
|
86
|
+
}
|
|
87
|
+
webmentions.children = [...feed.children, ...webmentions.children].sort((a, b) => {
|
|
88
|
+
return epoch(b.published || b["wm-received"]) - epoch(a.published || a["wm-received"])
|
|
89
|
+
})
|
|
90
|
+
await asset.save(webmentions, "json")
|
|
91
|
+
return webmentions
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return webmentions
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const filteredWebmentions = async () => {
|
|
99
|
+
const rawWebmentions = await fetchWebmentions()
|
|
100
|
+
let webmentions = {}
|
|
101
|
+
|
|
102
|
+
// Sort Webmentions into groups by target
|
|
103
|
+
rawWebmentions.children.forEach((webmention) => {
|
|
104
|
+
// Get the target of the Webmention and fix it up
|
|
105
|
+
let url = baseUrl(fixUrl(webmention["wm-target"].replace(/\/?$/, "/"), options.urlReplacements))
|
|
106
|
+
|
|
107
|
+
if (!webmentions[url]) {
|
|
108
|
+
webmentions[url] = []
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
webmentions[url].push(webmention)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Sort Webmentions in groups by url and remove duplicates by wm-id
|
|
115
|
+
for (let url in webmentions) {
|
|
116
|
+
webmentions[url] = uniqBy(webmentions[url], (item) => {
|
|
117
|
+
return item["wm-id"]
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return webmentions
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const getWebmentions = async (url, allowedTypes) => {
|
|
125
|
+
const webmentions = await filteredWebmentions()
|
|
126
|
+
url = absoluteURL(url, options.domain)
|
|
127
|
+
|
|
128
|
+
if (!url || !webmentions || !webmentions[url]) {
|
|
129
|
+
return []
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const results = webmentions[url]
|
|
133
|
+
// filter webmentions by allowedTypes only if passed
|
|
134
|
+
.filter((entry) => {
|
|
135
|
+
return Array.isArray(allowedTypes) ? allowedTypes.includes(entry["wm-property"]) : true
|
|
136
|
+
})
|
|
137
|
+
// remove webmentions without an author name
|
|
138
|
+
.filter((entry) => {
|
|
139
|
+
const { author } = entry
|
|
140
|
+
return !!author && !!author.name
|
|
141
|
+
})
|
|
142
|
+
// sanitize content of webmentions and check against HTML limit
|
|
143
|
+
.map((entry) => {
|
|
144
|
+
if (!("content" in entry)) {
|
|
145
|
+
return entry
|
|
146
|
+
}
|
|
147
|
+
const { html, text } = entry.content
|
|
148
|
+
if (html && html.length > options.maximumHtmlLength) {
|
|
149
|
+
entry.content.value = `mentioned this in <a href="${entry["wm-source"]}">${entry["wm-source"]}</a>`
|
|
150
|
+
} else {
|
|
151
|
+
entry.content.value = sanitizeHTML(html || text, options.allowedHTML)
|
|
152
|
+
}
|
|
153
|
+
return entry
|
|
154
|
+
})
|
|
155
|
+
// sort by published/wm-received
|
|
156
|
+
.sort((a, b) => {
|
|
157
|
+
return epoch(a.published || a["wm-received"]) - epoch(b.published || b["wm-received"])
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
return results
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const getWebmentionsFilter = async (url, allowedTypes, callback) => {
|
|
164
|
+
const webmentions = await getWebmentions(url, allowedTypes)
|
|
165
|
+
if (typeof callback === "function") {
|
|
166
|
+
callback(null, webmentions)
|
|
167
|
+
} else {
|
|
168
|
+
return webmentions
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (config && options) {
|
|
173
|
+
if (!options.domain) {
|
|
174
|
+
throw new Error("domain is a required option to be passed when adding the plugin to your config using addPlugin.")
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
config.addNunjucksAsyncFilter("getWebmentions", getWebmentionsFilter)
|
|
178
|
+
} else {
|
|
179
|
+
return filteredWebmentions()
|
|
180
|
+
}
|
|
181
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chrisburnell/eleventy-cache-webmentions",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Cache webmentions from webmention.io using eleventy-cache-assets.",
|
|
5
|
+
"main": "eleventy-cache-webmentions.js",
|
|
6
|
+
"author": "Chris Burnell <me@chrisburnell.com>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git@github.com:chrisburnell/chrisburnell.com.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/chrisburnell/chrisburnell.com/issues"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=10"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@11ty/eleventy-cache-assets": "^2.3.0",
|
|
20
|
+
"dotenv": "^10.0.0",
|
|
21
|
+
"lodash": "^4.17.21",
|
|
22
|
+
"node-fetch": "2.6.5",
|
|
23
|
+
"sanitize-html": "^2.5.2"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"eleventy"
|
|
27
|
+
]
|
|
28
|
+
}
|