From 546311e63a5d87660e7eff3aa5c61d838cc70b65 Mon Sep 17 00:00:00 2001 From: Chris Burnell Date: Sun, 14 Jan 2024 20:55:37 +0800 Subject: [PATCH] Version 2.0.0 - Contains breaking changes! (no more () at the end of require) - Exposes internal Liquid/Nunjucks and JS filters for use outside of plugin --- .github/workflows/npm-publish.yml | 19 ++ .npmignore | 1 - .prettierrc | 13 -- README.md | 287 +++++++++++++++++++----------- eleventy-cache-webmentions.js | 81 +++++---- package.json | 6 +- 6 files changed, 254 insertions(+), 153 deletions(-) create mode 100644 .github/workflows/npm-publish.yml delete mode 100644 .prettierrc diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000..31f0e25 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,19 @@ +name: Node.js Package + +on: + release: + types: [created] + +jobs: + publish-npm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 19 + registry-url: https://registry.npmjs.org/ + - run: npm install + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.npm_token}} diff --git a/.npmignore b/.npmignore index eb92b88..e7d0423 100644 --- a/.npmignore +++ b/.npmignore @@ -2,4 +2,3 @@ .vscode .editorconfig .eslintrc.json -.prettierrc diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index c38b808..0000000 --- a/.prettierrc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "semi": false, - "printWidth": 9999, - "tabWidth": 4, - "overrides": [ - { - "files": "*.json", - "options": { - "tabWidth": 2 - } - } - ] -} diff --git a/README.md b/README.md index 716e4a0..e1eb949 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,37 @@ # eleventy-cache-webmentions -> Cache webmentions using eleventy-fetch and make them available to use in collections, templates, pages, etc. in Eleventy. +> Cache webmentions using eleventy-fetch and make them available to use in collections, layouts, pages, etc. in Eleventy. ## Quick Guide -I wrote a quicker and simpler guide to getting this Eleventy plugin working that cuts out all the fluff and extra details. You can read about it here: [Webmention Setup for Eleventy](https://chrisburnell.com/article/webmention-eleventy-setup/). +I wrote a quicker and simpler guide to getting this Eleventy plugin working that cuts out all the fluff and extra details. + +Check it out: [Webmention Setup for Eleventy](https://chrisburnell.com/article/webmention-eleventy-setup/). ## Installation - **With npm:** `npm install @chrisburnell/eleventy-cache-webmentions` - **Direct download:** [https://github.com/chrisburnell/eleventy-cache-webmentions/archive/master.zip](https://github.com/chrisburnell/eleventy-cache-webmentions/archive/master.zip) -Once installed there are **two** more **required** set-up steps: - -### Add it to your config - -Inside your Eleventy config file (typically `.eleventy.js`), use `addPlugin`: +Inside your Eleventy config file, use `addPlugin()` to add it to your project: ```javascript const pluginWebmentions = require("@chrisburnell/eleventy-cache-webmentions") -module.exports = function (eleventyConfig) { - eleventyConfig.addPlugin(pluginWebmentions, { - // these 3 fields are all required! - domain: "https://example.com", - feed: "https://webmentions.example.com?token=S3cr3tT0k3n", - key: "children", - }) +module.exports = function(eleventyConfig) { + eleventyConfig.addPlugin(pluginWebmentions, { + // These 3 fields are all required! + domain: "https://example.com", + feed: "https://webmentions.example.com?token=S3cr3tT0k3n", + key: "array_of_webmentions" + }) } ``` -## Options +Make sure you get the correct values for this configuration. Check below for both Webmention.io configuration and go-jamming configuration. +
+Full options list @@ -116,124 +116,193 @@ module.exports = function (eleventyConfig) {
+
+ +## Usage + +`eleventy-cache-webmentions` comes with a number of ways of accessing your Webmentions as [Global Data](https://www.11ty.dev/docs/data-global-custom/) in both JavaScript and Liquid/Nunjucks as well as a series of [Eleventy Filters](https://www.11ty.dev/docs/filters/) and JavaScript Functions for filtering, sorting, and reading properties about each Webmention: -Advanced control over how the Webmentions are cached and processed is done by passing `options` into the plugin when using `addPlugin`: +### Global Data + +
+JavaScript ```javascript -const pluginWebmentions = require("@chrisburnell/eleventy-cache-webmentions") +const { + defaults, // default options for the plugin + webmentionsByUrl, // Object containing Arrays of Webmentions by URL +} = require("@chrisburnell/eleventy-cache-webmentions") +``` -module.exports = function (eleventyConfig) { - eleventyConfig.addPlugin(pluginWebmentions, { - domain: "https://example.com", - feed: "https://webmentions.example.com?token=S3cr3tT0k3n", - key: "children", - directory: ".cache", - duration: "1d", - uniqueKey: "webmentions", - allowedHTML: { - allowedTags: ["b", "i", "em", "strong", "a"], - allowedAttributes: { - a: ["href"], - }, - }, - allowlist: [], - blocklist: [], - urlReplacements: {}, - maximumHtmlLength: 2000, - maximumHtmlText: "mentioned this in", - }) -} +
+ +
+Liquid / Nunjucks + +```twig +{# default options for the plugin #} +{{ webmentionsDefaults }} +{# Object containing Arrays of Webmentions by URL #} +{{ webmentionsByUrl }} ``` -## JavaScript Usage +
+ +### Filters -Accessing the plugin in JavaScript in the way shown below will give you an Object containing your cached Webmentions organised in key:value pairs where the key is a URL on your domain and the value is an array of data for Webmentions sent to that URL. +
+JavaScript ```javascript -const Webmentions = require("@chrisburnell/eleventy-cache-webmentions")(null, { - domain: "https://example.com", - feed: "https://webmentions.example.com?token=S3cr3tT0k3n", - key: "children", +const { + getWebmentions, // get Array of Webmentions for a given URL + getByTypes, // filter Webmentions by their response type + getPublished, // get received/published time of a Webmention + getContent, // get content of a Webmention + getSource, // get source URL of a Webmention (where it's from) + getTarget, // get target URL of a Webmention (where it's sent to) + getType, // get response type of a Webmention +} = require("@chrisburnell/eleventy-cache-webmentions") + +// This is NOT the best way to get Webmentions! +// See "Attach Webmentions to Pages using Directory Data" below. +const webmentions = getWebmentions({ + domain: "https://example.com", + feed: "https://webmentions.example.com?token=S3cr3tT0k3n", + key: "array_of_webmentions" +}, "https://example.com/specific-page/") + +const responsesOnly = getByTypes(webmentions, ['mention-of', 'in-reply-to']) + +webmentions.forEach((webmention) => { + const published = getPublished(webmention) + const content = getContent(webmention) + const source = getSource(webmention) + const target = getTarget(webmention) + const type = getType(webmention) }) +``` + +
-const webmentionsByUrl = await Webmentions() +
+Liquid / Nunjucks + +```twig +{# get Array of Webmentions for a given URL #} +{% set webmentions = ("https://example.com" + page.url) | getWebmentions %} + +{# filter Webmentions by their response type #} +{{ set responses = webmentions | getWebmentionsByTypes(['mention-of', 'in-reply-to']) }} + +{% for webmention in webmentions %} + {# get received/published time of a Webmention #} + {{ webmentions | getWebmentionPublished }} + {# get content of a Webmention #} + {{ webmentions | getWebmentionContent }} + {# get source URL of a Webmention (where it's from) #} + {{ webmentions | getWebmentionSource }} + {# get target URL of a Webmention (where it's sent to) #} + {{ webmentions | getWebmentionTarget }} + {# get response type of a Webmention #} + {{ webmentions | getWebmentionType }} +{% endfor %} ``` -This can prove to be very useful when building out your pages. Using [Eleventy’s Data Cascade](https://www.11ty.dev/docs/data-cascade/), we can attach Webmentions to each page by using [Directory Specific Data Files](https://www.11ty.dev/docs/data-template-dir/): +
-```javascript -const Webmentions = require("@chrisburnell/eleventy-cache-webmentions")(null, { - domain: "https://example.com", - feed: "https://webmentions.example.com?token=S3cr3tT0k3n", - key: "children", -}) +## Attach Webmentions to Pages using Directory Data + +Using [Eleventy’s Data Cascade](https://www.11ty.dev/docs/data-cascade/), you can attach Webmentions to each page by using [Directory Specific Data Files](https://www.11ty.dev/docs/data-template-dir/). + +For example, if you have a folder, `/pages/`, and want to attach Webmentions to each page, create or add the following to a `pages.11tydata.js` file within the folder: -module.exports = async () => { - const webmentionsByUrl = await Webmentions() - - return { - eleventyComputed: { - webmentions: (data) => { - const webmentionsForUrl = webmentionsByUrl["https://example.com" + data.page.url] || [] - - if (webmentionsForUrl.length) { - return webmentionsForUrl.sort((a, b) => { - return (b.data.published || b.verified_date) - (a.data.published || a.verified_date) - }) - } - return [] - }, - }, - } +```javascript +const { getWebmentions, getPublished } = require("@chrisburnell/eleventy-cache-webmentions") + +module.exports = { + eleventyComputed: { + webmentions: (data) => { + // Get this page's Webmentions as an Array (based on the URL) + const webmentionsForUrl = getWebmentions({ + domain: "https://example.com", + feed: "https://webmentions.example.com?token=S3cr3tT0k3n", + key: "array_of_webmentions" + }, "https://example.com" + data.page.url) + + // If there are Webmentions for this page + if (webmentionsForUrl.length) { + // Sort them (based on when they were received/published) + return webmentionsForUrl.sort((a, b) => { + return getPublished(b) - getPublished(a) + }) + } + // Otherwise, return an empty Array + return [] + }, + }, } ``` -You can now use this data in a number of useful ways, not limited to things like creating a collection of pages ordered by number of Webmentions: +This attaches an Array containing Webmentions to each page (based on its URL). You can then access this Array of Webmentions with the variable, webmentions, within a [Layout](https://www.11ty.dev/docs/layouts/), [Include](https://www.11ty.dev/docs/includes/), or from the page itself: + +```twig +{% for webmention in webmentions %} + {# Do something with each Webmention #} +{% endfor %} +``` + +These Arrays of Webmentions can even be accessed when building [Collections](https://www.11ty.dev/docs/collections/), allowing you to create a Collection of pages sorted by their number of Webmentions, for example: ```javascript module.exports = (eleventyConfig) => { - eleventyConfig.addCollection("popular", (collection) => { - return collection.sort((a, b) => { - return b.data.webmentions.length - a.data.webmentions.length - }) - }) + eleventyConfig.addCollection("popular", (collection) => { + return collection + .sort((a, b) => { + return b.data.webmentions.length - a.data.webmentions.length + }) + }) } ``` -## Liquid/Nunjucks Usage +## Without Directory Data -Accessing the plugin in Liquid/Nunjucks by using a Filter and passing in a URL in the way shown below will give you an Array containing the cached Webmentions for the given URL. +If you would rather get Webmentions for a given page directly from a Layout/Include/Page itself, you can do so using the Filter, `getWebmentions`: ```twig -{% raw %}{% set responses = webmentions %}{% endraw %} +{% set webmentions = ("https://example.com" + page.url) | getWebmentions %} +{% for webmention in webmentions %} + ... +{% endfor %} ``` -**OR** +## Get specific types of Webmentions -```twig -{% raw %}{% set responses = page.url | getWebmentions %}{% endraw %} -``` - -You can get back only specific [response post types](https://indieweb.org/responses#Response_Post_Types) by passing a second argument: +Instead of getting all the Webmentions for a given page, you may want to grab only certain types of Webmentions. This is useful if you want to display different types of Webmentions separately, e.g.: ```twig -{% raw %}{% set reactions = page.url | getWebmentions(['like-of', 'repost-of', 'bookmark-of']) %} -{% set replies = page.url | getWebmentions(['mention-of', 'in-reply-to']) %}{% endraw %} +{% set bookmarks = webmentions | getTypes(['bookmark-of']) %} +{% set likes = webmentions | getTypes(['like-of']) %} +{% set reposts = webmentions | getTypes(['repost-of']) %} + +{% set replies = webmentions | getTypes(['mention-of', 'in-reply-to']) %} ``` -And, if you need it, the entire Object of sorted Webmentions is available too: +## Get all Webmentions at once + +If you need it, the plugin also makes available an Object containing your cached Webmentions organised in key:value pairs, where each key is a full URL on your website and its value is an Array of Webmentions sent to that URL: ```twig -{% raw %}{% set count = 0 %} -{% for url, array in webmentionsAll %} - {% set count = array.length + count %} +{% set count = 0 %} +{% for url, array in webmentionsByUrl %} + {% set count = array.length + count %} {% endfor %} -

This site has received {{ count }} Webmentions!

{% endraw %} +

This website has received {{ count }} Webmentions!

``` -

Webmention.io

+## Webmention.io -[Webmention.io](https://webmention.io) is a in-place Webmention receiver solution that you can use by authenticating yourself via [IndieAuth](https://indieauth.com/) (or host it yourself), and, like so much other publically-available IndieWeb software, is built and hosted by [Aaron Parecki](https://aaronparecki.com/). +[Webmention.io](https://webmention.io) is a in-place Webmention receiver solution that you can use by authenticating yourself via [IndieAuth](https://indieauth.com/) (or host it yourself), and, like *so much* other publicly-available IndieWeb software, is built and hosted by [Aaron Parecki](https://aaronparecki.com/). ### Add your token @@ -245,25 +314,31 @@ WEBMENTION_IO_TOKEN=njJql0lKXnotreal4x3Wmd ### Set your feed and key config options +The example below requests the [JF2](https://www.w3.org/TR/jf2/) file format, which I highly recommend using; although, there is a JSON format available from [Webmention.io](https://webmention.io) as well. The [official documentation](https://github.com/aaronpk/webmention.io) has more information on how to use these two formats. + +The key difference between the two feed formats is in the *naming* of the keys: the JF2 format holds the array of Webmentions in the `children` key, whereas the JSON format holds them in the `links` key. The JF2 format, however, provides keys and values that more tightly-align with [microformats](https://indieweb.org/microformats), the method I recommend the most for marking up HTML such that it can be consumed and understood by search engines, aggregators, and other tools across the Indieweb. + ```javascript const pluginWebmentions = require("@chrisburnell/eleventy-cache-webmentions") -module.exports = function (eleventyConfig) { - eleventyConfig.addPlugin(pluginWebmentions, { - domain: "https://example.com", - feed: `https://webmention.io/api/mentions.jf2?domain=example.com&per-page=9001&token=${process.env.WEBMENTION_IO_TOKEN}`, - key: "children", - }) +module.exports = function(eleventyConfig) { + eleventyConfig.addPlugin(pluginWebmentions, { + domain: "https://example.com", + feed: `https://webmention.io/api/mentions.jf2?domain=example.com&per-page=9001&token=${process.env.WEBMENTION_IO_TOKEN}`, + key: "children" + }) } ``` +If you want to use the JSON format instead, make sure that you replace `mentions.jf2` in the URL with `mentions.json` and change the value of the key from `children` to `links`. + ## go-jamming [go-jamming](https://git.brainbaking.com/wgroeneveld/go-jamming) is a self-hosted Webmention sender and receiver, built in Go by [Wouter Groeneveld](https://brainbaking.com) and available with more information on his [personal git instance](https://git.brainbaking.com/wgroeneveld/go-jamming). ### Add your token -Once you’ve set up your _go-jamming_ server and you’ve defined your token, you’ll need add it to your project as an environment variable, i.e. in a `.env` file in the root of your project: +Once you’ve set up your *go-jamming* server and you’ve defined your token, you’ll need add it to your project as an environment variable, i.e. in a `.env` file in the root of your project: ```text GO_JAMMING_TOKEN=njJql0lKXnotreal4x3Wmd @@ -274,12 +349,12 @@ GO_JAMMING_TOKEN=njJql0lKXnotreal4x3Wmd ```javascript const pluginWebmentions = require("@chrisburnell/eleventy-cache-webmentions") -module.exports = function (eleventyConfig) { - eleventyConfig.addPlugin(pluginWebmentions, { - domain: "https://example.com", - feed: `https://jam.example.com/webmention/example.com/${process.env.GO_JAMMING_TOKEN}`, - key: "json", - }) +module.exports = function(eleventyConfig) { + eleventyConfig.addPlugin(pluginWebmentions, { + domain: "https://example.com", + feed: `https://jam.example.com/webmention/example.com/${process.env.GO_JAMMING_TOKEN}`, + key: "json" + }) } ``` diff --git a/eleventy-cache-webmentions.js b/eleventy-cache-webmentions.js index 29e88c3..1077948 100644 --- a/eleventy-cache-webmentions.js +++ b/eleventy-cache-webmentions.js @@ -38,7 +38,7 @@ const epoch = (value) => { } const getPublished = (webmention) => { - return webmention["wm-received"] || webmention?.["data"]["published"] || webmention["verified_date"] || webmention["published"] + return webmention?.["data"]?.["published"] || webmention["published"] || webmention["wm-received"] || webmention["verified_date"] } const getContent = (webmention) => { @@ -46,7 +46,7 @@ const getContent = (webmention) => { } const getSource = (webmention) => { - return webmention["url"] || webmention?.["data"]["url"] || webmention["wm-source"] || webmention["source"] + return webmention?.["data"]?.["url"] || webmention["url"] || webmention["wm-source"] || webmention["source"] } const getTarget = (webmention) => { @@ -54,7 +54,13 @@ const getTarget = (webmention) => { } const getType = (webmention) => { - return webmention["wm-property"] || webmention?.["activity"]["type"] || webmention["type"] + return webmention["wm-property"] || webmention?.["activity"]?.["type"] || webmention["type"] +} + +const getByTypes = (webmentions, allowedTypes) => { + return webmentions.filter((webmention) => { + return allowedTypes.includes(getType(webmention)) + }) } const defaults = { @@ -97,15 +103,17 @@ const fetchWebmentions = async (options) => { } // If there is a cached file but it is outside of expiry, fetch fresh - // results since the most recent webmention + // results since the most recent Webmention if (!asset.isCacheValid(options.duration)) { + // Get the published date of the most recent Webmention, if it exists const since = webmentions.length ? getPublished(webmentions[0]) : false + // Build the URL for the fetch request const url = `${options.feed}${since ? `${options.feed.includes("?") ? "&" : "?"}since=${since}` : ""}` await fetch(url).then(async (response) => { if (response.ok) { const feed = await response.json() if (feed[options.key].length) { - // Combine newly-fetched entries with cached entries + // Combine newly-fetched Webmentions with cached Webmentions webmentions = feed[options.key].concat(webmentions) // Remove duplicates by source URL webmentions = uniqBy([...feed[options.key], ...webmentions], (webmention) => { @@ -172,9 +180,6 @@ const filteredWebmentions = async (options) => { filtered[url].push(webmention) }) - const filteredCount = Object.values(filtered).reduce((count, webmentions) => count + webmentions.length, 0) - console.log(`[${hostname(options.domain)}] ${filteredCount} filtered Webmentions pulled from cache.`) - return filtered } @@ -221,28 +226,44 @@ const getWebmentionsFilter = async (options, url, allowedTypes, callback) => { callback(null, webmentions) } -module.exports = (eleventyConfig, options = {}) => { +const eleventyCacheWebmentions = (eleventyConfig, options = {}) => { options = Object.assign(defaults, options) - if (eleventyConfig) { - // Global Data - const filtered = async () => await filteredWebmentions(options) - eleventyConfig.addGlobalData("webmentionsByUrl", filtered) - const unfiltered = async () => await filteredWebmentions(options).then((filtered) => Object.values(filtered).reduce((array, webmentions) => [...array, ...webmentions], [])) - eleventyConfig.addGlobalData("webmentionsAll", unfiltered) - eleventyConfig.addGlobalData("webmentions", unfiltered) - - // Liquid Filter - eleventyConfig.addLiquidFilter("getWebmentions", getWebmentionsFilter) - - // Nunjucks Filter - eleventyConfig.addNunjucksAsyncFilter("getWebmentions", getWebmentionsFilter) - } - - return { - defaults: defaults, - fetchWebmentions: fetchWebmentions, - filteredWebmentions: filteredWebmentions, - getWebmentions: getWebmentions, - } + // Global Data + eleventyConfig.addGlobalData("webmentionsDefaults", defaults) + const filtered = async () => await filteredWebmentions(options) + eleventyConfig.addGlobalData("webmentionsByUrl", filtered) + const unfiltered = async () => await filteredWebmentions(options).then((filtered) => Object.values(filtered).reduce((array, webmentions) => [...array, ...webmentions], [])) + eleventyConfig.addGlobalData("webmentionsAll", unfiltered) + + // Liquid Filters + eleventyConfig.addLiquidFilter("getWebmentions", getWebmentionsFilter) + eleventyConfig.addLiquidFilter("getWebmentionPublished", getPublished) + eleventyConfig.addLiquidFilter("getWebmentionContent", getContent) + eleventyConfig.addLiquidFilter("getWebmentionSource", getSource) + eleventyConfig.addLiquidFilter("getWebmentionTarget", getTarget) + eleventyConfig.addLiquidFilter("getWebmentionType", getType) + eleventyConfig.addLiquidFilter("getWebmentionsByTypes", getByTypes) + + // Nunjucks Filters + eleventyConfig.addNunjucksAsyncFilter("getWebmentions", getWebmentionsFilter) + eleventyConfig.addNunjucksFilter("getWebmentionsByTypes", getByTypes) + eleventyConfig.addNunjucksFilter("getWebmentionPublished", getPublished) + eleventyConfig.addNunjucksFilter("getWebmentionContent", getContent) + eleventyConfig.addNunjucksFilter("getWebmentionSource", getSource) + eleventyConfig.addNunjucksFilter("getWebmentionTarget", getTarget) + eleventyConfig.addNunjucksFilter("getWebmentionType", getType) } + +module.exports = eleventyCacheWebmentions +module.exports.defaults = defaults +module.exports.filteredWebmentions = filteredWebmentions +module.exports.webmentionsByUrl = filteredWebmentions +module.exports.fetchWebmentions = fetchWebmentions +module.exports.getWebmentions = getWebmentions +module.exports.getByTypes = getByTypes +module.exports.getPublished = getPublished +module.exports.getContent = getContent +module.exports.getSource = getSource +module.exports.getTarget = getTarget +module.exports.getType = getType diff --git a/package.json b/package.json index 2dc0bba..0f2893c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@chrisburnell/eleventy-cache-webmentions", - "version": "1.2.6-rc.2", - "description": "Fetch and cache webmentions using eleventy-fetch.", + "version": "2.0.0", + "description": "Cache webmentions using eleventy-fetch and make them available to use in collections, layouts, pages, etc. in Eleventy.", "author": "Chris Burnell ", "license": "MIT", "repository": { @@ -20,7 +20,7 @@ "webmention" ], "devDependencies": { - "eslint": "^8.47.0" + "eslint": "^8.56.0" }, "dependencies": { "@11ty/eleventy-fetch": "^4.0.0",