website-htmx-rustelo-code/scripts/sh/content/validate-content.sh

374 lines
12 KiB
Bash
Raw Normal View History

2026-07-10 03:44:13 +01:00
#!/bin/bash
# Content validation script for all localized content types
# Validates JSON structure, required fields, and content integrity
set -e
# Set bash mode for associative arrays
if [ -n "$BASH_VERSION" ]; then
set +h # Disable hash table to allow associative arrays
fi
# Configuration
CONTENT_DIR="content"
SUPPORTED_LANGUAGES=("en" "es")
# Dynamically discover content types from content-kinds.toml
get_content_types() {
if [ -f "content/content-kinds.toml" ]; then
# Extract content type names from TOML file
grep '^\[' content/content-kinds.toml | sed 's/\[//g' | sed 's/\]//g' | sort -u
else
# Fallback: discover from directory structure
find content -maxdepth 1 -type d -not -name "locales" -not -name "themes" -not -name "menus" -not -name "footer" -not -name "tmp" -printf '%f\n' | sort
fi
}
# Get content types dynamically
mapfile -t CONTENT_TYPES < <(get_content_types)
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Counters
TOTAL_FILES=0
VALID_FILES=0
ERROR_COUNT=0
WARNING_COUNT=0
echo -e "${BLUE}🔍 Content Validation Report${NC}"
echo "==============================="
# Function to log errors
log_error() {
echo -e "${RED}❌ ERROR: $1${NC}"
((ERROR_COUNT++))
}
# Function to log warnings
log_warning() {
echo -e "${YELLOW}⚠️ WARNING: $1${NC}"
((WARNING_COUNT++))
}
# Function to log success
log_success() {
echo -e "${GREEN}$1${NC}"
}
# Function to log info
log_info() {
echo -e "${BLUE} $1${NC}"
}
# Check if jq is available
check_dependencies() {
if ! command -v jq >/dev/null 2>&1; then
log_error "jq is required but not installed. Please install jq to continue."
exit 1
fi
log_success "Dependencies check passed"
}
# Validate JSON syntax
validate_json_syntax() {
local file="$1"
if ! jq empty "$file" 2>/dev/null; then
log_error "Invalid JSON syntax in $file"
return 1
fi
return 0
}
# Generic validate content item structure
validate_content_item() {
local file="$1"
local content_type="$2"
local index="$3"
# Determine content array key based on content type (blog -> blog_posts, recipes -> recipes)
local array_key="${content_type}"
if [[ "$content_type" == "blog" ]]; then
array_key="blog"
fi
local required_fields=("id" "title" "excerpt" "tags" "featured" "language" "source_file" "html_file" "translations")
# Add content-type specific required fields
if [[ "$content_type" == "blog" ]]; then
required_fields+=("author" "date" "read_time" "published")
elif [[ "$content_type" == "recipes" ]]; then
required_fields+=("category" "difficulty" "prep_time")
fi
for field in "${required_fields[@]}"; do
if ! jq -e ".${array_key}[$index] | has(\"$field\")" "$file" >/dev/null 2>&1; then
log_error "Missing required field '$field' in $content_type item at index $index in $file"
return 1
fi
done
# Validate specific field types and constraints
local id=$(jq -r ".${array_key}[$index].id" "$file")
local title=$(jq -r ".${array_key}[$index].title" "$file")
local language=$(jq -r ".${array_key}[$index].language" "$file")
local tags=$(jq -r ".${array_key}[$index].tags | length" "$file")
# Check if ID is non-empty
if [[ -z "$id" || "$id" == "null" ]]; then
log_error "$content_type item ID cannot be empty at index $index in $file"
return 1
fi
# Check if title is non-empty
if [[ -z "$title" || "$title" == "null" ]]; then
log_error "$content_type item title cannot be empty at index $index in $file"
return 1
fi
# Extract content type from file path dynamically
local expected_lang=$(echo "$file" | sed -n "s|.*${content_type}/\([^/]*\)/.*|\1|p")
if [[ "$language" != "$expected_lang" ]]; then
log_warning "Language mismatch in $file: expected '$expected_lang', found '$language'"
fi
# Content-type specific validations
if [[ "$content_type" == "blog" ]]; then
local date=$(jq -r ".${array_key}[$index].date" "$file")
# Check date format (YYYY-MM-DD)
if ! [[ "$date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
log_warning "Invalid date format in $content_type item '$id': expected YYYY-MM-DD, found '$date'"
fi
elif [[ "$content_type" == "recipes" ]]; then
local difficulty=$(jq -r ".${array_key}[$index].difficulty" "$file")
# Validate difficulty levels (allow translations)
local valid_difficulties=("Beginner" "Intermediate" "Advanced" "Expert" "Principiante" "Intermedio" "Avanzado" "Experto")
local difficulty_valid=false
for valid_diff in "${valid_difficulties[@]}"; do
if [[ "$difficulty" == "$valid_diff" ]]; then
difficulty_valid=true
break
fi
done
if [[ "$difficulty_valid" == "false" ]]; then
log_warning "Invalid difficulty level '$difficulty' in $content_type item '$id'. Valid levels: ${valid_difficulties[*]}"
fi
fi
# Check if tags array has at least one item
if [[ "$tags" == "0" ]]; then
log_warning "$content_type item '$id' has no tags"
fi
return 0
}
# Generic validate content index file
validate_content_index() {
local file="$1"
local content_type="$2"
local language="$3"
log_info "Validating $content_type index: $file"
((TOTAL_FILES++))
# Check if file exists
if [[ ! -f "$file" ]]; then
log_error "$content_type index file not found: $file"
return 1
fi
# Validate JSON syntax
if ! validate_json_syntax "$file"; then
return 1
fi
# Determine array key based on content type
local array_key="${content_type}"
if [[ "$content_type" == "blog" ]]; then
array_key="blog_posts"
fi
# Check required top-level fields
if ! jq -e "has(\"$array_key\")" "$file" >/dev/null 2>&1; then
log_error "Missing '$array_key' field in $file"
return 1
fi
if ! jq -e 'has("language")' "$file" >/dev/null 2>&1; then
log_error "Missing 'language' field in $file"
return 1
fi
# Check if content array is an array
if ! jq -e ".$array_key | type == \"array\"" "$file" >/dev/null 2>&1; then
log_error "'$array_key' must be an array in $file"
return 1
fi
# Check language field matches directory
local file_language=$(jq -r '.language' "$file")
if [[ "$file_language" != "$language" ]]; then
log_error "Language mismatch in $file: expected '$language', found '$file_language'"
return 1
fi
# Validate each content item
local item_count=$(jq ".$array_key | length" "$file")
local valid_items=0
for ((i=0; i<item_count; i++)); do
if validate_content_item "$file" "$content_type" "$i"; then
((valid_items++))
fi
done
log_info "Validated $valid_items/$item_count $content_type items in $file"
((VALID_FILES++))
return 0
}
# Check for duplicate IDs across languages
check_duplicate_ids() {
log_info "Checking for duplicate IDs..."
# Simple approach: collect all IDs and check for duplicates
local temp_dir="/tmp/content_validation_$$"
mkdir -p "$temp_dir"
# Collect blog IDs
for lang in "${SUPPORTED_LANGUAGES[@]}"; do
local blog_file="${CONTENT_DIR}/blog/${lang}/index.json"
if [[ -f "$blog_file" ]]; then
jq -r '.blog_posts[].id' "$blog_file" 2>/dev/null | while read -r id; do
if [[ -n "$id" && "$id" != "null" ]]; then
echo "$id" >> "$temp_dir/blog_ids"
fi
done
fi
done
# Check for duplicate blog IDs
if [[ -f "$temp_dir/blog_ids" ]]; then
local duplicates=$(sort "$temp_dir/blog_ids" | uniq -d)
if [[ -n "$duplicates" ]]; then
while read -r dup_id; do
log_warning "Duplicate blog post ID found: '$dup_id'"
done <<< "$duplicates"
fi
fi
# Collect recipe IDs
for lang in "${SUPPORTED_LANGUAGES[@]}"; do
local recipe_file="${CONTENT_DIR}/recipes/${lang}/index.json"
if [[ -f "$recipe_file" ]]; then
jq -r '.recipes[].id' "$recipe_file" 2>/dev/null | while read -r id; do
if [[ -n "$id" && "$id" != "null" ]]; then
echo "$id" >> "$temp_dir/recipe_ids"
fi
done
fi
done
# Check for duplicate recipe IDs
if [[ -f "$temp_dir/recipe_ids" ]]; then
local duplicates=$(sort "$temp_dir/recipe_ids" | uniq -d)
if [[ -n "$duplicates" ]]; then
while read -r dup_id; do
log_warning "Duplicate recipe ID found: '$dup_id'"
done <<< "$duplicates"
fi
fi
# Cleanup
rm -rf "$temp_dir"
}
# Validate translation references
validate_translations() {
log_info "Validating translation references..."
# Simple translation validation - check if any content claims translations
for lang in "${SUPPORTED_LANGUAGES[@]}"; do
local blog_file="${CONTENT_DIR}/blog/${lang}/index.json"
if [[ -f "$blog_file" ]]; then
local posts_with_translations=$(jq -r '.blog_posts[] | select(.translations | length > 0) | .id' "$blog_file" 2>/dev/null || echo "")
if [[ -n "$posts_with_translations" ]]; then
log_info "Blog posts in '$lang' claiming translations: $(echo "$posts_with_translations" | tr '\n' ' ')"
fi
fi
local recipe_file="${CONTENT_DIR}/recipes/${lang}/index.json"
if [[ -f "$recipe_file" ]]; then
local recipes_with_translations=$(jq -r '.recipes[] | select(.translations | length > 0) | .id' "$recipe_file" 2>/dev/null || echo "")
if [[ -n "$recipes_with_translations" ]]; then
log_info "Recipes in '$lang' claiming translations: $(echo "$recipes_with_translations" | tr '\n' ' ')"
fi
fi
done
}
# Main validation function
main() {
echo "Starting content validation..."
echo "Content directory: $CONTENT_DIR"
echo "Supported languages: ${SUPPORTED_LANGUAGES[*]}"
echo ""
# Check dependencies
check_dependencies
# Validate each language and content type
for lang in "${SUPPORTED_LANGUAGES[@]}"; do
log_info "Validating content for language: $lang"
# Validate content for each discovered content type
for content_type in "${CONTENT_TYPES[@]}"; do
local content_index="${CONTENT_DIR}/${content_type}/${lang}/index.json"
if [[ -f "$content_index" ]]; then
validate_content_index "$content_index" "$content_type" "$lang"
else
log_info "Content index not found (skipping): $content_index"
fi
done
done
# Cross-validation checks
check_duplicate_ids
validate_translations
# Summary
echo ""
echo "==============================="
echo -e "${BLUE}📊 Validation Summary${NC}"
echo "==============================="
echo "Total files validated: $TOTAL_FILES"
echo "Valid files: $VALID_FILES"
echo -e "${RED}Errors: $ERROR_COUNT${NC}"
echo -e "${YELLOW}Warnings: $WARNING_COUNT${NC}"
if [[ $ERROR_COUNT -eq 0 ]]; then
if [[ $WARNING_COUNT -eq 0 ]]; then
log_success "All content validation passed! 🎉"
else
echo -e "${YELLOW}Validation completed with warnings. Please review the warnings above.${NC}"
fi
exit 0
else
log_error "Validation failed with $ERROR_COUNT errors. Please fix the errors above."
exit 1
fi
}
# Run main function
main "$@"