@hhkaos/webmentions-widget 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Raul Jimenez Ortega
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,171 @@
1
+ # @hhkaos/webmentions-widget
2
+
3
+ Fetch and render [webmention.io](https://webmention.io) mentions for the current page.
4
+
5
+ Dependency-free, buildless ES modules. One implementation shared by every site
6
+ instead of a copy per repo — see [#1](https://github.com/hhkaos/webmentions-widget/issues/1)
7
+ for the history.
8
+
9
+ ## Why it exists
10
+
11
+ Three sites had grown three near-identical copies of "query `mentions.jf2`, dedupe,
12
+ render a facepile and a reply list". When webmention.io started returning
13
+ intermittent `502`s in September 2026, every copy had the same flaw: a single
14
+ un-retried `fetch`, and a widget that hides itself on failure. The 502 comes from
15
+ nginx with **no `Access-Control-Allow-Origin` header**, so in a browser it surfaces
16
+ as an opaque `Failed to fetch` — and the section silently vanished on all three
17
+ sites at once.
18
+
19
+ So this package builds the fixes in once:
20
+
21
+ - **Retries with exponential backoff** on network errors and 5xx.
22
+ - **Automatic fallback** from `/api/mentions.jf2` to the older `/api/mentions.json`,
23
+ reshaped into the same entry format.
24
+ - **`error` is distinct from empty**, so a caller can keep server-rendered markup
25
+ on screen during an outage instead of blanking the section.
26
+ - **Target URL variants** — `www`/no-`www`, trailing slash or not, locale prefixes.
27
+ webmention.io matches targets by exact string, so this is the single most common
28
+ cause of "the mention exists but does not show up".
29
+
30
+ ## Install
31
+
32
+ ```sh
33
+ npm install @hhkaos/webmentions-widget
34
+ ```
35
+
36
+ Or load it straight from a CDN — **always pin the version**, never `@latest`:
37
+
38
+ ```html
39
+ <script type="module">
40
+ import {renderWebmentions} from 'https://esm.sh/@hhkaos/webmentions-widget@0.1.0/render';
41
+ </script>
42
+ ```
43
+
44
+ ## Vanilla usage
45
+
46
+ ```html
47
+ <section class="webmentions h-feed" id="webmentions" hidden>
48
+ <h2>Mentions</h2>
49
+ <div class="webmentions__facepile" id="webmentions-facepile" hidden></div>
50
+ <ol class="webmentions__list" id="webmentions-list"></ol>
51
+ </section>
52
+
53
+ <script type="module">
54
+ import {renderWebmentions} from '@hhkaos/webmentions-widget/render';
55
+
56
+ renderWebmentions({
57
+ container: '#webmentions',
58
+ facepile: '#webmentions-facepile',
59
+ list: '#webmentions-list',
60
+ // targets default to <link rel="canonical"> expanded into every variant
61
+ facepileMode: 'grouped',
62
+ labels: {
63
+ 'like-of': {en: 'like', es: 'me gusta'},
64
+ 'in-reply-to': {en: 'replied', es: 'respondió'},
65
+ viewSource: {en: 'View source', es: 'Ver original'},
66
+ },
67
+ onError: (error) => console.warn('[webmentions]', error),
68
+ });
69
+ </script>
70
+ ```
71
+
72
+ A label can be a plain string, or a `{en, es}` map — which renders one
73
+ `<span class="i18n-en">` / `<span class="i18n-es">` per language, for sites that
74
+ ship both and toggle with CSS.
75
+
76
+ `facepileMode: 'grouped'` renders a separate like / repost / bookmark group with a
77
+ count and a glyph. `'flat'` (the default) renders one merged pile.
78
+
79
+ ## React / Docusaurus usage
80
+
81
+ ```jsx
82
+ import {getCanonicalTargets} from '@hhkaos/webmentions-widget';
83
+ import {Webmentions} from '@hhkaos/webmentions-widget/react';
84
+ import {useLocation} from '@docusaurus/router';
85
+ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
86
+
87
+ export default function SiteWebmentions() {
88
+ const {pathname} = useLocation();
89
+ const {siteConfig} = useDocusaurusContext();
90
+ const targets = getCanonicalTargets({
91
+ siteUrl: siteConfig.url,
92
+ pathname,
93
+ i18n: siteConfig.i18n,
94
+ });
95
+
96
+ return <Webmentions targets={targets} locale="es" />;
97
+ }
98
+ ```
99
+
100
+ `useWebmentions(targets, options)` is exported separately if you want the data
101
+ without the markup. It returns `{status, groups, error}` where `status` is
102
+ `idle | loading | success | error`.
103
+
104
+ ## API
105
+
106
+ ### `fetchWebmentions(options)`
107
+
108
+ | Option | Default | Notes |
109
+ | --- | --- | --- |
110
+ | `targets` | from `<link rel="canonical">` | array of exact target URLs |
111
+ | `apiUrl` | `.../api/mentions.jf2` | |
112
+ | `jsonApiUrl` | `.../api/mentions.json` | used only for the fallback |
113
+ | `perPage` | `20` | |
114
+ | `sortBy` / `sortDir` | `published` / `down` | |
115
+ | `retries` | `2` | extra attempts *per endpoint* |
116
+ | `retryDelayMs` | `400` | doubles each attempt |
117
+ | `fallbackToJson` | `true` | |
118
+ | `signal` | — | `AbortSignal`; aborts are never retried |
119
+ | `fetch` | `globalThis.fetch` | injectable, for tests or a proxy |
120
+
121
+ Resolves to an array of jf2 entries. Rejects with a `WebmentionFetchError`
122
+ (carrying `.status` and `.attempts`) once every attempt is spent. A `4xx` is not
123
+ retried — it will not fix itself — but the fallback endpoint is still tried.
124
+
125
+ ### `getCanonicalTargets({siteUrl, pathname, i18n, localePrefixes})`
126
+
127
+ Expands one page into every target string webmention.io might have stored it under.
128
+
129
+ ### `groupWebmentions(mentions)`
130
+
131
+ Returns `{interactions, threads, byProperty, counts, total}`. Facepile entries are
132
+ deduped per author *per property*, so one person's like and repost both survive.
133
+ Duplicate `wm-id`s from overlapping target queries are dropped.
134
+
135
+ ### `getMentionContent(mention, {maxLength, parseHTML})`
136
+
137
+ Finds the anchor in the source page that points back at you and quotes the
138
+ sentence around it, rather than excerpting from the top of the post. Falls back to
139
+ `content.text`. Pass `parseHTML` to run outside a browser.
140
+
141
+ ### `getMentionSourceUrl(mention, content)`
142
+
143
+ Appends a `#:~:text=` fragment so the source link lands on the quoted sentence.
144
+
145
+ ### `renderWebmentions(options)` / `renderGroups(groups, options)`
146
+
147
+ Imperative DOM rendering. `renderGroups` paints an already-fetched feed, so a site
148
+ can hydrate from a build-time snapshot without touching the network.
149
+
150
+ Every remote string is written with `textContent`. Nothing in this package ever
151
+ assigns remote HTML.
152
+
153
+ ## Notes on webmention.io quirks
154
+
155
+ - The jf2 feed says `mention-of`; some payloads say `mention`. `normalizeProperty`
156
+ folds them together, so only ever branch on `mention-of`.
157
+ - Bridgy mangles emoji into runs of `?` and `U+FFFD` when extracting plain text.
158
+ `stripMojibake` removes the debris — the emoji is not recoverable.
159
+
160
+ ## Development
161
+
162
+ ```sh
163
+ npm test
164
+ ```
165
+
166
+ No dependencies, no build step. Tests run on `node:test` against a ~90-line fake
167
+ DOM in `test/fake-dom.js`.
168
+
169
+ ## License
170
+
171
+ MIT
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@hhkaos/webmentions-widget",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic, dependency-free widget to fetch and render webmention.io mentions for the current page.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Raul Jimenez Ortega",
8
+ "homepage": "https://github.com/hhkaos/webmentions-widget#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/hhkaos/webmentions-widget.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/hhkaos/webmentions-widget/issues"
15
+ },
16
+ "keywords": [
17
+ "webmention",
18
+ "webmention.io",
19
+ "indieweb",
20
+ "microformats",
21
+ "widget"
22
+ ],
23
+ "sideEffects": false,
24
+ "exports": {
25
+ ".": "./src/index.js",
26
+ "./core": "./src/core.js",
27
+ "./render": "./src/render.js",
28
+ "./react": "./src/react.js",
29
+ "./package.json": "./package.json"
30
+ },
31
+ "files": [
32
+ "src",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "peerDependencies": {
37
+ "react": ">=17"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "react": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "scripts": {
45
+ "test": "node --test test/*.test.js"
46
+ },
47
+ "engines": {
48
+ "node": ">=20"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ }
53
+ }