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 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
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-12-21
4
+
5
+ - Initial release
@@ -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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JekyllStats
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jekyll"
4
+ require_relative "jekyll-stats/version"
5
+ require_relative "jekyll-stats/stats_calculator"
6
+ require_relative "jekyll-stats/formatter"
7
+ require_relative "jekyll-stats/command"
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: []