{{ reply.content.html | safe }}
+diff --git a/.eleventy.js b/.eleventy.js index d4fd663..b40520b 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -7,6 +7,15 @@ const markdownItAttrs = require("markdown-it-attrs"); const markdownItEleventyImg = require("markdown-it-eleventy-img"); const pluginRss = require("@11ty/eleventy-plugin-rss"); const embedYouTube = require("eleventy-plugin-youtube-embed"); +const sanitizeHtml = require("sanitize-html"); +const metadata = require("./src/_data/metadata.json"); + +const sanitize = (html) => { + return sanitizeHtml(html, { + allowedTags: ['b', 'i', 'em', 'strong', 'a'], + allowedAttributes: { 'a': ['href'] } + }); +}; module.exports = function(eleventyConfig) { eleventyConfig.addPlugin(embedYouTube); @@ -57,7 +66,49 @@ module.exports = function(eleventyConfig) { return ''; }); }); + eleventyConfig.addFilter('webmentionsByUrl', function(webmentions, url) { + const allowedTypes = ['in-reply-to', 'like-of', 'repost-of']; + const hasRequiredFields = mention => { + const { author, published, content } = mention; + return author.name && published && content; + }; + const matchesUrl = mention => { + return mention['wm-target'] === url; + }; + const filteredMentions = webmentions + .filter(mention => { + const isAllowedType = allowedTypes.includes(mention['wm-property']); + if (isAllowedType) { + const isReply = mention['wm-property'] === 'in-reply-to'; + if (isReply) { + return matchesUrl(mention) && hasRequiredFields(mention); + } + return matchesUrl(mention); + } + } + ); + const sanitizedMentions = filteredMentions.map((mention) => { + if (mention?.content?.html) { + mention.content.html = sanitize(mention.content.html); + const mastodonInstance = metadata.author.mastodon.instance; + const mastodonUsername = metadata.author.mastodon.username; + const mentionLink = `@${metadata.author.mastodon.username} `; + if (mention.content.html.startsWith(mentionLink)) { + mention.content.html = mention.content.html.substring(mentionLink.length) + } + } + return mention; + }); + const sortedMentions = sanitizedMentions.reduce((accumulator, mention) => { + const key = mention['wm-property']; + const curGroup = accumulator[key] ?? []; + return { ...accumulator, [key]: [...curGroup, mention] }; + }, {}); + + console.log(sortedMentions); + return sortedMentions; + }); const markdownLibrary = markdownIt({ html: true }) .use(markdownItAnchor, { slugify }) diff --git a/assets/css/theme.css b/assets/css/theme.css index 192bf0c..56ff0df 100644 --- a/assets/css/theme.css +++ b/assets/css/theme.css @@ -325,6 +325,81 @@ p:first-of-type { justify-self: end; } +.webmentions-likes, .webmentions-boosts, .webmentions-replies { + list-style-type: none; + padding: 0; + margin: 0; +} +.webmentions-replies li { + display: flex; + align-items: flex-start; +} +.webmentions-replies li:not(:last-child) { + margin-bottom: 1.25rem; +} +.webmentions-replies li > a, +.webmentions-likes li > a, +.webmentions-boosts li > a { + display: block; + position: relative; +} +.webmentions-likes li > a, +.webmentions-boosts li > a { + width: 4rem; + height: 4rem; +} +.webmentions-replies li > a { + width: 6rem; + height: 6rem; + margin-right: 1rem; +} +.webmentions-replies .from-mastodon > a::before, +.webmentions-likes .from-mastodon > a::before, +.webmentions-boosts .from-mastodon > a::before { + content: ""; + position: absolute; + background: url('/images/mastodon-logo.png') no-repeat center center; + background-size: contain; + filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.5)); +} +.webmentions-replies .from-mastodon > a::before { + bottom: 0.25rem; + left: 0.25rem; + width: 1.25rem; + height: 1.25rem; +} +.webmentions-likes .from-mastodon > a::before, +.webmentions-boosts .from-mastodon > a::before { + bottom: 0.15rem; + left: 0.15rem; + width: 1rem; + height: 1rem; +} +.webmentions-replies li img { + width: 6rem; +} +.webmentions-replies li .content { + flex: 1; +} +.webmentions-replies li .content .published-datetime { + font-size: 0.8em; + opacity: 0.8; +} +.webmentions-replies li .content > a { + color: var(--text); +} +.webmentions-likes, .webmentions-boosts { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} +.webmentions-likes li img, .webmentions-boosts li img { + width: 4rem; +} +.mention-heading { + margin-bottom: 1.2rem; +} + .hidden { display: none; } diff --git a/package-lock.json b/package-lock.json index 66735cb..22f2fe2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1621,6 +1621,12 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true + }, "is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", @@ -2173,6 +2179,12 @@ "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", "dev": true }, + "nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true + }, "napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -2396,6 +2408,12 @@ "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", "dev": true }, + "picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2417,6 +2435,17 @@ "semver-compare": "^1.0.0" } }, + "postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "requires": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + } + }, "posthtml": { "version": "0.16.6", "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.6.tgz", @@ -2765,6 +2794,71 @@ "truncate-utf8-bytes": "^1.0.0" } }, + "sanitize-html": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.0.tgz", + "integrity": "sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==", + "dev": true, + "requires": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + }, + "dependencies": { + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + } + } + }, "sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", @@ -2894,6 +2988,12 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, + "source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", diff --git a/package.json b/package.json index f8f7298..8617c15 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "markdown-it-attrs": "^4.1.4", "markdown-it-eleventy-img": "^0.9.0", "markdown-it-obsidian": "github:raddevon/markdown-it-obsidian", + "sanitize-html": "^2.13.0", "simpledotcss": "^2.1.0" } } diff --git a/src/_data/metadata.json b/src/_data/metadata.json new file mode 100644 index 0000000..5306d38 --- /dev/null +++ b/src/_data/metadata.json @@ -0,0 +1,14 @@ +{ + "title": "devon.lol", + "description": "I write essays to no one about video games, technology, the internet, and other things in the world.", + "language": "en-us", + "url": "https://devon.lol/", + "author": { + "name": "Devon Campbell", + "email": "devon@devon.lol", + "mastodon": { + "username": "RadDevon", + "instance": "techhub.social" + } + } +} diff --git a/src/_data/webmentions.js b/src/_data/webmentions.js index c624308..29907ff 100644 --- a/src/_data/webmentions.js +++ b/src/_data/webmentions.js @@ -3,11 +3,10 @@ const EleventyFetch = require("@11ty/eleventy-fetch"); module.exports = async function fetchMentions() { const url = `https://webmention.io/api/mentions.jf2?domain=devon.lol&token=${process.env.WEBMENTIONSIO_TOKEN}`; - const mentions = await EleventyFetch(url, { + const mentionsResponse = await EleventyFetch(url, { duration: "1s", type: "json", }); - console.log('mentions:', mentions); - console.log('mentions.children:', mentions.children); - return mentions.children; + const mentions = mentionsResponse.children; + return mentions; }; diff --git a/src/_includes/article.njk b/src/_includes/article.njk index fff7727..b45cf39 100644 --- a/src/_includes/article.njk +++ b/src/_includes/article.njk @@ -22,10 +22,11 @@ layout: base.njk {{ content | safe }} + {% include 'webmentions.njk' %} diff --git a/src/_includes/base.njk b/src/_includes/base.njk index 8b2febc..5ea8ede 100644 --- a/src/_includes/base.njk +++ b/src/_includes/base.njk @@ -16,7 +16,7 @@ - + diff --git a/src/_includes/webmentions.njk b/src/_includes/webmentions.njk new file mode 100644 index 0000000..3b5ec0c --- /dev/null +++ b/src/_includes/webmentions.njk @@ -0,0 +1,64 @@ +{% set pageAbsoluteUrl = page.url | absoluteUrl(metadata.url) %} +{% set mentions = webmentions | webmentionsByUrl(pageAbsoluteUrl) %} +{% set mastodonSourcePrefix = r/^https:\/\/brid\.gy\/\w+\/mastodon\// %} + +{% if mentions['in-reply-to'] | length %} +
{{ reply.content.html | safe }}
+