#!/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/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 "$@"