jekyll-webmentions-static 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +223 -0
- data/lib/jekyll/webmentions_generator.rb +73 -0
- data/lib/jekyll/webmentions_tag.rb +78 -0
- data/lib/jekyll-webmentions-static/fetcher.rb +98 -0
- data/lib/jekyll-webmentions-static/version.rb +5 -0
- data/lib/jekyll-webmentions-static.rb +4 -0
- metadata +68 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6bdd84c741b25702134b381a11ff8b6e5388a8a949f4531e562bedf6d3032872
|
|
4
|
+
data.tar.gz: ed687e8bd29cdc3bb11e56f07cdaf11213bfe23266b7b1d657ac6225fd39c2b1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f0fe721abe9b8af701b4b11569c85e72c8f8a47cd8fd15ff678db13381e666afd0d622af9851bf3ea1150b5f16b3742a33fc1fdd733f107d627a8d84737f3cfc
|
|
7
|
+
data.tar.gz: 84108908d211adfd0d69c3a04ed5c0855a52959ce548fe82f10327fe8cef3a1d57c63d948ede793bd9fe8a95a67dff9f445060675d79035cfae712a5ac4a8e49
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jason Chance
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# jekyll-webmentions-static
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/jekyll-webmentions-static)
|
|
4
|
+
[](https://github.com/jchance/jekyll-webmentions-static/actions)
|
|
5
|
+
|
|
6
|
+
Fetch and render [Webmention.io](https://webmention.io) mentions as static HTML at Jekyll build time. No JavaScript, no runtime API calls — pure static output.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## How it works
|
|
11
|
+
|
|
12
|
+
During `jekyll build`, the plugin:
|
|
13
|
+
|
|
14
|
+
1. Fetches all webmentions for your domain from the Webmention.io API.
|
|
15
|
+
2. Groups them by target URL and stores them in `site.data["webmentions"]`.
|
|
16
|
+
3. Injects matching mentions into each post's `page.webmentions` array.
|
|
17
|
+
4. Caches the API response locally so repeated builds don't hammer the API.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
Add to your site's `Gemfile`:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
gem "jekyll-webmentions-static"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
And to `_config.yml`:
|
|
30
|
+
|
|
31
|
+
```yaml
|
|
32
|
+
plugins:
|
|
33
|
+
- jekyll-webmentions-static
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then run:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
bundle install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
Add to `_config.yml`:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
webmentions:
|
|
50
|
+
token: YOUR_TOKEN # or use WEBMENTION_TOKEN env var (preferred for CI/CD)
|
|
51
|
+
cache: true # default: true — cache API responses locally
|
|
52
|
+
cache_ttl: 3600 # default: 3600 — seconds before the cache is considered stale
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Getting a token
|
|
56
|
+
|
|
57
|
+
1. Go to [webmention.io](https://webmention.io) and sign in with your domain (using GitHub, email `rel=me`, or another IndieAuth provider).
|
|
58
|
+
2. Once authenticated, your API token is shown in your [settings](https://webmention.io/settings).
|
|
59
|
+
3. Add the token to your `_config.yml` **or** (strongly preferred) set it as an environment variable:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
export WEBMENTION_TOKEN=your-token-here
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The environment variable always takes precedence over the config file value. This keeps secrets out of version control.
|
|
66
|
+
|
|
67
|
+
### Add the `rel="webmention"` link to your `<head>`
|
|
68
|
+
|
|
69
|
+
Webmention.io needs to know your site is using it. Add these two tags to your HTML `<head>`:
|
|
70
|
+
|
|
71
|
+
```html
|
|
72
|
+
<link rel="webmention" href="https://webmention.io/yourdomain.com/webmention" />
|
|
73
|
+
<link rel="pingback" href="https://webmention.io/xmlrpc" />
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Usage in templates
|
|
79
|
+
|
|
80
|
+
### Option 1 — Liquid tag (built-in template)
|
|
81
|
+
|
|
82
|
+
Place `{% webmentions %}` anywhere in a post or page layout to render a minimal, accessible webmentions section:
|
|
83
|
+
|
|
84
|
+
```liquid
|
|
85
|
+
{% webmentions %}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This renders likes, reposts, and bookmarks as counts, and replies as individual cards with author name, avatar, and content.
|
|
89
|
+
|
|
90
|
+
### Option 2 — Manual template with `page.webmentions`
|
|
91
|
+
|
|
92
|
+
For full control over markup, iterate over `page.webmentions` directly:
|
|
93
|
+
|
|
94
|
+
```liquid
|
|
95
|
+
{% if page.webmentions %}
|
|
96
|
+
<section class="webmentions">
|
|
97
|
+
{% assign likes = page.webmentions | where: "type", "like-of" %}
|
|
98
|
+
{% assign replies = page.webmentions | where: "type", "in-reply-to" %}
|
|
99
|
+
|
|
100
|
+
{% if likes.size > 0 %}
|
|
101
|
+
<p>{{ likes.size }} like{{ likes.size | plural: "", "s" }}</p>
|
|
102
|
+
{% endif %}
|
|
103
|
+
|
|
104
|
+
{% for mention in replies %}
|
|
105
|
+
<article>
|
|
106
|
+
<strong>{{ mention.author.name }}</strong>
|
|
107
|
+
{% if mention.content.text %}
|
|
108
|
+
<p>{{ mention.content.text }}</p>
|
|
109
|
+
{% endif %}
|
|
110
|
+
<a href="{{ mention.url }}">{{ mention.published | date: "%B %-d, %Y" }}</a>
|
|
111
|
+
</article>
|
|
112
|
+
{% endfor %}
|
|
113
|
+
</section>
|
|
114
|
+
{% endif %}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Accessing all webmentions in any template
|
|
118
|
+
|
|
119
|
+
The full hash is available at `site.data.webmentions`:
|
|
120
|
+
|
|
121
|
+
```liquid
|
|
122
|
+
{% assign mentions = site.data.webmentions[page.url | prepend: site.url] %}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Caching
|
|
128
|
+
|
|
129
|
+
The plugin writes fetched data to `_webmentions_cache.json` in your site source directory. Subsequent builds within the TTL window (default: 1 hour) read from this file instead of calling the API.
|
|
130
|
+
|
|
131
|
+
**Add the cache file to your `.gitignore`:**
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
_webmentions_cache.json
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
To force a fresh fetch, delete the cache file or set `cache: false` in your config.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## GitHub Actions setup
|
|
142
|
+
|
|
143
|
+
Store your token as a [repository secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets) named `WEBMENTION_TOKEN`, then expose it in your workflow:
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
- name: Build Jekyll site
|
|
147
|
+
run: bundle exec jekyll build
|
|
148
|
+
env:
|
|
149
|
+
WEBMENTION_TOKEN: ${{ secrets.WEBMENTION_TOKEN }}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Because the cache file is in `.gitignore`, each CI build fetches fresh data. To cache across CI runs, add `_webmentions_cache.json` to your Actions cache key. Without CI caching you'll make one API call per build, which is fine for most sites.
|
|
153
|
+
|
|
154
|
+
### Keeping mentions fresh with a scheduled rebuild
|
|
155
|
+
|
|
156
|
+
Webmentions only update when your site is rebuilt. If your site doesn't publish frequently, add a `schedule` trigger to your GitHub Pages workflow so mentions stay current automatically:
|
|
157
|
+
|
|
158
|
+
```yaml
|
|
159
|
+
on:
|
|
160
|
+
push:
|
|
161
|
+
branches: [main]
|
|
162
|
+
schedule:
|
|
163
|
+
- cron: '0 */6 * * *' # rebuild every 6 hours
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Adjust the cron interval to taste — daily (`0 0 * * *`) is reasonable for most sites. Each scheduled run fetches any new mentions from Webmention.io and publishes the updated site.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Webmention types
|
|
171
|
+
|
|
172
|
+
| `type` | Meaning |
|
|
173
|
+
|-----------------|----------------------------------|
|
|
174
|
+
| `like-of` | Someone liked your post |
|
|
175
|
+
| `repost-of` | Someone reposted / boosted it |
|
|
176
|
+
| `in-reply-to` | A reply with content |
|
|
177
|
+
| `bookmark-of` | Someone bookmarked it |
|
|
178
|
+
| `mention-of` | A generic link mention |
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Styling
|
|
183
|
+
|
|
184
|
+
The `{% webmentions %}` tag outputs unstyled semantic HTML. All elements use BEM class names:
|
|
185
|
+
|
|
186
|
+
```css
|
|
187
|
+
/* Outer section */
|
|
188
|
+
.webmentions { }
|
|
189
|
+
|
|
190
|
+
/* Reaction count pills */
|
|
191
|
+
.webmentions__counts { }
|
|
192
|
+
.webmentions__likes { }
|
|
193
|
+
.webmentions__reposts { }
|
|
194
|
+
.webmentions__bookmarks { }
|
|
195
|
+
|
|
196
|
+
/* Replies section */
|
|
197
|
+
.webmentions__replies { }
|
|
198
|
+
.webmentions__replies-heading { }
|
|
199
|
+
|
|
200
|
+
/* Individual reply card */
|
|
201
|
+
.webmention__reply { }
|
|
202
|
+
.webmention__author { }
|
|
203
|
+
.webmention__avatar { } /* circular avatar img */
|
|
204
|
+
.webmention__author-name { } /* linked name */
|
|
205
|
+
.webmention__date { }
|
|
206
|
+
.webmention__content { }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The count pills include a `title` attribute (e.g. `title="2 likes"`) for native browser tooltip text on hover.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Requirements
|
|
214
|
+
|
|
215
|
+
- Ruby >= 2.7
|
|
216
|
+
- Jekyll >= 4.0
|
|
217
|
+
- No additional gems required — uses Ruby's built-in `net/http`
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
[MIT](LICENSE.txt) © 2025 Jason Chance
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require "jekyll"
|
|
2
|
+
require_relative "../jekyll-webmentions-static/fetcher"
|
|
3
|
+
|
|
4
|
+
module Jekyll
|
|
5
|
+
class WebmentionsGenerator < Jekyll::Generator
|
|
6
|
+
safe true
|
|
7
|
+
priority :low
|
|
8
|
+
|
|
9
|
+
def generate(site)
|
|
10
|
+
token = resolve_token(site)
|
|
11
|
+
|
|
12
|
+
if token.nil? || token.strip.empty?
|
|
13
|
+
Jekyll.logger.info "Webmentions:", "No token configured — skipping"
|
|
14
|
+
return
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
domain = extract_domain(site.config["url"])
|
|
18
|
+
|
|
19
|
+
if domain.nil? || domain.empty?
|
|
20
|
+
Jekyll.logger.warn "Webmentions:", "site.url not set — skipping"
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
cfg = site.config.fetch("webmentions", {})
|
|
25
|
+
cache = cfg.fetch("cache", true)
|
|
26
|
+
cache_ttl = cfg.fetch("cache_ttl", Jekyll::WebmentionsStatic::Fetcher::DEFAULT_TTL)
|
|
27
|
+
|
|
28
|
+
fetcher = Jekyll::WebmentionsStatic::Fetcher.new(
|
|
29
|
+
token: token,
|
|
30
|
+
domain: domain,
|
|
31
|
+
site_source: site.source,
|
|
32
|
+
cache: cache,
|
|
33
|
+
cache_ttl: cache_ttl
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
grouped = fetcher.fetch_and_group
|
|
37
|
+
|
|
38
|
+
site.data["webmentions"] = grouped
|
|
39
|
+
|
|
40
|
+
inject_into_posts(site, grouped)
|
|
41
|
+
|
|
42
|
+
total = grouped.values.sum(&:length)
|
|
43
|
+
Jekyll.logger.info "Webmentions:", "Loaded #{total} mention(s) across #{grouped.keys.length} URL(s)"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def resolve_token(site)
|
|
49
|
+
env_token = ENV["WEBMENTION_TOKEN"]
|
|
50
|
+
return env_token unless env_token.nil? || env_token.strip.empty?
|
|
51
|
+
|
|
52
|
+
site.config.dig("webmentions", "token")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def extract_domain(url)
|
|
56
|
+
return nil if url.nil? || url.strip.empty?
|
|
57
|
+
|
|
58
|
+
url.strip
|
|
59
|
+
.sub(%r{\Ahttps?://}, "")
|
|
60
|
+
.sub(%r{/+\z}, "")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def inject_into_posts(site, grouped)
|
|
64
|
+
base_url = site.config["url"].to_s.sub(%r{/+\z}, "")
|
|
65
|
+
|
|
66
|
+
site.posts.docs.each do |post|
|
|
67
|
+
full_url = base_url + post.url
|
|
68
|
+
mentions = grouped[full_url]
|
|
69
|
+
post.data["webmentions"] = mentions if mentions
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require "jekyll"
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
class WebmentionsTag < Liquid::Tag
|
|
5
|
+
|
|
6
|
+
ICON_HEART = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>'.freeze
|
|
7
|
+
ICON_REPOST = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"/></svg>'.freeze
|
|
8
|
+
ICON_BOOKMARK = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg>'.freeze
|
|
9
|
+
|
|
10
|
+
def render(context)
|
|
11
|
+
page = context["page"]
|
|
12
|
+
mentions = page["webmentions"]
|
|
13
|
+
|
|
14
|
+
return "" if mentions.nil? || mentions.empty?
|
|
15
|
+
|
|
16
|
+
likes = mentions.select { |m| m["type"] == "like-of" }
|
|
17
|
+
reposts = mentions.select { |m| m["type"] == "repost-of" }
|
|
18
|
+
replies = mentions.select { |m| m["type"] == "in-reply-to" }
|
|
19
|
+
bookmarks = mentions.select { |m| m["type"] == "bookmark-of" }
|
|
20
|
+
|
|
21
|
+
html = []
|
|
22
|
+
html << '<section class="webmentions" aria-label="Webmentions">'
|
|
23
|
+
|
|
24
|
+
if likes.any? || reposts.any? || bookmarks.any?
|
|
25
|
+
html << ' <div class="webmentions__counts">'
|
|
26
|
+
html << " <span class=\"webmentions__likes\" title=\"#{likes.length} #{likes.length == 1 ? "like" : "likes"}\">#{ICON_HEART} #{likes.length}</span>" if likes.any?
|
|
27
|
+
html << " <span class=\"webmentions__reposts\" title=\"#{reposts.length} #{reposts.length == 1 ? "repost" : "reposts"}\">#{ICON_REPOST} #{reposts.length}</span>" if reposts.any?
|
|
28
|
+
html << " <span class=\"webmentions__bookmarks\" title=\"#{bookmarks.length} #{bookmarks.length == 1 ? "bookmark" : "bookmarks"}\">#{ICON_BOOKMARK} #{bookmarks.length}</span>" if bookmarks.any?
|
|
29
|
+
html << " </div>"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if replies.any?
|
|
33
|
+
html << ' <div class="webmentions__replies">'
|
|
34
|
+
html << " <h3 class=\"webmentions__replies-heading\">Replies</h3>"
|
|
35
|
+
replies.each do |reply|
|
|
36
|
+
author = reply["author"] || {}
|
|
37
|
+
name = CGI.escapeHTML(author["name"].to_s.then { |n| n.empty? ? "Anonymous" : n })
|
|
38
|
+
photo = author["photo"]
|
|
39
|
+
url = CGI.escapeHTML(reply["url"].to_s)
|
|
40
|
+
text = reply.dig("content", "text").to_s
|
|
41
|
+
content = CGI.escapeHTML(text.length > 300 ? "#{text[0, 300]}…" : text)
|
|
42
|
+
date = format_date(reply["published"])
|
|
43
|
+
|
|
44
|
+
html << ' <article class="webmention__reply">'
|
|
45
|
+
html << ' <header class="webmention__author">'
|
|
46
|
+
if photo
|
|
47
|
+
html << " <img src=\"#{CGI.escapeHTML(photo)}\" alt=\"#{name}\" class=\"webmention__avatar\" width=\"40\" height=\"40\" loading=\"lazy\">"
|
|
48
|
+
end
|
|
49
|
+
if url.empty?
|
|
50
|
+
html << " <span class=\"webmention__author-name\">#{name}</span>"
|
|
51
|
+
else
|
|
52
|
+
html << " <a href=\"#{url}\" class=\"webmention__author-name\" rel=\"nofollow noopener noreferrer\" target=\"_blank\">#{name}</a>"
|
|
53
|
+
end
|
|
54
|
+
html << " <time class=\"webmention__date\" datetime=\"#{CGI.escapeHTML(reply["published"].to_s)}\">#{date}</time>" if date
|
|
55
|
+
html << " </header>"
|
|
56
|
+
html << " <p class=\"webmention__content\">#{content}</p>" unless content.empty?
|
|
57
|
+
html << " </article>"
|
|
58
|
+
end
|
|
59
|
+
html << " </div>"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
html << "</section>"
|
|
63
|
+
html.join("\n")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def format_date(iso_str)
|
|
69
|
+
return nil if iso_str.nil? || iso_str.strip.empty?
|
|
70
|
+
|
|
71
|
+
Time.parse(iso_str).strftime("%B %-d, %Y")
|
|
72
|
+
rescue ArgumentError
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
Liquid::Template.register_tag("webmentions", Jekyll::WebmentionsTag)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Jekyll
|
|
7
|
+
module WebmentionsStatic
|
|
8
|
+
class Fetcher
|
|
9
|
+
ENDPOINT = "https://webmention.io/api/mentions.jf2"
|
|
10
|
+
DEFAULT_TTL = 3600
|
|
11
|
+
|
|
12
|
+
def initialize(token:, domain:, site_source:, cache: true, cache_ttl: DEFAULT_TTL)
|
|
13
|
+
@token = token
|
|
14
|
+
@domain = domain
|
|
15
|
+
@site_source = site_source
|
|
16
|
+
@cache = cache
|
|
17
|
+
@cache_ttl = cache_ttl.to_i
|
|
18
|
+
@cache_path = File.join(site_source, "_webmentions_cache.json")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Returns a hash keyed by wm-target URL → Array of mention hashes.
|
|
22
|
+
def fetch_and_group
|
|
23
|
+
raw = load_from_cache || fetch_from_api
|
|
24
|
+
group_by_target(raw)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def cache_valid?
|
|
30
|
+
return false unless @cache && File.exist?(@cache_path)
|
|
31
|
+
|
|
32
|
+
age = Time.now - File.mtime(@cache_path)
|
|
33
|
+
age < @cache_ttl
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def load_from_cache
|
|
37
|
+
return nil unless cache_valid?
|
|
38
|
+
|
|
39
|
+
Jekyll.logger.info "Webmentions:", "Using cached data (#{@cache_path})"
|
|
40
|
+
JSON.parse(File.read(@cache_path))
|
|
41
|
+
rescue JSON::ParserError => e
|
|
42
|
+
Jekyll.logger.warn "Webmentions:", "Cache parse error: #{e.message} — refetching"
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def fetch_from_api
|
|
47
|
+
uri = URI(ENDPOINT)
|
|
48
|
+
uri.query = URI.encode_www_form(
|
|
49
|
+
"token" => @token,
|
|
50
|
+
"domain" => @domain,
|
|
51
|
+
"per-page" => 500
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
Jekyll.logger.info "Webmentions:", "Fetching from #{uri}"
|
|
55
|
+
|
|
56
|
+
response = Net::HTTP.get_response(uri)
|
|
57
|
+
|
|
58
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
59
|
+
Jekyll.logger.warn "Webmentions:", "HTTP #{response.code} — skipping"
|
|
60
|
+
return []
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
data = JSON.parse(response.body)
|
|
64
|
+
items = data["items"] || []
|
|
65
|
+
|
|
66
|
+
write_cache(items)
|
|
67
|
+
items
|
|
68
|
+
rescue SocketError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Net::OpenTimeout,
|
|
69
|
+
Net::ReadTimeout, OpenSSL::SSL::SSLError => e
|
|
70
|
+
Jekyll.logger.warn "Webmentions:", "Network error: #{e.message} — skipping"
|
|
71
|
+
[]
|
|
72
|
+
rescue JSON::ParserError => e
|
|
73
|
+
Jekyll.logger.warn "Webmentions:", "JSON parse error: #{e.message} — skipping"
|
|
74
|
+
[]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def write_cache(items)
|
|
78
|
+
return unless @cache
|
|
79
|
+
|
|
80
|
+
File.write(@cache_path, JSON.generate(items))
|
|
81
|
+
Jekyll.logger.info "Webmentions:", "Cache written to #{@cache_path}"
|
|
82
|
+
rescue Errno::EACCES, Errno::EROFS => e
|
|
83
|
+
Jekyll.logger.warn "Webmentions:", "Could not write cache: #{e.message}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def group_by_target(items)
|
|
87
|
+
grouped = {}
|
|
88
|
+
items.each do |item|
|
|
89
|
+
target = item["wm-target"]
|
|
90
|
+
next if target.nil? || target.strip.empty?
|
|
91
|
+
|
|
92
|
+
(grouped[target] ||= []) << item
|
|
93
|
+
end
|
|
94
|
+
grouped
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: jekyll-webmentions-static
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jason Chance
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-08-21 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: jekyll
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '4.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '4.0'
|
|
26
|
+
description: |
|
|
27
|
+
jekyll-webmentions-static fetches Webmention.io mentions during Jekyll build
|
|
28
|
+
and injects them as static data into site.data["webmentions"] keyed by post URL.
|
|
29
|
+
No JavaScript, no runtime API calls — pure static HTML.
|
|
30
|
+
email:
|
|
31
|
+
- jason@jasonchance.com
|
|
32
|
+
executables: []
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- LICENSE.txt
|
|
37
|
+
- README.md
|
|
38
|
+
- lib/jekyll-webmentions-static.rb
|
|
39
|
+
- lib/jekyll-webmentions-static/fetcher.rb
|
|
40
|
+
- lib/jekyll-webmentions-static/version.rb
|
|
41
|
+
- lib/jekyll/webmentions_generator.rb
|
|
42
|
+
- lib/jekyll/webmentions_tag.rb
|
|
43
|
+
homepage: https://github.com/jchance/jekyll-webmentions-static
|
|
44
|
+
licenses:
|
|
45
|
+
- MIT
|
|
46
|
+
metadata:
|
|
47
|
+
source_code_uri: https://github.com/jchance/jekyll-webmentions-static
|
|
48
|
+
changelog_uri: https://github.com/jchance/jekyll-webmentions-static/blob/main/CHANGELOG.md
|
|
49
|
+
bug_tracker_uri: https://github.com/jchance/jekyll-webmentions-static/issues
|
|
50
|
+
rubygems_mfa_required: 'true'
|
|
51
|
+
rdoc_options: []
|
|
52
|
+
require_paths:
|
|
53
|
+
- lib
|
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '2.7'
|
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '0'
|
|
64
|
+
requirements: []
|
|
65
|
+
rubygems_version: 3.6.2
|
|
66
|
+
specification_version: 4
|
|
67
|
+
summary: Fetch and render Webmention.io mentions as static HTML at Jekyll build time.
|
|
68
|
+
test_files: []
|