jekyll-theme-zer0 0.10.6 → 0.15.2
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 +428 -0
- data/README.md +79 -31
- data/_data/README.md +419 -17
- data/_data/generate_statistics.rb +216 -9
- data/_data/generate_statistics.sh +106 -0
- data/_data/github-actions-example.yml +210 -0
- data/_data/navigation/about.yml +39 -11
- data/_data/navigation/docs.yml +53 -23
- data/_data/navigation/home.yml +27 -9
- data/_data/navigation/main.yml +27 -8
- data/_data/navigation/posts.yml +22 -6
- data/_data/navigation/quickstart.yml +19 -6
- data/_data/posts_organization.yml +153 -0
- data/_data/prerequisites.yml +112 -0
- data/_data/statistics_config.yml +203 -0
- data/_data/ui-text.yml +321 -0
- data/_data/update_statistics.sh +126 -0
- data/_includes/README.md +2 -0
- data/_includes/components/js-cdn.html +4 -1
- data/_includes/components/post-card.html +2 -11
- data/_includes/components/preview-image.html +32 -0
- data/_includes/content/intro.html +9 -10
- data/_includes/core/header.html +14 -0
- data/_includes/navigation/sidebar-categories.html +20 -9
- data/_includes/navigation/sidebar-folders.html +8 -7
- data/_includes/navigation/sidebar-right.html +16 -10
- data/_layouts/blog.html +15 -45
- data/_layouts/category.html +4 -24
- data/_layouts/collection.html +2 -12
- data/_layouts/default.html +1 -1
- data/_layouts/journals.html +2 -12
- data/_layouts/notebook.html +296 -0
- data/_sass/core/_docs.scss +1 -1
- data/_sass/custom.scss +54 -17
- data/_sass/notebooks.scss +458 -0
- data/assets/images/notebooks/test-notebook_files/test-notebook_4_0.png +0 -0
- data/assets/js/sidebar.js +511 -0
- data/scripts/README.md +131 -105
- data/scripts/analyze-commits.sh +9 -311
- data/scripts/bin/build +22 -22
- data/scripts/build +7 -111
- data/scripts/convert-notebooks.sh +415 -0
- data/scripts/features/validate_preview_urls.py +500 -0
- data/scripts/fix-markdown-format.sh +8 -262
- data/scripts/generate-preview-images.sh +7 -787
- data/scripts/install-preview-generator.sh +8 -528
- data/scripts/lib/README.md +5 -5
- data/scripts/lib/changelog.sh +89 -57
- data/scripts/lib/gem.sh +19 -7
- data/scripts/release +7 -236
- data/scripts/setup.sh +9 -153
- data/scripts/test/lib/run_tests.sh +1 -2
- data/scripts/test-auto-version.sh +7 -256
- data/scripts/test-mermaid.sh +7 -287
- data/scripts/test.sh +9 -154
- metadata +16 -10
- data/scripts/features/preview_generator.py +0 -646
- data/scripts/lib/test/run_tests.sh +0 -140
- data/scripts/lib/test/test_changelog.sh +0 -87
- data/scripts/lib/test/test_gem.sh +0 -68
- data/scripts/lib/test/test_git.sh +0 -82
- data/scripts/lib/test/test_validation.sh +0 -72
- data/scripts/lib/test/test_version.sh +0 -96
- data/scripts/version.sh +0 -178
|
@@ -5,10 +5,13 @@ require 'yaml'
|
|
|
5
5
|
require 'date'
|
|
6
6
|
require 'fileutils'
|
|
7
7
|
|
|
8
|
-
# Statistics Generator for Jekyll Site
|
|
8
|
+
# Enhanced Statistics Generator for Jekyll Site
|
|
9
9
|
# Analyzes site content and generates comprehensive statistics
|
|
10
|
+
# Features: focus areas, skill levels, authors, content types analysis
|
|
10
11
|
|
|
11
12
|
class SiteStatisticsGenerator
|
|
13
|
+
CONFIG_FILE = '_data/statistics_config.yml'
|
|
14
|
+
|
|
12
15
|
def initialize(site_root = '.')
|
|
13
16
|
@site_root = File.expand_path(site_root)
|
|
14
17
|
@posts_dir = File.join(@site_root, '_posts')
|
|
@@ -19,15 +22,63 @@ class SiteStatisticsGenerator
|
|
|
19
22
|
File.join(@site_root, '_projects')
|
|
20
23
|
]
|
|
21
24
|
@output_file = File.join(@site_root, '_data', 'content_statistics.yml')
|
|
25
|
+
@config_file = File.join(@site_root, CONFIG_FILE)
|
|
26
|
+
|
|
27
|
+
# Load configuration
|
|
28
|
+
@config = load_config
|
|
22
29
|
|
|
23
30
|
@stats = {
|
|
24
31
|
'generated_at' => Time.now.strftime('%Y-%m-%d %H:%M:%S'),
|
|
25
32
|
'overview' => {},
|
|
26
33
|
'categories' => {},
|
|
27
34
|
'tags' => {},
|
|
35
|
+
'authors' => {},
|
|
36
|
+
'years' => {},
|
|
28
37
|
'content_breakdown' => {},
|
|
29
38
|
'monthly_distribution' => {},
|
|
30
|
-
'word_statistics' => {}
|
|
39
|
+
'word_statistics' => {},
|
|
40
|
+
'focus_areas' => {},
|
|
41
|
+
'skill_levels' => {},
|
|
42
|
+
'content_types' => {},
|
|
43
|
+
'date_range' => {},
|
|
44
|
+
'drafts' => 0,
|
|
45
|
+
'published' => 0
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def load_config
|
|
50
|
+
if File.exist?(@config_file)
|
|
51
|
+
puts "📋 Loading configuration from #{CONFIG_FILE}..."
|
|
52
|
+
YAML.load_file(@config_file)
|
|
53
|
+
else
|
|
54
|
+
puts "⚠️ Configuration file not found: #{CONFIG_FILE}"
|
|
55
|
+
puts "📝 Using default configuration..."
|
|
56
|
+
default_config
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def default_config
|
|
61
|
+
{
|
|
62
|
+
'focus_areas' => {
|
|
63
|
+
'AI & Machine Learning' => ['ai', 'ml', 'machine-learning'],
|
|
64
|
+
'Web Development' => ['web', 'javascript', 'jekyll'],
|
|
65
|
+
'DevOps & Infrastructure' => ['devops', 'docker', 'ci/cd'],
|
|
66
|
+
'System Administration' => ['linux', 'windows', 'system'],
|
|
67
|
+
'Programming & Scripting' => ['programming', 'python', 'bash', 'ruby'],
|
|
68
|
+
'Data & Analytics' => ['data', 'database', 'analytics']
|
|
69
|
+
},
|
|
70
|
+
'skill_levels' => {
|
|
71
|
+
'beginner' => ['beginner', 'intro', 'basics', 'quickstart'],
|
|
72
|
+
'intermediate' => ['intermediate', 'practical', 'hands-on'],
|
|
73
|
+
'advanced' => ['advanced', 'expert', 'architecture'],
|
|
74
|
+
'expert' => ['expert', 'research', 'innovation']
|
|
75
|
+
},
|
|
76
|
+
'content_types' => {
|
|
77
|
+
'Tutorial' => ['tutorial', 'guide', 'how-to'],
|
|
78
|
+
'Article' => ['article', 'analysis', 'insights'],
|
|
79
|
+
'Journal Entry' => ['journal', 'learning-journey', 'post'],
|
|
80
|
+
'Documentation' => ['documentation', 'reference', 'docs']
|
|
81
|
+
}
|
|
31
82
|
}
|
|
32
83
|
end
|
|
33
84
|
|
|
@@ -38,6 +89,7 @@ class SiteStatisticsGenerator
|
|
|
38
89
|
analyze_pages
|
|
39
90
|
analyze_collections
|
|
40
91
|
calculate_overview_metrics
|
|
92
|
+
calculate_derived_stats
|
|
41
93
|
sort_and_finalize_data
|
|
42
94
|
write_statistics_file
|
|
43
95
|
|
|
@@ -123,7 +175,12 @@ class SiteStatisticsGenerator
|
|
|
123
175
|
end
|
|
124
176
|
|
|
125
177
|
# Skip drafts
|
|
126
|
-
|
|
178
|
+
if front_matter['draft'] == true
|
|
179
|
+
@stats['drafts'] += 1
|
|
180
|
+
return
|
|
181
|
+
else
|
|
182
|
+
@stats['published'] += 1
|
|
183
|
+
end
|
|
127
184
|
|
|
128
185
|
# Count this content
|
|
129
186
|
@stats['content_breakdown'][content_type] ||= 0
|
|
@@ -153,7 +210,10 @@ class SiteStatisticsGenerator
|
|
|
153
210
|
end
|
|
154
211
|
end
|
|
155
212
|
|
|
156
|
-
# Process
|
|
213
|
+
# Process authors
|
|
214
|
+
analyze_authors(front_matter['author'])
|
|
215
|
+
|
|
216
|
+
# Process dates for monthly distribution and year tracking
|
|
157
217
|
if front_matter['date']
|
|
158
218
|
begin
|
|
159
219
|
# Handle different date formats
|
|
@@ -169,12 +229,26 @@ class SiteStatisticsGenerator
|
|
|
169
229
|
month_key = date.strftime('%Y-%m')
|
|
170
230
|
@stats['monthly_distribution'][month_key] ||= 0
|
|
171
231
|
@stats['monthly_distribution'][month_key] += 1
|
|
232
|
+
|
|
233
|
+
# Track years
|
|
234
|
+
year = date.year.to_s
|
|
235
|
+
@stats['years'][year] ||= 0
|
|
236
|
+
@stats['years'][year] += 1
|
|
172
237
|
rescue => e
|
|
173
238
|
puts "⚠️ Date parsing error in #{file_path}: #{e.message} (date: #{front_matter['date']})"
|
|
174
239
|
# Skip this date
|
|
175
240
|
end
|
|
176
241
|
end
|
|
177
242
|
|
|
243
|
+
# Analyze skill levels (from tags, categories, and content)
|
|
244
|
+
analyze_skill_levels(front_matter)
|
|
245
|
+
|
|
246
|
+
# Analyze focus areas (from categories and tags)
|
|
247
|
+
analyze_focus_areas(front_matter)
|
|
248
|
+
|
|
249
|
+
# Analyze content types (from layout, categories, or inferred)
|
|
250
|
+
analyze_content_types(front_matter)
|
|
251
|
+
|
|
178
252
|
# Count words in content
|
|
179
253
|
word_count = count_words(body_content)
|
|
180
254
|
@stats['word_statistics'][File.basename(file_path)] = {
|
|
@@ -184,6 +258,86 @@ class SiteStatisticsGenerator
|
|
|
184
258
|
}
|
|
185
259
|
end
|
|
186
260
|
|
|
261
|
+
def analyze_authors(author)
|
|
262
|
+
return unless author.is_a?(String) && !author.strip.empty?
|
|
263
|
+
|
|
264
|
+
clean_author = author.strip
|
|
265
|
+
@stats['authors'][clean_author] ||= 0
|
|
266
|
+
@stats['authors'][clean_author] += 1
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def analyze_skill_levels(front_matter)
|
|
270
|
+
return unless @config['skill_levels']
|
|
271
|
+
|
|
272
|
+
all_text = [
|
|
273
|
+
front_matter['title'],
|
|
274
|
+
front_matter['description'],
|
|
275
|
+
front_matter['categories'],
|
|
276
|
+
front_matter['tags']
|
|
277
|
+
].flatten.compact.join(' ').downcase
|
|
278
|
+
|
|
279
|
+
@config['skill_levels'].each do |level, indicators|
|
|
280
|
+
if indicators.any? { |indicator| all_text.include?(indicator) }
|
|
281
|
+
@stats['skill_levels'][level] ||= 0
|
|
282
|
+
@stats['skill_levels'][level] += 1
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def analyze_focus_areas(front_matter)
|
|
288
|
+
return unless @config['focus_areas']
|
|
289
|
+
|
|
290
|
+
all_text = [
|
|
291
|
+
front_matter['title'],
|
|
292
|
+
front_matter['description'],
|
|
293
|
+
front_matter['categories'],
|
|
294
|
+
front_matter['tags']
|
|
295
|
+
].flatten.compact.join(' ').downcase
|
|
296
|
+
|
|
297
|
+
@config['focus_areas'].each do |area, keywords|
|
|
298
|
+
if keywords.any? { |keyword| all_text.include?(keyword) }
|
|
299
|
+
@stats['focus_areas'][area] ||= 0
|
|
300
|
+
@stats['focus_areas'][area] += 1
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def analyze_content_types(front_matter)
|
|
306
|
+
return unless @config['content_types']
|
|
307
|
+
|
|
308
|
+
# Check layout first
|
|
309
|
+
layout = front_matter['layout']
|
|
310
|
+
if layout
|
|
311
|
+
case layout.downcase
|
|
312
|
+
when 'journals', 'journal', 'blog'
|
|
313
|
+
increment_content_type('Journal Entry')
|
|
314
|
+
when 'tutorial', 'guide'
|
|
315
|
+
increment_content_type('Tutorial')
|
|
316
|
+
when 'article'
|
|
317
|
+
increment_content_type('Article')
|
|
318
|
+
when 'documentation', 'docs'
|
|
319
|
+
increment_content_type('Documentation')
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Check title and description
|
|
324
|
+
all_text = [
|
|
325
|
+
front_matter['title'],
|
|
326
|
+
front_matter['description']
|
|
327
|
+
].compact.join(' ').downcase
|
|
328
|
+
|
|
329
|
+
@config['content_types'].each do |type, keywords|
|
|
330
|
+
if keywords.any? { |keyword| all_text.include?(keyword) }
|
|
331
|
+
increment_content_type(type)
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def increment_content_type(type)
|
|
337
|
+
@stats['content_types'][type] ||= 0
|
|
338
|
+
@stats['content_types'][type] += 1
|
|
339
|
+
end
|
|
340
|
+
|
|
187
341
|
def count_words(content)
|
|
188
342
|
# Remove markdown syntax and count words
|
|
189
343
|
text = content.gsub(/[#*_`\[\](){}]/, ' ') # Remove markdown syntax
|
|
@@ -208,19 +362,48 @@ class SiteStatisticsGenerator
|
|
|
208
362
|
'total_content' => total_content,
|
|
209
363
|
'total_categories' => @stats['categories'].keys.length,
|
|
210
364
|
'total_tags' => @stats['tags'].keys.length,
|
|
365
|
+
'total_authors' => @stats['authors'].keys.length,
|
|
211
366
|
'total_words' => total_words,
|
|
212
|
-
'average_words_per_post' => total_content > 0 ? (total_words.to_f / total_content).round(1) : 0
|
|
367
|
+
'average_words_per_post' => total_content > 0 ? (total_words.to_f / total_content).round(1) : 0,
|
|
368
|
+
'published' => @stats['published'],
|
|
369
|
+
'drafts' => @stats['drafts']
|
|
213
370
|
}
|
|
214
371
|
end
|
|
215
372
|
|
|
373
|
+
def calculate_derived_stats
|
|
374
|
+
# Calculate date range
|
|
375
|
+
years = @stats['years'].keys.map(&:to_i).sort
|
|
376
|
+
if years.any?
|
|
377
|
+
@stats['date_range'] = {
|
|
378
|
+
'earliest' => years.first,
|
|
379
|
+
'latest' => years.last,
|
|
380
|
+
'span_years' => years.last - years.first + 1
|
|
381
|
+
}
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Calculate counts
|
|
385
|
+
@stats['category_count'] = @stats['categories'].length
|
|
386
|
+
@stats['tag_count'] = @stats['tags'].length
|
|
387
|
+
@stats['author_count'] = @stats['authors'].length
|
|
388
|
+
end
|
|
389
|
+
|
|
216
390
|
def sort_and_finalize_data
|
|
217
391
|
# Sort categories and tags by count (descending)
|
|
218
392
|
@stats['categories'] = @stats['categories'].sort_by { |_, count| -count }.to_h
|
|
219
393
|
@stats['tags'] = @stats['tags'].sort_by { |_, count| -count }.to_h
|
|
394
|
+
@stats['authors'] = @stats['authors'].sort_by { |_, count| -count }.to_h
|
|
395
|
+
@stats['focus_areas'] = @stats['focus_areas'].sort_by { |_, count| -count }.to_h
|
|
396
|
+
@stats['skill_levels'] = @stats['skill_levels'].sort_by { |_, count| -count }.to_h
|
|
397
|
+
@stats['content_types'] = @stats['content_types'].sort_by { |_, count| -count }.to_h
|
|
220
398
|
|
|
221
399
|
# Sort monthly distribution by date
|
|
222
400
|
@stats['monthly_distribution'] = @stats['monthly_distribution'].sort.to_h
|
|
223
401
|
|
|
402
|
+
# Create top lists
|
|
403
|
+
@stats['top_categories'] = @stats['categories'].first(5).to_h
|
|
404
|
+
@stats['top_tags'] = @stats['tags'].first(10).to_h
|
|
405
|
+
@stats['top_authors'] = @stats['authors'].first(5).to_h
|
|
406
|
+
|
|
224
407
|
# Convert to arrays for Jekyll (categories and tags)
|
|
225
408
|
@stats['categories'] = @stats['categories'].map { |name, count| [name, count] }
|
|
226
409
|
@stats['tags'] = @stats['tags'].map { |name, count| [name, count] }
|
|
@@ -242,25 +425,49 @@ class SiteStatisticsGenerator
|
|
|
242
425
|
puts "📝 Total Posts: #{@stats['overview']['total_posts']}"
|
|
243
426
|
puts "📄 Total Pages: #{@stats['overview']['total_pages']}"
|
|
244
427
|
puts "📚 Total Content: #{@stats['overview']['total_content']}"
|
|
428
|
+
puts "✅ Published: #{@stats['overview']['published']}"
|
|
429
|
+
puts "📝 Drafts: #{@stats['overview']['drafts']}"
|
|
245
430
|
puts "📂 Categories: #{@stats['overview']['total_categories']}"
|
|
246
431
|
puts "🏷️ Tags: #{@stats['overview']['total_tags']}"
|
|
432
|
+
puts "👥 Authors: #{@stats['overview']['total_authors']}"
|
|
247
433
|
puts "📝 Total Words: #{@stats['overview']['total_words'].to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}"
|
|
248
434
|
puts "📊 Average Words/Post: #{@stats['overview']['average_words_per_post']}"
|
|
249
435
|
|
|
250
|
-
if @stats['
|
|
436
|
+
if @stats['date_range'].any?
|
|
437
|
+
puts "\n📅 DATE RANGE:"
|
|
438
|
+
puts " Earliest: #{@stats['date_range']['earliest']}"
|
|
439
|
+
puts " Latest: #{@stats['date_range']['latest']}"
|
|
440
|
+
puts " Span: #{@stats['date_range']['span_years']} year(s)"
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
if @stats['top_categories'].any?
|
|
251
444
|
puts "\n🏆 TOP CATEGORIES:"
|
|
252
|
-
@stats['
|
|
445
|
+
@stats['top_categories'].each_with_index do |(name, count), index|
|
|
253
446
|
puts " #{index + 1}. #{name}: #{count} posts"
|
|
254
447
|
end
|
|
255
448
|
end
|
|
256
449
|
|
|
257
|
-
if @stats['
|
|
450
|
+
if @stats['top_tags'].any?
|
|
258
451
|
puts "\n🏷️ TOP TAGS:"
|
|
259
|
-
@stats['
|
|
452
|
+
@stats['top_tags'].first(5).each_with_index do |(name, count), index|
|
|
260
453
|
puts " #{index + 1}. #{name}: #{count} uses"
|
|
261
454
|
end
|
|
262
455
|
end
|
|
263
456
|
|
|
457
|
+
if @stats['focus_areas'].any?
|
|
458
|
+
puts "\n🎯 FOCUS AREAS:"
|
|
459
|
+
@stats['focus_areas'].first(5).each_with_index do |(area, count), index|
|
|
460
|
+
puts " #{index + 1}. #{area}: #{count} posts"
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
if @stats['skill_levels'].any?
|
|
465
|
+
puts "\n📈 SKILL LEVELS:"
|
|
466
|
+
@stats['skill_levels'].each do |level, count|
|
|
467
|
+
puts " #{level.capitalize}: #{count} posts"
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
264
471
|
puts "\n✅ Statistics generation complete!"
|
|
265
472
|
end
|
|
266
473
|
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Generate Content Statistics Script
|
|
4
|
+
# Used by: Manual execution or CI/CD workflows
|
|
5
|
+
# Purpose: Run the Ruby statistics generator with proper feedback
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
# Colors for output
|
|
10
|
+
RED='\033[0;31m'
|
|
11
|
+
GREEN='\033[0;32m'
|
|
12
|
+
YELLOW='\033[1;33m'
|
|
13
|
+
BLUE='\033[0;34m'
|
|
14
|
+
NC='\033[0m' # No Color
|
|
15
|
+
|
|
16
|
+
# Script directory
|
|
17
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
18
|
+
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
19
|
+
RUBY_SCRIPT="$SCRIPT_DIR/generate_statistics.rb"
|
|
20
|
+
OUTPUT_FILE="$SCRIPT_DIR/content_statistics.yml"
|
|
21
|
+
CONFIG_FILE="$SCRIPT_DIR/statistics_config.yml"
|
|
22
|
+
|
|
23
|
+
echo -e "${BLUE}📊 Zer0-Mistakes Content Statistics Generator${NC}"
|
|
24
|
+
echo "=================================================="
|
|
25
|
+
|
|
26
|
+
# Check if Ruby is available
|
|
27
|
+
if ! command -v ruby &> /dev/null; then
|
|
28
|
+
echo -e "${RED}❌ Error: Ruby is not installed or not in PATH${NC}"
|
|
29
|
+
echo "Please install Ruby to run the statistics generator."
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
echo -e "${GREEN}✅ Ruby found: $(ruby --version)${NC}"
|
|
34
|
+
|
|
35
|
+
# Check if the Ruby script exists
|
|
36
|
+
if [[ ! -f "$RUBY_SCRIPT" ]]; then
|
|
37
|
+
echo -e "${RED}❌ Error: Ruby script not found at $RUBY_SCRIPT${NC}"
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
echo -e "${GREEN}✅ Statistics generator script found${NC}"
|
|
42
|
+
|
|
43
|
+
# Check if config file exists
|
|
44
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
45
|
+
echo -e "${GREEN}✅ Configuration file found: statistics_config.yml${NC}"
|
|
46
|
+
else
|
|
47
|
+
echo -e "${YELLOW}⚠️ Configuration file not found, using defaults${NC}"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Check for content directories
|
|
51
|
+
POSTS_DIR="$PROJECT_ROOT/pages/_posts"
|
|
52
|
+
if [[ -d "$POSTS_DIR" ]]; then
|
|
53
|
+
POST_COUNT=$(find "$POSTS_DIR" -name "*.md" -type f | wc -l | tr -d ' ')
|
|
54
|
+
echo -e "${GREEN}✅ Found $POST_COUNT posts in pages/_posts${NC}"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
PAGES_DIR="$PROJECT_ROOT/pages"
|
|
58
|
+
if [[ -d "$PAGES_DIR" ]]; then
|
|
59
|
+
PAGE_COUNT=$(find "$PAGES_DIR" -name "*.md" -type f ! -path "*/_posts/*" | wc -l | tr -d ' ')
|
|
60
|
+
echo -e "${GREEN}✅ Found $PAGE_COUNT pages in pages/${NC}"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Run the Ruby script from project root
|
|
64
|
+
echo -e "${YELLOW}🔄 Generating statistics...${NC}"
|
|
65
|
+
cd "$PROJECT_ROOT"
|
|
66
|
+
if ruby "$RUBY_SCRIPT"; then
|
|
67
|
+
echo -e "${GREEN}✅ Statistics generated successfully!${NC}"
|
|
68
|
+
else
|
|
69
|
+
echo -e "${RED}❌ Error: Failed to generate statistics${NC}"
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Check if output file was created
|
|
74
|
+
if [[ -f "$OUTPUT_FILE" ]]; then
|
|
75
|
+
FILE_SIZE=$(du -h "$OUTPUT_FILE" | cut -f1)
|
|
76
|
+
echo -e "${GREEN}✅ Output file created: $OUTPUT_FILE ($FILE_SIZE)${NC}"
|
|
77
|
+
|
|
78
|
+
# Show a preview of the generated statistics
|
|
79
|
+
echo -e "${BLUE}📋 Statistics Preview:${NC}"
|
|
80
|
+
echo "------------------------"
|
|
81
|
+
if command -v yq &> /dev/null; then
|
|
82
|
+
echo "Generated at: $(yq '.generated_at' "$OUTPUT_FILE")"
|
|
83
|
+
echo "Total Posts: $(yq '.overview.total_posts' "$OUTPUT_FILE")"
|
|
84
|
+
echo "Total Pages: $(yq '.overview.total_pages' "$OUTPUT_FILE")"
|
|
85
|
+
echo "Published: $(yq '.overview.published' "$OUTPUT_FILE")"
|
|
86
|
+
echo "Categories: $(yq '.overview.total_categories' "$OUTPUT_FILE")"
|
|
87
|
+
echo "Tags: $(yq '.overview.total_tags' "$OUTPUT_FILE")"
|
|
88
|
+
else
|
|
89
|
+
# Fallback to head if yq is not available
|
|
90
|
+
echo "Preview of generated data:"
|
|
91
|
+
head -25 "$OUTPUT_FILE"
|
|
92
|
+
fi
|
|
93
|
+
else
|
|
94
|
+
echo -e "${RED}❌ Error: Output file was not created${NC}"
|
|
95
|
+
exit 1
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
echo ""
|
|
99
|
+
echo "=========================="
|
|
100
|
+
echo -e "${GREEN}🎉 Content statistics generation complete!${NC}"
|
|
101
|
+
echo ""
|
|
102
|
+
echo "📝 The data is now available in _data/content_statistics.yml"
|
|
103
|
+
echo "📊 Use the stats layout for display: layout: stats"
|
|
104
|
+
echo ""
|
|
105
|
+
echo "🔄 To regenerate statistics, run this script again:"
|
|
106
|
+
echo " bash _data/generate_statistics.sh"
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Example GitHub Actions workflow for automated content statistics
|
|
2
|
+
# Save this as .github/workflows/update-statistics.yml
|
|
3
|
+
#
|
|
4
|
+
# This workflow automatically updates content_statistics.yml when:
|
|
5
|
+
# - Posts or documentation content changes
|
|
6
|
+
# - The statistics configuration is updated
|
|
7
|
+
# - Manually triggered via workflow_dispatch
|
|
8
|
+
# - Weekly scheduled run for maintenance
|
|
9
|
+
#
|
|
10
|
+
# For the Zer0-Mistakes Jekyll theme
|
|
11
|
+
|
|
12
|
+
name: Update Content Statistics
|
|
13
|
+
|
|
14
|
+
on:
|
|
15
|
+
# Run when content changes
|
|
16
|
+
push:
|
|
17
|
+
paths:
|
|
18
|
+
- 'pages/_posts/**'
|
|
19
|
+
- 'pages/_docs/**'
|
|
20
|
+
- 'pages/_quickstart/**'
|
|
21
|
+
- '_data/statistics_config.yml'
|
|
22
|
+
branches: [main]
|
|
23
|
+
|
|
24
|
+
# Allow manual trigger with options
|
|
25
|
+
workflow_dispatch:
|
|
26
|
+
inputs:
|
|
27
|
+
force_update:
|
|
28
|
+
description: 'Force update even if no changes detected'
|
|
29
|
+
required: false
|
|
30
|
+
default: 'false'
|
|
31
|
+
type: boolean
|
|
32
|
+
skip_push:
|
|
33
|
+
description: 'Generate stats but do not commit/push'
|
|
34
|
+
required: false
|
|
35
|
+
default: 'false'
|
|
36
|
+
type: boolean
|
|
37
|
+
|
|
38
|
+
# Run weekly to catch any missed updates
|
|
39
|
+
schedule:
|
|
40
|
+
- cron: '0 6 * * 1' # Every Monday at 6 AM UTC
|
|
41
|
+
|
|
42
|
+
env:
|
|
43
|
+
RUBY_VERSION: '3.2'
|
|
44
|
+
YQ_VERSION: 'v4.40.5'
|
|
45
|
+
|
|
46
|
+
jobs:
|
|
47
|
+
update-statistics:
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
|
|
50
|
+
permissions:
|
|
51
|
+
contents: write # Allow the workflow to commit changes
|
|
52
|
+
|
|
53
|
+
steps:
|
|
54
|
+
- name: Checkout repository
|
|
55
|
+
uses: actions/checkout@v4
|
|
56
|
+
with:
|
|
57
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
58
|
+
fetch-depth: 0
|
|
59
|
+
|
|
60
|
+
- name: Setup Ruby
|
|
61
|
+
uses: ruby/setup-ruby@v1
|
|
62
|
+
with:
|
|
63
|
+
ruby-version: ${{ env.RUBY_VERSION }}
|
|
64
|
+
bundler-cache: true
|
|
65
|
+
|
|
66
|
+
- name: Install yq (for YAML processing)
|
|
67
|
+
run: |
|
|
68
|
+
sudo wget -qO /usr/local/bin/yq \
|
|
69
|
+
https://github.com/mikefarah/yq/releases/download/${{ env.YQ_VERSION }}/yq_linux_amd64
|
|
70
|
+
sudo chmod +x /usr/local/bin/yq
|
|
71
|
+
yq --version
|
|
72
|
+
|
|
73
|
+
- name: Verify required files exist
|
|
74
|
+
run: |
|
|
75
|
+
echo "🔍 Checking required files..."
|
|
76
|
+
|
|
77
|
+
if [ ! -f "_data/generate_statistics.rb" ]; then
|
|
78
|
+
echo "❌ Missing: _data/generate_statistics.rb"
|
|
79
|
+
exit 1
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
if [ ! -f "_data/update_statistics.sh" ]; then
|
|
83
|
+
echo "❌ Missing: _data/update_statistics.sh"
|
|
84
|
+
exit 1
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
echo "✅ All required files present"
|
|
88
|
+
|
|
89
|
+
- name: Update content statistics
|
|
90
|
+
env:
|
|
91
|
+
CI: true
|
|
92
|
+
AUTO_PUSH: ${{ github.event.inputs.skip_push != 'true' }}
|
|
93
|
+
FORCE_UPDATE: ${{ github.event.inputs.force_update == 'true' }}
|
|
94
|
+
run: |
|
|
95
|
+
# Make scripts executable
|
|
96
|
+
chmod +x _data/update_statistics.sh
|
|
97
|
+
chmod +x _data/generate_statistics.sh 2>/dev/null || true
|
|
98
|
+
|
|
99
|
+
# Run the update script
|
|
100
|
+
bash _data/update_statistics.sh
|
|
101
|
+
|
|
102
|
+
- name: Display statistics summary
|
|
103
|
+
if: always()
|
|
104
|
+
run: |
|
|
105
|
+
if [ -f "_data/content_statistics.yml" ]; then
|
|
106
|
+
echo ""
|
|
107
|
+
echo "📊 Content Statistics Summary"
|
|
108
|
+
echo "════════════════════════════════════════════════════"
|
|
109
|
+
echo ""
|
|
110
|
+
|
|
111
|
+
# Basic counts
|
|
112
|
+
echo "📝 Content Overview:"
|
|
113
|
+
echo " Total Posts: $(yq '.total_posts // 0' _data/content_statistics.yml)"
|
|
114
|
+
echo " Published: $(yq '.published // 0' _data/content_statistics.yml)"
|
|
115
|
+
echo " Drafts: $(yq '.drafts // 0' _data/content_statistics.yml)"
|
|
116
|
+
echo " Categories: $(yq '.category_count // 0' _data/content_statistics.yml)"
|
|
117
|
+
echo " Tags: $(yq '.tag_count // 0' _data/content_statistics.yml)"
|
|
118
|
+
echo ""
|
|
119
|
+
|
|
120
|
+
# Focus areas if present
|
|
121
|
+
if yq -e '.focus_areas' _data/content_statistics.yml > /dev/null 2>&1; then
|
|
122
|
+
echo "🎯 Top Focus Areas:"
|
|
123
|
+
yq '.focus_areas | to_entries | sort_by(-.value) | .[0:5] | .[] | " • " + .key + ": " + (.value | tostring)' \
|
|
124
|
+
_data/content_statistics.yml 2>/dev/null || echo " (none)"
|
|
125
|
+
echo ""
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# Skill levels if present
|
|
129
|
+
if yq -e '.skill_levels' _data/content_statistics.yml > /dev/null 2>&1; then
|
|
130
|
+
echo "📈 Skill Level Distribution:"
|
|
131
|
+
yq '.skill_levels | to_entries | .[] | " • " + .key + ": " + (.value | tostring)' \
|
|
132
|
+
_data/content_statistics.yml 2>/dev/null || echo " (none)"
|
|
133
|
+
echo ""
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# Authors if present
|
|
137
|
+
if yq -e '.authors' _data/content_statistics.yml > /dev/null 2>&1; then
|
|
138
|
+
echo "✍️ Top Authors:"
|
|
139
|
+
yq '.authors | to_entries | sort_by(-.value) | .[0:3] | .[] | " • " + .key + ": " + (.value | tostring) + " posts"' \
|
|
140
|
+
_data/content_statistics.yml 2>/dev/null || echo " (none)"
|
|
141
|
+
echo ""
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
# Content freshness
|
|
145
|
+
if yq -e '.content_freshness' _data/content_statistics.yml > /dev/null 2>&1; then
|
|
146
|
+
echo "🕐 Content Freshness:"
|
|
147
|
+
yq '.content_freshness | to_entries | .[] | " • " + .key + ": " + (.value | tostring)' \
|
|
148
|
+
_data/content_statistics.yml 2>/dev/null || echo " (none)"
|
|
149
|
+
echo ""
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# Derived stats
|
|
153
|
+
if yq -e '.derived_stats' _data/content_statistics.yml > /dev/null 2>&1; then
|
|
154
|
+
echo "📊 Derived Statistics:"
|
|
155
|
+
AVG_TAGS=$(yq '.derived_stats.avg_tags_per_post // "N/A"' _data/content_statistics.yml)
|
|
156
|
+
AVG_CATS=$(yq '.derived_stats.avg_categories_per_post // "N/A"' _data/content_statistics.yml)
|
|
157
|
+
TOP_CAT=$(yq '.derived_stats.most_common_category // "N/A"' _data/content_statistics.yml)
|
|
158
|
+
echo " • Avg tags/post: ${AVG_TAGS}"
|
|
159
|
+
echo " • Avg categories/post: ${AVG_CATS}"
|
|
160
|
+
echo " • Most common category: ${TOP_CAT}"
|
|
161
|
+
echo ""
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
# Last updated
|
|
165
|
+
echo "🕒 Last Updated: $(yq '.generated_at // "unknown"' _data/content_statistics.yml)"
|
|
166
|
+
echo ""
|
|
167
|
+
echo "════════════════════════════════════════════════════"
|
|
168
|
+
else
|
|
169
|
+
echo "⚠️ Statistics file not found: _data/content_statistics.yml"
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
- name: Check for changes
|
|
173
|
+
id: changes
|
|
174
|
+
run: |
|
|
175
|
+
if git diff --quiet _data/content_statistics.yml; then
|
|
176
|
+
echo "has_changes=false" >> $GITHUB_OUTPUT
|
|
177
|
+
else
|
|
178
|
+
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
- name: Commit and push changes
|
|
182
|
+
if: steps.changes.outputs.has_changes == 'true' && github.event.inputs.skip_push != 'true'
|
|
183
|
+
run: |
|
|
184
|
+
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
|
185
|
+
git config --local user.name "github-actions[bot]"
|
|
186
|
+
|
|
187
|
+
git add _data/content_statistics.yml
|
|
188
|
+
git commit -m "chore(stats): update content statistics [skip ci]"
|
|
189
|
+
git push
|
|
190
|
+
|
|
191
|
+
echo "✅ Statistics updated and pushed to repository"
|
|
192
|
+
|
|
193
|
+
- name: Job summary
|
|
194
|
+
run: |
|
|
195
|
+
echo "## 📊 Content Statistics Update" >> $GITHUB_STEP_SUMMARY
|
|
196
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
197
|
+
|
|
198
|
+
if [ -f "_data/content_statistics.yml" ]; then
|
|
199
|
+
echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY
|
|
200
|
+
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
|
|
201
|
+
echo "| Total Posts | $(yq '.total_posts // 0' _data/content_statistics.yml) |" >> $GITHUB_STEP_SUMMARY
|
|
202
|
+
echo "| Published | $(yq '.published // 0' _data/content_statistics.yml) |" >> $GITHUB_STEP_SUMMARY
|
|
203
|
+
echo "| Drafts | $(yq '.drafts // 0' _data/content_statistics.yml) |" >> $GITHUB_STEP_SUMMARY
|
|
204
|
+
echo "| Categories | $(yq '.category_count // 0' _data/content_statistics.yml) |" >> $GITHUB_STEP_SUMMARY
|
|
205
|
+
echo "| Tags | $(yq '.tag_count // 0' _data/content_statistics.yml) |" >> $GITHUB_STEP_SUMMARY
|
|
206
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
207
|
+
echo "_Last updated: $(yq '.generated_at // "unknown"' _data/content_statistics.yml)_" >> $GITHUB_STEP_SUMMARY
|
|
208
|
+
else
|
|
209
|
+
echo "⚠️ Statistics file not generated" >> $GITHUB_STEP_SUMMARY
|
|
210
|
+
fi
|