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 "════════════════════════════════════════════════════════════════════════════════"