jekyll-stats 0.2.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab21769c8de5f9ff18a6ad985b0a0240dbf00c29be751b3f0aee27f8c313a257
4
- data.tar.gz: '071349d927967afb2c9164d9382a56175d9aa106d818ac267d32930169aca4e7'
3
+ metadata.gz: 5c9845c1c909307a6ee955d23c51412d24c1a12789d7144a817f8ac619bdaa91
4
+ data.tar.gz: ec4fdcf95f6b96fca65dac435e0edd307fff6c5ac7ea416e44bb473a0ee29d85
5
5
  SHA512:
6
- metadata.gz: c1f4bc991fa3230b0e0c3d5c0a471495738a0a853021ba257cc5501cf6748fb8ed567892b0005f5166071c3a2e2aab398380b83fbcb90a11819d2896583957ce
7
- data.tar.gz: de7e7fc767997a55e345f63df9214fc46501790ff9a3f7fba3ee166968f43ce40ec1fec8ed8f4002a4c39a3c5de95ab6bc06307ecb3881fc19b191466b0fc28c
6
+ metadata.gz: b88c1fb528e0fa65d53c7d22a3580a875b40b18d74f418128457c8faaa8b3bce8fa323e675ae5c64f06d19933cdb85731d3d8218016559c4f35a2b0d79ef4066
7
+ data.tar.gz: 279c44108897e9686dba753f11aadb8947bdc1fb95ae466ff8bbc5f306364689168ea476ab2cd1500982c4e92e03c62a9d814f6ba91f2599b4d52dea6dd6a494
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2026-08-30
4
+
5
+ - Add `external_links` to stats: total, unique, and per-domain counts of outbound links
6
+ - Each domain entry includes `posts` (distinct source posts) so single-post link lists are visible
7
+
8
+ ## [0.3.0] - 2026-08-30
9
+
10
+ - Add `internal_links` to stats: posts ranked by inbound links from other posts
11
+ - Source posts can be excluded by tag via `jekyll-stats.link_source_exclude_tags` in `_config.yml`
12
+ - `--json` no longer prints "Loading site..." to stdout, so output pipes cleanly to `jq`
13
+
3
14
  ## [0.2.0] - 2026-01-03
4
15
 
5
16
  - Add `--tags` / `-t` option to filter stats by tags
data/README.md CHANGED
@@ -81,10 +81,39 @@ The `--save` flag writes `_data/stats.json` with this structure:
81
81
  "posts_by_day_of_week": { "monday": 23, "tuesday": 18, "wednesday": 15, "thursday": 20, "friday": 22, "saturday": 14, "sunday": 15 },
82
82
  "tags": [{ "name": "ruby", "count": 34 }, { "name": "opensource", "count": 28 }],
83
83
  "categories": [{ "name": "code", "count": 45 }, { "name": "cars", "count": 32 }],
84
+ "internal_links": [
85
+ { "url": "/2024/01/foo", "title": "Foo", "inbound_count": 7, "inbound_from": ["/2024/03/bar", "/2024/06/baz"] }
86
+ ],
87
+ "external_links": {
88
+ "total": 4637,
89
+ "unique": 3723,
90
+ "domains": [
91
+ { "host": "github.com", "count": 1380, "posts": 172 },
92
+ { "host": "en.wikipedia.org", "count": 94, "posts": 61 }
93
+ ]
94
+ },
84
95
  "drafts_count": 3
85
96
  }
