jekyll-stats 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE +21 -0
- data/README.md +147 -0
- data/Rakefile +8 -0
- data/lib/jekyll-stats/command.rb +59 -0
- data/lib/jekyll-stats/formatter.rb +122 -0
- data/lib/jekyll-stats/stats_calculator.rb +159 -0
- data/lib/jekyll-stats/version.rb +5 -0
- data/lib/jekyll-stats.rb +7 -0
- metadata +69 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ebf4d6ad1019af942a6644fe37472e28472ccbbe2ac58f3ba6503bf134f563e0
|
|
4
|
+
data.tar.gz: e7c6050c13373f795eca54ed88573ee3c46df3a91b6994839e74e4b53dd869fd
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 81fbfb5befa5de983e88c396c97c45f5271e3b42bf17388ea86345e68349f71d8aff0e02f01900827c833b61264620cd15ebcb88b9beadfd01caf239a6825e67
|
|
7
|
+
data.tar.gz: e833ddc5568285c114f41f75efc29aeb497ff0cfa35388405f80b6b6418b270756f17a8a8be87fe2c5305f7695b0cceb8783a7605e5cf762d8ce0b34a0e6fe92
|
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"jekyll-stats" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["andrewnez@gmail.com"](mailto:"andrewnez@gmail.com").
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Andrew Nesbitt
|
|
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,147 @@
|
|
|
1
|
+
# jekyll-stats
|
|
2
|
+
|
|
3
|
+
A Jekyll plugin that adds a `jekyll stats` command to display site statistics.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Jekyll site's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "jekyll-stats"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run `bundle install`.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
Run from your Jekyll site directory:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Print stats to terminal
|
|
21
|
+
jekyll stats
|
|
22
|
+
|
|
23
|
+
# Save stats to _data/stats.json
|
|
24
|
+
jekyll stats --save
|
|
25
|
+
|
|
26
|
+
# Output raw JSON to stdout
|
|
27
|
+
jekyll stats --json
|
|
28
|
+
|
|
29
|
+
# Include drafts in calculations
|
|
30
|
+
jekyll stats --drafts
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Terminal Output
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Site Statistics
|
|
37
|
+
-----------------------------------
|
|
38
|
+
Posts: 127 (43,521 words, ~3h 38m read time)
|
|
39
|
+
Avg: 343 words | Longest: "My Best Post" (2,847 words)
|
|
40
|
+
First: 2019-03-14 | Last: 2025-12-18 (6.8 years)
|
|
41
|
+
Frequency: 1.6 posts/month
|
|
42
|
+
|
|
43
|
+
Posts by Year:
|
|
44
|
+
2025: ████████████ 24
|
|
45
|
+
2024: ███████████████████ 38
|
|
46
|
+
2023: ██████████████ 28
|
|
47
|
+
|
|
48
|
+
Top 10 Tags:
|
|
49
|
+
ruby (34) | opensource (28) | packages (19)
|
|
50
|
+
|
|
51
|
+
Categories:
|
|
52
|
+
code (45) | cars (32)
|
|
53
|
+
-----------------------------------
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### JSON Output
|
|
57
|
+
|
|
58
|
+
The `--save` flag writes `_data/stats.json` with this structure:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"generated_at": "2025-12-21T09:30:00Z",
|
|
63
|
+
"total_posts": 127,
|
|
64
|
+
"total_words": 43521,
|
|
65
|
+
"reading_minutes": 218,
|
|
66
|
+
"average_words": 343,
|
|
67
|
+
"longest_post": { "title": "My Best Post", "url": "/2024/01/my-best-post", "words": 2847 },
|
|
68
|
+
"shortest_post": { "title": "Quick Note", "url": "/2024/03/quick-note", "words": 89 },
|
|
69
|
+
"first_post": { "title": "Hello World", "url": "/2019/03/hello-world", "date": "2019-03-14" },
|
|
70
|
+
"last_post": { "title": "Recent Post", "url": "/2025/12/recent-post", "date": "2025-12-18" },
|
|
71
|
+
"years_active": 6.8,
|
|
72
|
+
"posts_per_month": 1.6,
|
|
73
|
+
"posts_by_year": [{ "year": 2025, "count": 24 }, { "year": 2024, "count": 38 }],
|
|
74
|
+
"posts_by_month": [{ "month": "2025-12", "count": 3 }],
|
|
75
|
+
"posts_by_day_of_week": { "monday": 23, "tuesday": 18, "wednesday": 15, "thursday": 20, "friday": 22, "saturday": 14, "sunday": 15 },
|
|
76
|
+
"tags": [{ "name": "ruby", "count": 34 }, { "name": "opensource", "count": 28 }],
|
|
77
|
+
"categories": [{ "name": "code", "count": 45 }, { "name": "cars", "count": 32 }],
|
|
78
|
+
"drafts_count": 3
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Building a Stats Page
|
|
83
|
+
|
|
84
|
+
After running `jekyll stats --save`, create a stats page using Liquid:
|
|
85
|
+
|
|
86
|
+
```liquid
|
|
87
|
+
---
|
|
88
|
+
layout: page
|
|
89
|
+
title: Site Statistics
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
<p>
|
|
93
|
+
<strong>{{ site.data.stats.total_posts }}</strong> posts,
|
|
94
|
+
<strong>{{ site.data.stats.total_words | divided_by: 1000 }}k</strong> words,
|
|
95
|
+
<strong>{{ site.data.stats.reading_minutes | divided_by: 60 }}</strong> hours of reading.
|
|
96
|
+
</p>
|
|
97
|
+
|
|
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>
|
|
102
|
+
|
|
103
|
+
<h2>Posts by Year</h2>
|
|
104
|
+
<ul>
|
|
105
|
+
{% for year in site.data.stats.posts_by_year %}
|
|
106
|
+
<li>{{ year.year }}: {{ year.count }} posts</li>
|
|
107
|
+
{% endfor %}
|
|
108
|
+
</ul>
|
|
109
|
+
|
|
110
|
+
<h2>Top Tags</h2>
|
|
111
|
+
<ul>
|
|
112
|
+
{% for tag in site.data.stats.tags limit:10 %}
|
|
113
|
+
<li>{{ tag.name }} ({{ tag.count }})</li>
|
|
114
|
+
{% endfor %}
|
|
115
|
+
</ul>
|
|
116
|
+
|
|
117
|
+
<h2>Extremes</h2>
|
|
118
|
+
<ul>
|
|
119
|
+
<li>Longest: <a href="{{ site.data.stats.longest_post.url }}">{{ site.data.stats.longest_post.title }}</a> ({{ site.data.stats.longest_post.words }} words)</li>
|
|
120
|
+
<li>Shortest: <a href="{{ site.data.stats.shortest_post.url }}">{{ site.data.stats.shortest_post.title }}</a> ({{ site.data.stats.shortest_post.words }} words)</li>
|
|
121
|
+
</ul>
|
|
122
|
+
|
|
123
|
+
<h2>Day of Week</h2>
|
|
124
|
+
<p>
|
|
125
|
+
I write most on
|
|
126
|
+
{% assign max_day = "" %}
|
|
127
|
+
{% assign max_count = 0 %}
|
|
128
|
+
{% for day in site.data.stats.posts_by_day_of_week %}
|
|
129
|
+
{% if day[1] > max_count %}
|
|
130
|
+
{% assign max_day = day[0] %}
|
|
131
|
+
{% assign max_count = day[1] %}
|
|
132
|
+
{% endif %}
|
|
133
|
+
{% endfor %}
|
|
134
|
+
{{ max_day | capitalize }}s ({{ max_count }} posts).
|
|
135
|
+
</p>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
bin/setup
|
|
142
|
+
rake test
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module JekyllStats
|
|
7
|
+
class Command < Jekyll::Command
|
|
8
|
+
class << self
|
|
9
|
+
def init_with_program(prog)
|
|
10
|
+
prog.command(:stats) do |c|
|
|
11
|
+
c.syntax "stats [options]"
|
|
12
|
+
c.description "Display site statistics"
|
|
13
|
+
c.option "save", "--save", "Save stats to _data/stats.json"
|
|
14
|
+
c.option "json", "--json", "Output raw JSON to stdout"
|
|
15
|
+
c.option "drafts", "-D", "--drafts", "Include drafts in calculations"
|
|
16
|
+
c.option "config", "--config CONFIG_FILE[,CONFIG_FILE2,...]", Array, "Custom configuration file"
|
|
17
|
+
c.option "source", "-s", "--source SOURCE", "Custom source directory"
|
|
18
|
+
c.option "destination", "-d", "--destination DESTINATION", "Custom destination directory"
|
|
19
|
+
|
|
20
|
+
c.action do |_args, options|
|
|
21
|
+
process(options)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def process(options)
|
|
27
|
+
options = configuration_from_options(options)
|
|
28
|
+
site = Jekyll::Site.new(options)
|
|
29
|
+
|
|
30
|
+
Jekyll.logger.info "Loading site..."
|
|
31
|
+
site.reset
|
|
32
|
+
site.read
|
|
33
|
+
|
|
34
|
+
calculator = StatsCalculator.new(site, include_drafts: options["drafts"])
|
|
35
|
+
stats = calculator.calculate
|
|
36
|
+
|
|
37
|
+
if options["json"]
|
|
38
|
+
puts JSON.pretty_generate(stats)
|
|
39
|
+
else
|
|
40
|
+
formatter = Formatter.new(stats)
|
|
41
|
+
puts formatter.to_terminal
|
|
42
|
+
|
|
43
|
+
if options["save"]
|
|
44
|
+
save_stats(site, stats)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def save_stats(site, stats)
|
|
50
|
+
data_dir = File.join(site.source, "_data")
|
|
51
|
+
FileUtils.mkdir_p(data_dir)
|
|
52
|
+
|
|
53
|
+
path = File.join(data_dir, "stats.json")
|
|
54
|
+
File.write(path, JSON.pretty_generate(stats))
|
|
55
|
+
Jekyll.logger.info "Stats saved to #{path}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllStats
|
|
4
|
+
class Formatter
|
|
5
|
+
attr_reader :stats
|
|
6
|
+
|
|
7
|
+
def initialize(stats)
|
|
8
|
+
@stats = stats
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_terminal
|
|
12
|
+
return "No posts found." if stats[:total_posts].zero?
|
|
13
|
+
|
|
14
|
+
lines = []
|
|
15
|
+
lines << ""
|
|
16
|
+
lines << "\u{1F4CA} Site Statistics"
|
|
17
|
+
lines << "\u2500" * 35
|
|
18
|
+
|
|
19
|
+
lines << post_summary
|
|
20
|
+
lines << averages_line
|
|
21
|
+
lines << date_range_line
|
|
22
|
+
lines << frequency_line
|
|
23
|
+
|
|
24
|
+
lines << ""
|
|
25
|
+
lines << posts_by_year_chart
|
|
26
|
+
|
|
27
|
+
if stats[:tags].any?
|
|
28
|
+
lines << ""
|
|
29
|
+
lines << top_tags
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if stats[:categories].any?
|
|
33
|
+
lines << ""
|
|
34
|
+
lines << categories_line
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if stats[:drafts_count].positive?
|
|
38
|
+
lines << ""
|
|
39
|
+
lines << "Drafts: #{stats[:drafts_count]}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
lines << "\u2500" * 35
|
|
43
|
+
lines << ""
|
|
44
|
+
|
|
45
|
+
lines.join("\n")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def post_summary
|
|
49
|
+
total = stats[:total_posts]
|
|
50
|
+
words = format_number(stats[:total_words])
|
|
51
|
+
time = format_reading_time(stats[:reading_minutes])
|
|
52
|
+
"Posts: #{total} (#{words} words, ~#{time} read time)"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def averages_line
|
|
56
|
+
avg = stats[:average_words]
|
|
57
|
+
longest = stats[:longest_post]
|
|
58
|
+
"Avg: #{avg} words | Longest: \"#{truncate(longest[:title], 30)}\" (#{format_number(longest[:words])} words)"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def date_range_line
|
|
62
|
+
first = stats[:first_post][:date]
|
|
63
|
+
last = stats[:last_post][:date]
|
|
64
|
+
years = stats[:years_active]
|
|
65
|
+
"First: #{first} | Last: #{last} (#{years} years)"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def frequency_line
|
|
69
|
+
"Frequency: #{stats[:posts_per_month]} posts/month"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def posts_by_year_chart
|
|
73
|
+
years = stats[:posts_by_year]
|
|
74
|
+
return "" if years.empty?
|
|
75
|
+
|
|
76
|
+
max_count = years.map { |y| y[:count] }.max
|
|
77
|
+
bar_width = 20
|
|
78
|
+
|
|
79
|
+
lines = ["Posts by Year:"]
|
|
80
|
+
years.first(10).each do |year_data|
|
|
81
|
+
year = year_data[:year]
|
|
82
|
+
count = year_data[:count]
|
|
83
|
+
bar_length = ((count.to_f / max_count) * bar_width).round
|
|
84
|
+
bar = "\u2588" * bar_length
|
|
85
|
+
lines << " #{year}: #{bar} #{count}"
|
|
86
|
+
end
|
|
87
|
+
lines.join("\n")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def top_tags
|
|
91
|
+
tags = stats[:tags].first(10)
|
|
92
|
+
tag_strs = tags.map { |t| "#{t[:name]} (#{t[:count]})" }
|
|
93
|
+
"Top #{[10, stats[:tags].size].min} Tags:\n #{tag_strs.join(" | ")}"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def categories_line
|
|
97
|
+
cats = stats[:categories]
|
|
98
|
+
cat_strs = cats.map { |c| "#{c[:name]} (#{c[:count]})" }
|
|
99
|
+
"Categories:\n #{cat_strs.join(" | ")}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def format_number(n)
|
|
103
|
+
n.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def format_reading_time(minutes)
|
|
107
|
+
if minutes >= 60
|
|
108
|
+
hours = minutes / 60
|
|
109
|
+
mins = minutes % 60
|
|
110
|
+
"#{hours}h #{mins}m"
|
|
111
|
+
else
|
|
112
|
+
"#{minutes}m"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def truncate(str, length)
|
|
117
|
+
return str if str.length <= length
|
|
118
|
+
|
|
119
|
+
"#{str[0, length]}..."
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllStats
|
|
4
|
+
class StatsCalculator
|
|
5
|
+
WORDS_PER_MINUTE = 200
|
|
6
|
+
|
|
7
|
+
attr_reader :site, :include_drafts
|
|
8
|
+
|
|
9
|
+
def initialize(site, include_drafts: false)
|
|
10
|
+
@site = site
|
|
11
|
+
@include_drafts = include_drafts
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def calculate
|
|
15
|
+
posts = collect_posts
|
|
16
|
+
return empty_stats if posts.empty?
|
|
17
|
+
|
|
18
|
+
word_counts = posts.map { |p| [p, word_count(p)] }
|
|
19
|
+
sorted_by_words = word_counts.sort_by { |_, count| -count }
|
|
20
|
+
sorted_by_date = posts.sort_by { |p| p.date }
|
|
21
|
+
|
|
22
|
+
total_words = word_counts.sum { |_, count| count }
|
|
23
|
+
dates = sorted_by_date.map(&:date)
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
generated_at: Time.now.utc.iso8601,
|
|
27
|
+
total_posts: posts.size,
|
|
28
|
+
total_words: total_words,
|
|
29
|
+
reading_minutes: (total_words / WORDS_PER_MINUTE.to_f).ceil,
|
|
30
|
+
average_words: (total_words / posts.size.to_f).round,
|
|
31
|
+
longest_post: post_info(sorted_by_words.first[0], sorted_by_words.first[1]),
|
|
32
|
+
shortest_post: post_info(sorted_by_words.last[0], sorted_by_words.last[1]),
|
|
33
|
+
first_post: post_info_with_date(sorted_by_date.first),
|
|
34
|
+
last_post: post_info_with_date(sorted_by_date.last),
|
|
35
|
+
years_active: years_active(dates.first, dates.last),
|
|
36
|
+
posts_per_month: posts_per_month(posts.size, dates.first, dates.last),
|
|
37
|
+
posts_by_year: posts_by_year(posts),
|
|
38
|
+
posts_by_month: posts_by_month(posts),
|
|
39
|
+
posts_by_day_of_week: posts_by_day_of_week(posts),
|
|
40
|
+
tags: tag_counts(posts),
|
|
41
|
+
categories: category_counts(posts),
|
|
42
|
+
drafts_count: drafts_count
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def collect_posts
|
|
47
|
+
posts = site.posts.docs.dup
|
|
48
|
+
posts += site.drafts if include_drafts && site.respond_to?(:drafts)
|
|
49
|
+
posts
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def word_count(post)
|
|
53
|
+
content = post.content.to_s
|
|
54
|
+
text = content.gsub(/<[^>]*>/, " ")
|
|
55
|
+
text = text.gsub(/```[\s\S]*?```/, " ")
|
|
56
|
+
text = text.gsub(/`[^`]*`/, " ")
|
|
57
|
+
text = text.gsub(/\[([^\]]*)\]\([^)]*\)/, '\1')
|
|
58
|
+
text = text.gsub(/[#*_~`]/, "")
|
|
59
|
+
text.split(/\s+/).count { |w| w.match?(/\w/) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def post_info(post, words)
|
|
63
|
+
{
|
|
64
|
+
title: post.data["title"] || "(untitled)",
|
|
65
|
+
url: post.url,
|
|
66
|
+
words: words
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def post_info_with_date(post)
|
|
71
|
+
{
|
|
72
|
+
title: post.data["title"] || "(untitled)",
|
|
73
|
+
url: post.url,
|
|
74
|
+
date: post.date.strftime("%Y-%m-%d")
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def years_active(first_date, last_date)
|
|
79
|
+
seconds = (last_date - first_date).to_f
|
|
80
|
+
days = seconds / 86400.0
|
|
81
|
+
(days / 365.25).round(1)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def posts_per_month(count, first_date, last_date)
|
|
85
|
+
months = ((last_date.year - first_date.year) * 12) + (last_date.month - first_date.month) + 1
|
|
86
|
+
(count / months.to_f).round(1)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def posts_by_year(posts)
|
|
90
|
+
counts = posts.group_by { |p| p.date.year }
|
|
91
|
+
.transform_values(&:size)
|
|
92
|
+
.sort_by { |year, _| -year }
|
|
93
|
+
counts.map { |year, count| { year: year, count: count } }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def posts_by_month(posts)
|
|
97
|
+
counts = posts.group_by { |p| p.date.strftime("%Y-%m") }
|
|
98
|
+
.transform_values(&:size)
|
|
99
|
+
.sort_by { |month, _| month }
|
|
100
|
+
.reverse
|
|
101
|
+
counts.map { |month, count| { month: month, count: count } }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def posts_by_day_of_week(posts)
|
|
105
|
+
days = %w[sunday monday tuesday wednesday thursday friday saturday]
|
|
106
|
+
counts = Hash.new(0)
|
|
107
|
+
posts.each { |p| counts[days[p.date.wday]] += 1 }
|
|
108
|
+
days.each_with_object({}) { |day, h| h[day] = counts[day] }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def tag_counts(posts)
|
|
112
|
+
counts = Hash.new(0)
|
|
113
|
+
posts.each do |post|
|
|
114
|
+
tags = post.data["tags"] || []
|
|
115
|
+
tags.each { |tag| counts[tag] += 1 }
|
|
116
|
+
end
|
|
117
|
+
counts.sort_by { |_, count| -count }
|
|
118
|
+
.map { |name, count| { name: name, count: count } }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def category_counts(posts)
|
|
122
|
+
counts = Hash.new(0)
|
|
123
|
+
posts.each do |post|
|
|
124
|
+
categories = post.data["categories"] || []
|
|
125
|
+
categories.each { |cat| counts[cat] += 1 }
|
|
126
|
+
end
|
|
127
|
+
counts.sort_by { |_, count| -count }
|
|
128
|
+
.map { |name, count| { name: name, count: count } }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def drafts_count
|
|
132
|
+
return 0 unless site.respond_to?(:drafts) && site.drafts
|
|
133
|
+
|
|
134
|
+
site.drafts.size
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def empty_stats
|
|
138
|
+
{
|
|
139
|
+
generated_at: Time.now.utc.iso8601,
|
|
140
|
+
total_posts: 0,
|
|
141
|
+
total_words: 0,
|
|
142
|
+
reading_minutes: 0,
|
|
143
|
+
average_words: 0,
|
|
144
|
+
longest_post: nil,
|
|
145
|
+
shortest_post: nil,
|
|
146
|
+
first_post: nil,
|
|
147
|
+
last_post: nil,
|
|
148
|
+
years_active: 0,
|
|
149
|
+
posts_per_month: 0,
|
|
150
|
+
posts_by_year: [],
|
|
151
|
+
posts_by_month: [],
|
|
152
|
+
posts_by_day_of_week: %w[sunday monday tuesday wednesday thursday friday saturday].each_with_object({}) { |d, h| h[d] = 0 },
|
|
153
|
+
tags: [],
|
|
154
|
+
categories: [],
|
|
155
|
+
drafts_count: 0
|
|
156
|
+
}
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
data/lib/jekyll-stats.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: jekyll-stats
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andrew Nesbitt
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 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: Adds a 'jekyll stats' command that computes and displays site statistics
|
|
27
|
+
including post counts, word counts, reading times, tag/category distributions, and
|
|
28
|
+
posting frequency.
|
|
29
|
+
email:
|
|
30
|
+
- andrewnez@gmail.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- CHANGELOG.md
|
|
36
|
+
- CODE_OF_CONDUCT.md
|
|
37
|
+
- LICENSE
|
|
38
|
+
- README.md
|
|
39
|
+
- Rakefile
|
|
40
|
+
- lib/jekyll-stats.rb
|
|
41
|
+
- lib/jekyll-stats/command.rb
|
|
42
|
+
- lib/jekyll-stats/formatter.rb
|
|
43
|
+
- lib/jekyll-stats/stats_calculator.rb
|
|
44
|
+
- lib/jekyll-stats/version.rb
|
|
45
|
+
homepage: https://github.com/andrew/jekyll-stats
|
|
46
|
+
licenses:
|
|
47
|
+
- MIT
|
|
48
|
+
metadata:
|
|
49
|
+
homepage_uri: https://github.com/andrew/jekyll-stats
|
|
50
|
+
source_code_uri: https://github.com/andrew/jekyll-stats
|
|
51
|
+
changelog_uri: https://github.com/andrew/jekyll-stats/blob/main/CHANGELOG.md
|
|
52
|
+
rdoc_options: []
|
|
53
|
+
require_paths:
|
|
54
|
+
- lib
|
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: 2.7.0
|
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: '0'
|
|
65
|
+
requirements: []
|
|
66
|
+
rubygems_version: 4.0.1
|
|
67
|
+
specification_version: 4
|
|
68
|
+
summary: Jekyll plugin that generates site statistics
|
|
69
|
+
test_files: []
|