jekyll-stats 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +109 -35
- data/lib/jekyll-stats/command.rb +3 -1
- data/lib/jekyll-stats/formatter.rb +14 -0
- data/lib/jekyll-stats/stats_calculator.rb +61 -4
- data/lib/jekyll-stats/version.rb +1 -1
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3465b65818c34b7a72b5b76d9bb242a37487837e5639927ae65c7d9a00b9ac62
|
|
4
|
+
data.tar.gz: 9e408de4fcfb55f1581a2b1905018852095cd4ae5c9a4a2f7592e252e24e42b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bd03f85ad69f529b003a211a7004acdf0a30851f0a2084abab6f3f142a511a620d7c3d4f755f4dcab6abc2286c55bb5f25c7bec610ccf565e0f012e975ba21d7
|
|
7
|
+
data.tar.gz: d42e6b8897070b6529b1cc32f7e6e52c11563e2890bcfdd5c03f4a2237216cb2afb80c62d2ec94489fe6433ffb3706e954d308e31c72a1b4f67fa7a486716ab1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-08-30
|
|
4
|
+
|
|
5
|
+
- Add `internal_links` to stats: posts ranked by inbound links from other posts
|
|
6
|
+
- Source posts can be excluded by tag via `jekyll-stats.link_source_exclude_tags` in `_config.yml`
|
|
7
|
+
- `--json` no longer prints "Loading site..." to stdout, so output pipes cleanly to `jq`
|
|
8
|
+
|
|
9
|
+
## [0.2.0] - 2026-01-03
|
|
10
|
+
|
|
11
|
+
- Add `--tags` / `-t` option to filter stats by tags
|
|
12
|
+
|
|
3
13
|
## [0.1.0] - 2025-12-21
|
|
4
14
|
|
|
5
15
|
- Initial release
|
data/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A Jekyll plugin that adds a `jekyll stats` command to display site statistics.
|
|
4
4
|
|
|
5
|
+
[rubygems.org/gems/jekyll-stats](https://rubygems.org/gems/jekyll-stats)
|
|
6
|
+
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
7
9
|
Add to your Jekyll site's Gemfile:
|
|
@@ -28,6 +30,10 @@ jekyll stats --json
|
|
|
28
30
|
|
|
29
31
|
# Include drafts in calculations
|
|
30
32
|
jekyll stats --drafts
|
|
33
|
+
|
|
34
|
+
# Filter by tags (comma-separated, matches posts with ANY of the tags)
|
|
35
|
+
jekyll stats --tags ruby
|
|
36
|
+
jekyll stats -t ruby,rails
|
|
31
37
|
```
|
|
32
38
|
|
|
33
39
|
### Terminal Output
|
|
@@ -75,10 +81,27 @@ The `--save` flag writes `_data/stats.json` with this structure:
|
|
|
75
81
|
"posts_by_day_of_week": { "monday": 23, "tuesday": 18, "wednesday": 15, "thursday": 20, "friday": 22, "saturday": 14, "sunday": 15 },
|
|
76
82
|
"tags": [{ "name": "ruby", "count": 34 }, { "name": "opensource", "count": 28 }],
|
|
77
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
|
+
],
|
|
78
87
|
"drafts_count": 3
|
|
79
88
|
}
|
|
80
89
|
```
|
|
81
90
|
|
|
91
|
+
### Internal links
|
|
92
|
+
|
|
93
|
+
`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.
|
|
94
|
+
|
|
95
|
+
To stop link-list style posts (year in review, roundups) inflating the counts, exclude them as sources by tag in `_config.yml`:
|
|
96
|
+
|
|
97
|
+
```yaml
|
|
98
|
+
jekyll-stats:
|
|
99
|
+
link_source_exclude_tags:
|
|
100
|
+
- roundup
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Excluded posts still appear as targets; they just don't contribute outbound links.
|
|
104
|
+
|
|
82
105
|
## Building a Stats Page
|
|
83
106
|
|
|
84
107
|
After running `jekyll stats --save`, create a stats page using Liquid:
|
|
@@ -89,50 +112,101 @@ layout: page
|
|
|
89
112
|
title: Site Statistics
|
|
90
113
|
---
|
|
91
114
|
|
|
92
|
-
<p>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
<strong>{{ site.data.stats.reading_minutes | divided_by: 60 }}</strong> hours of reading.
|
|
96
|
-
</p>
|
|
115
|
+
<p>{{ site.data.stats.total_posts }} posts, {{ site.data.stats.total_words }} words, ~{{ site.data.stats.reading_minutes }} minutes reading time.</p>
|
|
116
|
+
|
|
117
|
+
<p>Average {{ site.data.stats.average_words }} words per post. Longest: <a href="{{ site.data.stats.longest_post.url }}">{{ site.data.stats.longest_post.title }}</a> ({{ site.data.stats.longest_post.words }} words).</p>
|
|
97
118
|
|
|
98
|
-
<p>
|
|
99
|
-
Writing since {{ site.data.stats.first_post.date }} ({{ site.data.stats.years_active }} years).
|
|
100
|
-
Averaging {{ site.data.stats.posts_per_month }} posts per month.
|
|
101
|
-
</p>
|
|
119
|
+
<p>First post: {{ site.data.stats.first_post.date }} | Last post: {{ site.data.stats.last_post.date }} | {{ site.data.stats.years_active }} years active.</p>
|
|
102
120
|
|
|
103
121
|
<h2>Posts by Year</h2>
|
|
104
|
-
<
|
|
122
|
+
<table>
|
|
123
|
+
<thead>
|
|
124
|
+
<tr>
|
|
125
|
+
<th style="text-align: left">Year</th>
|
|
126
|
+
<th style="text-align: right">Posts</th>
|
|
127
|
+
</tr>
|
|
128
|
+
</thead>
|
|
129
|
+
<tbody>
|
|
105
130
|
{% for year in site.data.stats.posts_by_year %}
|
|
106
|
-
|
|
131
|
+
<tr>
|
|
132
|
+
<td>{{ year.year }}</td>
|
|
133
|
+
<td style="text-align: right">{{ year.count }}</td>
|
|
134
|
+
</tr>
|
|
107
135
|
{% endfor %}
|
|
108
|
-
</
|
|
136
|
+
</tbody>
|
|
137
|
+
</table>
|
|
109
138
|
|
|
110
139
|
<h2>Top Tags</h2>
|
|
111
|
-
<
|
|
140
|
+
<table>
|
|
141
|
+
<thead>
|
|
142
|
+
<tr>
|
|
143
|
+
<th style="text-align: left">Tag</th>
|
|
144
|
+
<th style="text-align: right">Posts</th>
|
|
145
|
+
</tr>
|
|
146
|
+
</thead>
|
|
147
|
+
<tbody>
|
|
112
148
|
{% for tag in site.data.stats.tags limit:10 %}
|
|
113
|
-
|
|
149
|
+
<tr>
|
|
150
|
+
<td>{{ tag.name }}</td>
|
|
151
|
+
<td style="text-align: right">{{ tag.count }}</td>
|
|
152
|
+
</tr>
|
|
114
153
|
{% endfor %}
|
|
115
|
-
</
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
154
|
+
</tbody>
|
|
155
|
+
</table>
|
|
156
|
+
|
|
157
|
+
<p><small>Generated {{ site.data.stats.generated_at | date: "%b %d, %Y" }}</small></p>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Automating Stats Updates
|
|
161
|
+
|
|
162
|
+
You can regenerate stats automatically before each build.
|
|
163
|
+
|
|
164
|
+
### Rake Task
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
# Rakefile
|
|
168
|
+
task :build do
|
|
169
|
+
sh "jekyll stats --save"
|
|
170
|
+
sh "jekyll build"
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### GitHub Actions
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
# .github/workflows/deploy.yml
|
|
178
|
+
jobs:
|
|
179
|
+
build:
|
|
180
|
+
runs-on: ubuntu-latest
|
|
181
|
+
steps:
|
|
182
|
+
- uses: actions/checkout@v4
|
|
183
|
+
- uses: ruby/setup-ruby@v1
|
|
184
|
+
with:
|
|
185
|
+
ruby-version: '3.3'
|
|
186
|
+
bundler-cache: true
|
|
187
|
+
|
|
188
|
+
- name: Generate stats
|
|
189
|
+
run: bundle exec jekyll stats --save
|
|
190
|
+
|
|
191
|
+
- name: Commit stats
|
|
192
|
+
run: |
|
|
193
|
+
git config user.name "github-actions[bot]"
|
|
194
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
195
|
+
git add _data/stats.json
|
|
196
|
+
git diff --staged --quiet || git commit -m "Update site stats"
|
|
197
|
+
git push
|
|
198
|
+
|
|
199
|
+
- name: Build site
|
|
200
|
+
run: bundle exec jekyll build
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Git Pre-commit Hook
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
#!/bin/sh
|
|
207
|
+
# .git/hooks/pre-commit
|
|
208
|
+
bundle exec jekyll stats --save
|
|
209
|
+
git add _data/stats.json
|
|
136
210
|
```
|
|
137
211
|
|
|
138
212
|
## Development
|
data/lib/jekyll-stats/command.rb
CHANGED
|
@@ -13,6 +13,7 @@ module JekyllStats
|
|
|
13
13
|
c.option "save", "--save", "Save stats to _data/stats.json"
|
|
14
14
|
c.option "json", "--json", "Output raw JSON to stdout"
|
|
15
15
|
c.option "drafts", "-D", "--drafts", "Include drafts in calculations"
|
|
16
|
+
c.option "tags", "-t", "--tags TAGS", Array, "Filter stats by tags (comma-separated)"
|
|
16
17
|
c.option "config", "--config CONFIG_FILE[,CONFIG_FILE2,...]", Array, "Custom configuration file"
|
|
17
18
|
c.option "source", "-s", "--source SOURCE", "Custom source directory"
|
|
18
19
|
c.option "destination", "-d", "--destination DESTINATION", "Custom destination directory"
|
|
@@ -24,6 +25,7 @@ module JekyllStats
|
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def process(options)
|
|
28
|
+
Jekyll.logger.log_level = :error if options["json"]
|
|
27
29
|
options = configuration_from_options(options)
|
|
28
30
|
site = Jekyll::Site.new(options)
|
|
29
31
|
|
|
@@ -31,7 +33,7 @@ module JekyllStats
|
|
|
31
33
|
site.reset
|
|
32
34
|
site.read
|
|
33
35
|
|
|
34
|
-
calculator = StatsCalculator.new(site, include_drafts: options["drafts"])
|
|
36
|
+
calculator = StatsCalculator.new(site, include_drafts: options["drafts"], filter_tags: options["tags"])
|
|
35
37
|
stats = calculator.calculate
|
|
36
38
|
|
|
37
39
|
if options["json"]
|
|
@@ -34,6 +34,11 @@ 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
|
+
|
|
37
42
|
if stats[:drafts_count].positive?
|
|
38
43
|
lines << ""
|
|
39
44
|
lines << "Drafts: #{stats[:drafts_count]}"
|
|
@@ -99,6 +104,15 @@ module JekyllStats
|
|
|
99
104
|
"Categories:\n #{cat_strs.join(" | ")}"
|
|
100
105
|
end
|
|
101
106
|
|
|
107
|
+
def most_linked_posts
|
|
108
|
+
links = stats[:internal_links].first(5)
|
|
109
|
+
lines = ["Most Linked Posts:"]
|
|
110
|
+
links.each do |l|
|
|
111
|
+
lines << " #{l[:inbound_count].to_s.rjust(2)} #{truncate(l[:title], 50)}"
|
|
112
|
+
end
|
|
113
|
+
lines.join("\n")
|
|
114
|
+
end
|
|
115
|
+
|
|
102
116
|
def format_number(n)
|
|
103
117
|
n.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
104
118
|
end
|
|
@@ -4,11 +4,12 @@ module JekyllStats
|
|
|
4
4
|
class StatsCalculator
|
|
5
5
|
WORDS_PER_MINUTE = 200
|
|
6
6
|
|
|
7
|
-
attr_reader :site, :include_drafts
|
|
7
|
+
attr_reader :site, :include_drafts, :filter_tags
|
|
8
8
|
|
|
9
|
-
def initialize(site, include_drafts: false)
|
|
9
|
+
def initialize(site, include_drafts: false, filter_tags: nil)
|
|
10
10
|
@site = site
|
|
11
11
|
@include_drafts = include_drafts
|
|
12
|
+
@filter_tags = normalize_filter_tags(filter_tags)
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def calculate
|
|
@@ -39,6 +40,7 @@ module JekyllStats
|
|
|
39
40
|
posts_by_day_of_week: posts_by_day_of_week(posts),
|
|
40
41
|
tags: tag_counts(posts),
|
|
41
42
|
categories: category_counts(posts),
|
|
43
|
+
internal_links: internal_links(posts),
|
|
42
44
|
drafts_count: drafts_count
|
|
43
45
|
}
|
|
44
46
|
end
|
|
@@ -46,9 +48,23 @@ module JekyllStats
|
|
|
46
48
|
def collect_posts
|
|
47
49
|
posts = site.posts.docs.dup
|
|
48
50
|
posts += site.drafts if include_drafts && site.respond_to?(:drafts)
|
|
51
|
+
posts = filter_posts_by_tags(posts) if filter_tags
|
|
49
52
|
posts
|
|
50
53
|
end
|
|
51
54
|
|
|
55
|
+
def filter_posts_by_tags(posts)
|
|
56
|
+
posts.select do |post|
|
|
57
|
+
post_tags = (post.data["tags"] || []).map { |t| normalize_tag(t) }
|
|
58
|
+
(filter_tags & post_tags).any?
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def normalize_filter_tags(tags)
|
|
63
|
+
return nil if tags.nil? || tags.empty?
|
|
64
|
+
|
|
65
|
+
tags.map { |t| normalize_tag(t) }
|
|
66
|
+
end
|
|
67
|
+
|
|
52
68
|
def word_count(post)
|
|
53
69
|
content = post.content.to_s
|
|
54
70
|
text = content.gsub(/<[^>]*>/, " ")
|
|
@@ -112,22 +128,62 @@ module JekyllStats
|
|
|
112
128
|
counts = Hash.new(0)
|
|
113
129
|
posts.each do |post|
|
|
114
130
|
tags = post.data["tags"] || []
|
|
115
|
-
tags.each { |tag| counts[tag] += 1 }
|
|
131
|
+
tags.each { |tag| counts[normalize_tag(tag)] += 1 }
|
|
116
132
|
end
|
|
117
133
|
counts.sort_by { |_, count| -count }
|
|
118
134
|
.map { |name, count| { name: name, count: count } }
|
|
119
135
|
end
|
|
120
136
|
|
|
137
|
+
def normalize_tag(tag)
|
|
138
|
+
tag.to_s.strip.gsub(/[,;:]+\z/, "").strip
|
|
139
|
+
end
|
|
140
|
+
|
|
121
141
|
def category_counts(posts)
|
|
122
142
|
counts = Hash.new(0)
|
|
123
143
|
posts.each do |post|
|
|
124
144
|
categories = post.data["categories"] || []
|
|
125
|
-
categories.each { |cat| counts[cat] += 1 }
|
|
145
|
+
categories.each { |cat| counts[normalize_tag(cat)] += 1 }
|
|
126
146
|
end
|
|
127
147
|
counts.sort_by { |_, count| -count }
|
|
128
148
|
.map { |name, count| { name: name, count: count } }
|
|
129
149
|
end
|
|
130
150
|
|
|
151
|
+
def internal_links(posts)
|
|
152
|
+
by_url = posts.each_with_object({}) { |p, h| h[normalize_url(p.url)] = p }
|
|
153
|
+
exclude = Array(site.config.dig("jekyll-stats", "link_source_exclude_tags")).map { |t| normalize_tag(t) }
|
|
154
|
+
site_url = site.config["url"].to_s
|
|
155
|
+
|
|
156
|
+
inbound = Hash.new { |h, k| h[k] = [] }
|
|
157
|
+
posts.each do |post|
|
|
158
|
+
post_tags = (post.data["tags"] || []).map { |t| normalize_tag(t) }
|
|
159
|
+
next if exclude.any? && (exclude & post_tags).any?
|
|
160
|
+
|
|
161
|
+
post.content.to_s.scan(%r{(?:\]\(|<a\s[^>]*href=["'])([^"'\s)]+)}i).flatten.each do |href|
|
|
162
|
+
href = href.delete_prefix(site_url) unless site_url.empty?
|
|
163
|
+
next unless href.start_with?("/")
|
|
164
|
+
|
|
165
|
+
target = by_url[normalize_url(href)]
|
|
166
|
+
next unless target
|
|
167
|
+
next if target == post
|
|
168
|
+
|
|
169
|
+
inbound[target] << post
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
inbound.map { |target, sources|
|
|
174
|
+
{
|
|
175
|
+
url: target.url,
|
|
176
|
+
title: target.data["title"] || "(untitled)",
|
|
177
|
+
inbound_count: sources.uniq.size,
|
|
178
|
+
inbound_from: sources.uniq.map(&:url).sort
|
|
179
|
+
}
|
|
180
|
+
}.sort_by { |r| [-r[:inbound_count], r[:url]] }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def normalize_url(url)
|
|
184
|
+
url.sub(/[#?].*\z/, "").sub(%r{/index\.html\z}, "").chomp(".html").chomp("/")
|
|
185
|
+
end
|
|
186
|
+
|
|
131
187
|
def drafts_count
|
|
132
188
|
return 0 unless site.respond_to?(:drafts) && site.drafts
|
|
133
189
|
|
|
@@ -152,6 +208,7 @@ module JekyllStats
|
|
|
152
208
|
posts_by_day_of_week: %w[sunday monday tuesday wednesday thursday friday saturday].each_with_object({}) { |d, h| h[d] = 0 },
|
|
153
209
|
tags: [],
|
|
154
210
|
categories: [],
|
|
211
|
+
internal_links: [],
|
|
155
212
|
drafts_count: 0
|
|
156
213
|
}
|
|
157
214
|
end
|
data/lib/jekyll-stats/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Nesbitt
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '4.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: logger
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
26
40
|
description: Adds a 'jekyll stats' command that computes and displays site statistics
|
|
27
41
|
including post counts, word counts, reading times, tag/category distributions, and
|
|
28
42
|
posting frequency.
|
|
@@ -63,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
63
77
|
- !ruby/object:Gem::Version
|
|
64
78
|
version: '0'
|
|
65
79
|
requirements: []
|
|
66
|
-
rubygems_version: 4.0.
|
|
80
|
+
rubygems_version: 4.0.17
|
|
67
81
|
specification_version: 4
|
|
68
82
|
summary: Jekyll plugin that generates site statistics
|
|
69
83
|
test_files: []
|