86
97
  ```
87
98
 
99
+ ### Internal links
100
+
101
+ `internal_links` lists posts ranked by how many other posts link to them. It matches markdown and `<a href>` links that point at another post's URL, either as a root-relative path or an absolute URL under your site's `url:`. Self-links and duplicate links from the same source post are ignored.
102
+
103
+ To stop link-list style posts (year in review, roundups) inflating the counts, exclude them as sources by tag in `_config.yml`:
104
+
105
+ ```yaml
106
+ jekyll-stats:
107
+ link_source_exclude_tags:
108
+ - roundup
109
+ ```
110
+
111
+ Excluded posts still appear as targets; they just don't contribute outbound links.
112
+
113
+ ### External links
114
+
115
+ `external_links` counts outbound `http(s)` links to hosts other than your site's `url:`. `total` is raw link occurrences, `unique` is distinct URLs, and each `domains` entry has `count` (distinct URLs at that host) and `posts` (distinct source posts). A high `count` with a low `posts` means one or two posts account for most links to that host. The same `link_source_exclude_tags` config applies.
116
+
88
117
  ## Building a Stats Page
89
118
 
90
119
  After running `jekyll stats --save`, create a stats page using Liquid:
@@ -25,6 +25,7 @@ module JekyllStats
25
25
  end
26
26
 
27
27
  def process(options)
28
+ Jekyll.logger.log_level = :error if options["json"]
28
29
  options = configuration_from_options(options)
29
30
  site = Jekyll::Site.new(options)
30
31
 
@@ -34,6 +34,16 @@ module JekyllStats
34
34
  lines << categories_line
35
35
  end
36
36
 
37
+ if stats[:internal_links].any?
38
+ lines << ""
39
+ lines << most_linked_posts
40
+ end
41
+
42
+ if stats[:external_links][:domains].any?
43
+ lines << ""
44
+ lines << top_external_domains
45
+ end
46
+
37
47
  if stats[:drafts_count].positive?
38
48
  lines << ""
39
49
  lines << "Drafts: #{stats[:drafts_count]}"
@@ -99,6 +109,24 @@ module JekyllStats
99
109
  "Categories:\n #{cat_strs.join(" | ")}"
100
110
  end
101
111
 
112
+ def most_linked_posts
113
+ links = stats[:internal_links].first(5)
114
+ lines = ["Most Linked Posts:"]
115
+ links.each do |l|
116
+ lines << " #{l[:inbound_count].to_s.rjust(2)} #{truncate(l[:title], 50)}"
117
+ end
118
+ lines.join("\n")
119
+ end
120
+
121
+ def top_external_domains
122
+ ext = stats[:external_links]
123
+ lines = ["External Links: #{format_number(ext[:total])} (#{format_number(ext[:unique])} unique, #{ext[:domains].size} hosts)"]
124
+ ext[:domains].first(5).each do |d|
125
+ lines << " #{d[:count].to_s.rjust(4)} #{d[:host]} (#{d[:posts]} posts)"
126
+ end
127
+ lines.join("\n")
128
+ end
129
+
102
130
  def format_number(n)
103
131
  n.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
104
132
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "uri"
4
+
3
5
  module JekyllStats
4
6
  class StatsCalculator
5
7
  WORDS_PER_MINUTE = 200
@@ -40,6 +42,8 @@ module JekyllStats
40
42
  posts_by_day_of_week: posts_by_day_of_week(posts),
41
43
  tags: tag_counts(posts),
42
44
  categories: category_counts(posts),
45
+ internal_links: internal_links(posts),
46
+ external_links: external_links(posts),
43
47
  drafts_count: drafts_count
44
48
  }
45
49
  end
@@ -147,6 +151,74 @@ module JekyllStats
147
151
  .map { |name, count| { name: name, count: count } }
148
152
  end
149
153
 
154
+ def internal_links(posts)
155
+ by_url = posts.each_with_object({}) { |p, h| h[normalize_url(p.url)] = p }
156
+ exclude = Array(site.config.dig("jekyll-stats", "link_source_exclude_tags")).map { |t| normalize_tag(t) }
157
+ site_url = site.config["url"].to_s
158
+
159
+ inbound = Hash.new { |h, k| h[k] = [] }
160
+ posts.each do |post|
161
+ post_tags = (post.data["tags"] || []).map { |t| normalize_tag(t) }
162
+ next if exclude.any? && (exclude & post_tags).any?
163
+
164
+ post.content.to_s.scan(%r{(?:\]\(|<a\s[^>]*href=["'])([^"'\s)]+)}i).flatten.each do |href|
165
+ href = href.delete_prefix(site_url) unless site_url.empty?
166
+ next unless href.start_with?("/")
167
+
168
+ target = by_url[normalize_url(href)]
169
+ next unless target
170
+ next if target == post
171
+
172
+ inbound[target] << post
173
+ end
174
+ end
175
+
176
+ inbound.map { |target, sources|
177
+ {
178
+ url: target.url,
179
+ title: target.data["title"] || "(untitled)",
180
+ inbound_count: sources.uniq.size,
181
+ inbound_from: sources.uniq.map(&:url).sort
182
+ }
183
+ }.sort_by { |r| [-r[:inbound_count], r[:url]] }
184
+ end
185
+
186
+ def external_links(posts)
187
+ exclude = Array(site.config.dig("jekyll-stats", "link_source_exclude_tags")).map { |t| normalize_tag(t) }
188
+ site_host = (URI.parse(site.config["url"].to_s).host rescue nil)
189
+
190
+ urls = []
191
+ posts.each do |post|
192
+ post_tags = (post.data["tags"] || []).map { |t| normalize_tag(t) }
193
+ next if exclude.any? && (exclude & post_tags).any?
194
+
195
+ post.content.to_s.scan(%r{(?:\]\(|<a\s[^>]*href=["'])(https?://[^"'\s)]+)}i).flatten.each do |href|
196
+ host = URI.parse(href).host rescue nil
197
+ next if host.nil?
198
+ next if site_host && host == site_host
199
+
200
+ urls << [href, host, post]
201
+ end
202
+ end
203
+
204
+ unique = urls.uniq { |u, _, _| u }
205
+ domains = urls.group_by { |_, h, _| h }
206
+ .map { |host, us|
207
+ {
208
+ host: host,
209
+ count: us.map { |u, _, _| u }.uniq.size,
210
+ posts: us.map { |_, _, p| p }.uniq.size
211
+ }
212
+ }
213
+ .sort_by { |d| [-d[:count], d[:host]] }
214
+
215
+ { total: urls.size, unique: unique.size, domains: domains }
216
+ end
217
+
218
+ def normalize_url(url)
219
+ url.sub(/[#?].*\z/, "").sub(%r{/index\.html\z}, "").chomp(".html").chomp("/")
220
+ end
221
+
150
222
  def drafts_count
151
223
  return 0 unless site.respond_to?(:drafts) && site.drafts
152
224
 
@@ -171,6 +243,8 @@ module JekyllStats
171
243
  posts_by_day_of_week: %w[sunday monday tuesday wednesday thursday friday saturday].each_with_object({}) { |d, h| h[d] = 0 },
172
244
  tags: [],
173
245
  categories: [],
246
+ internal_links: [],
247
+ external_links: { total: 0, unique: 0, domains: [] },
174
248
  drafts_count: 0
175
249
  }
176
250
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JekyllStats
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-stats
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Nesbitt
@@ -77,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
77
  - !ruby/object:Gem::Version
78
78
  version: '0'
79
79
  requirements: []
80
- rubygems_version: 4.0.1
80
+ rubygems_version: 4.0.17
81
81
  specification_version: 4
82
82
  summary: Jekyll plugin that generates site statistics
83
83
  test_files: []