Blog Stats
I’ve had some blog stats available on my About page for a while, but I wanted to see if I could make my script a little more robust. For one, I wanted more stats, but I also wanted graphs.
What I had before was a simple bash script that would post the total number of posts and the total word count. Every time I would publish a post, Hugo itself would run the script and output to a shortcode. It was janky and didn’t really count things accurately.
Now, I have a more complicated script and I’ve moved the processing of the script to Gitlab’s CI/CD. Everytime the repo is updated, the bash script runs, and Hugo will read from the output and put that in the shortcode.
The updated script counts using awk and a bunch of regex (to filter out front matter), and then outputs some stats:
- Total blog posts
- Total image posts
- Total link posts
- Total word count
- Word count per month and year
- Posts per month
- And graphs to go along with these stats
Here’s what it looks like if you don’t want to go to About to see it in action:
📊 Blog Statistics Report
════════════════════════════════════════════════════════════════════════════════
Word Counts
────────────────────────────────────────────────────────────────────────────────
Total words: 85348
Post Type Breakdown
────────────────────────────────────────────────────────────────────────────────
Regular posts: 161
Link posts: 4
Image posts: 3
Total posts: 168
Time-Based Statistics
────────────────────────────────────────────────────────────────────────────────
Blogging since: 10-2024
Latest post: 10-2025
Active months: 13
Average posts per month: 12.92
Average words per month: 6565
Posts per Month (Last 12 Months)
────────────────────────────────────────────────────────────────────────────────
10-2024 | 2 ███
12-2024 | 2 ███
01-2025 | 5 ███████
03-2025 | 1 █
04-2025 | 13 ███████████████████
05-2025 | 26 ███████████████████████████████████████
06-2025 | 23 ██████████████████████████████████
07-2025 | 18 ███████████████████████████
08-2025 | 33 ██████████████████████████████████████████████████
09-2025 | 20 ██████████████████████████████
10-2025 | 25 █████████████████████████████████████
Words per Month (Last 12 Months)
────────────────────────────────────────────────────────────────────────────────
10-2024 | 2139 ███████
12-2024 | 2140 ███████
01-2025 | 3705 ████████████
03-2025 | 444 █
04-2025 | 8696 ██████████████████████████████
05-2025 | 14200 █████████████████████████████████████████████████
06-2025 | 12233 ██████████████████████████████████████████
07-2025 | 9585 █████████████████████████████████
08-2025 | 14341 ██████████████████████████████████████████████████
09-2025 | 8426 █████████████████████████████
10-2025 | 9439 ████████████████████████████████
Posts per Year
────────────────────────────────────────────────────────────────────────────────
2024 | 4 █
2025 | 164 ██████████████████████████████████████████████████
Words per Year
────────────────────────────────────────────────────────────────────────────────
2024 | 4279 ██
2025 | 81069 ██████████████████████████████████████████████████
════════════════════════════════════════════════════════════════════════════════
I think I finally have the script counting things accurately. The regex is always hard for me so I had Claude help me out there, so we’ll see how that works out.
I really like this idea. Having some stats of how I’ve done this year in terms of blogging keeps me motivated. I’m on my way to 200 posts this year. If I keep blogging every day, I should get there, even if I cheat sometimes and post link or image posts. That’s a pretty good year blogging for someone who wrote 4 posts all of last year.
I have some more ideas for my stats page. I want to add in some color, and if I can add in some tag statistics (I bet a lot of my posts have the ‘blogging’ tag), that would be cool.
My script for doing this is here.
EDIT: Since that link seems to not work for some reason, here’s the full script:
#!/bin/bash
# Enhanced Word Count Script for Hugo Blog
# Analyzes markdown files with statistics, graphs, and post type breakdowns
# Get the directory where the script is located and change to it
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR" || exit 1
# Detect if output is a terminal or being piped/redirected
if [ -t 1 ]; then
# Color codes for terminal output
BOLD='\033[1m'
RESET='\033[0m'
DIM='\033[2m'
else
# No colors for piped/redirected output
BOLD=''
RESET=''
DIM=''
fi
echo -e "${BOLD}📊 Blog Statistics Report${RESET}"
echo "════════════════════════════════════════════════════════════════════════════════"
echo
# ============================================================================
# BASIC WORD COUNTS
# ============================================================================
echo -e "${BOLD}Word Counts${RESET}"
echo "────────────────────────────────────────────────────────────────────────────────"
# Count words excluding front matter (TOML +++ and YAML ---)
# Start with p=1 (print mode), toggle to p=0 (skip mode) when entering front matter
content_words=$(find ./posts ./links ./images -type f -name "*.md" 2>/dev/null -exec awk 'FNR==1{p=1} /^---$/{p=1-p; next} /^\+\+\+$/{p=1-p; next} p' {} + | wc -w)
[ -z "$content_words" ] && content_words=0
printf "Total words: %'d\n" $content_words
echo
# ============================================================================
# POST TYPE STATISTICS
# ============================================================================
echo -e "${BOLD}Post Type Breakdown${RESET}"
echo "────────────────────────────────────────────────────────────────────────────────"
# Count different post types
regular_posts=$(find ./posts -type f -name "*.md" 2>/dev/null | wc -l)
link_posts=$(find ./links -type f -name "*.md" 2>/dev/null | wc -l)
image_posts=$(find ./images -type f -name "*.md" 2>/dev/null | wc -l)
total_posts=$((regular_posts + link_posts + image_posts))
echo "Regular posts: $regular_posts"
echo "Link posts: $link_posts"
echo "Image posts: $image_posts"
echo "Total posts: $total_posts"
echo
# ============================================================================
# TIME-BASED STATISTICS
# ============================================================================
echo -e "${BOLD}Time-Based Statistics${RESET}"
echo "────────────────────────────────────────────────────────────────────────────────"
# Extract dates and analyze by month/year
declare -A posts_by_month
declare -A posts_by_year
declare -A words_by_month
declare -A words_by_year
posts_with_dates=0
posts_without_dates=0
words_with_dates=0
words_without_dates=0
# Process all markdown files for dates
while IFS= read -r file; do
# Extract date from front matter (supports both TOML and YAML)
date=$(grep -E "^date\s*[=:]" "$file" | head -1 | sed -E "s/date\s*[=:]\s*['\"]?([0-9]{4}-[0-9]{2}-[0-9]{2}).*/\1/")
# Count words for this file (start with p=1 to print content, toggle to 0 in front matter)
file_words=$(awk 'FNR==1{p=1} /^---$/{p=1-p; next} /^\+\+\+$/{p=1-p; next} p' "$file" | wc -w)
if [[ $date =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
year="${date:0:4}" # YYYY
month="${date:5:2}" # MM
year_month="${month}-${year}" # MM-YYYY
# Count posts by month
((posts_by_month[$year_month]++))
# Count posts by year
((posts_by_year[$year]++))
# Count words by month (excluding front matter)
words_by_month[$year_month]=$((${words_by_month[$year_month]:-0} + file_words))
# Count words by year
words_by_year[$year]=$((${words_by_year[$year]:-0} + file_words))
((posts_with_dates++))
words_with_dates=$((words_with_dates + file_words))
else
((posts_without_dates++))
words_without_dates=$((words_without_dates + file_words))
fi
done < <(find ./posts ./links ./images -type f -name "*.md" 2>/dev/null)
# Calculate time span
if [ ${#posts_by_month[@]} -gt 0 ]; then
# Sort MM-YYYY by converting to YYYY-MM for comparison
first_month=$(printf '%s\n' "${!posts_by_month[@]}" | awk -F'-' '{print $2"-"$1}' | sort | head -1 | awk -F'-' '{print $2"-"$1}')
last_month=$(printf '%s\n' "${!posts_by_month[@]}" | awk -F'-' '{print $2"-"$1}' | sort | tail -1 | awk -F'-' '{print $2"-"$1}')
# Calculate months between first and last post
first_mon=${first_month:0:2}
first_year=${first_month:3:4}
last_mon=${last_month:0:2}
last_year=${last_month:3:4}
months_total=$(( (last_year - first_year) * 12 + 10#$last_mon - 10#$first_mon + 1 ))
# Calculate averages (using bash arithmetic for compatibility)
avg_posts_month_int=$((total_posts / months_total))
avg_posts_month_dec=$(( (total_posts * 100 / months_total) % 100 ))
avg_words_month=$((content_words / months_total))
echo "Blogging since: $first_month"
echo "Latest post: $last_month"
echo "Active months: $months_total"
printf "Average posts per month: %d.%02d\n" $avg_posts_month_int $avg_posts_month_dec
printf "Average words per month: %'d\n" $avg_words_month
fi
echo
# ============================================================================
# POSTS PER MONTH GRAPH
# ============================================================================
echo -e "${BOLD}Posts per Month (Last 12 Months)${RESET}"
echo "────────────────────────────────────────────────────────────────────────────────"
# Get last 12 months and display as bar chart (sort by YYYY-MM, then convert back to MM-YYYY)
sorted_months=($(printf '%s\n' "${!posts_by_month[@]}" | awk -F'-' '{print $2"-"$1}' | sort -r | head -12 | sort | awk -F'-' '{print $2"-"$1}'))
if [ ${#sorted_months[@]} -gt 0 ]; then
max_posts=0
for month in "${sorted_months[@]}"; do
count=${posts_by_month[$month]}
((count > max_posts)) && max_posts=$count
done
# Display bar chart
for month in "${sorted_months[@]}"; do
count=${posts_by_month[$month]}
bar_length=$((count * 50 / max_posts))
[ $bar_length -eq 0 ] && [ $count -gt 0 ] && bar_length=1
printf "%s | %3d " "$month" "$count"
printf '█%.0s' $(seq 1 $bar_length)
echo
done
else
echo "No date information found in posts"
fi
echo
# ============================================================================
# WORDS PER MONTH GRAPH
# ============================================================================
echo -e "${BOLD}Words per Month (Last 12 Months)${RESET}"
echo "────────────────────────────────────────────────────────────────────────────────"
if [ ${#sorted_months[@]} -gt 0 ]; then
max_words=0
for month in "${sorted_months[@]}"; do
count=${words_by_month[$month]:-0}
((count > max_words)) && max_words=$count
done
# Display bar chart
for month in "${sorted_months[@]}"; do
count=${words_by_month[$month]:-0}
bar_length=$((count * 50 / max_words))
[ $bar_length -eq 0 ] && [ $count -gt 0 ] && bar_length=1
printf "%s | %5d " "$month" "$count"
printf '█%.0s' $(seq 1 $bar_length)
echo
done
fi
echo
# ============================================================================
# POSTS PER YEAR GRAPH
# ============================================================================
echo -e "${BOLD}Posts per Year${RESET}"
echo "────────────────────────────────────────────────────────────────────────────────"
sorted_years=($(printf '%s\n' "${!posts_by_year[@]}" | sort))
if [ ${#sorted_years[@]} -gt 0 ]; then
max_posts_year=0
for year in "${sorted_years[@]}"; do
count=${posts_by_year[$year]}
((count > max_posts_year)) && max_posts_year=$count
done
# Display bar chart
for year in "${sorted_years[@]}"; do
count=${posts_by_year[$year]}
bar_length=$((count * 50 / max_posts_year))
[ $bar_length -eq 0 ] && [ $count -gt 0 ] && bar_length=1
printf "%s | %3d " "$year" "$count"
printf '█%.0s' $(seq 1 $bar_length)
echo
done
fi
echo
# ============================================================================
# WORDS PER YEAR GRAPH
# ============================================================================
echo -e "${BOLD}Words per Year${RESET}"
echo "────────────────────────────────────────────────────────────────────────────────"
if [ ${#sorted_years[@]} -gt 0 ]; then
max_words_year=0
for year in "${sorted_years[@]}"; do
count=${words_by_year[$year]:-0}
((count > max_words_year)) && max_words_year=$count
done
# Display bar chart
for year in "${sorted_years[@]}"; do
count=${words_by_year[$year]:-0}
bar_length=$((count * 50 / max_words_year))
[ $bar_length -eq 0 ] && [ $count -gt 0 ] && bar_length=1
printf "%s | %5d " "$year" "$count"
printf '█%.0s' $(seq 1 $bar_length)
echo
done
fi
echo
echo "════════════════════════════════════════════════════════════════════════════════"