website-htmx-rustelo-code/scripts/sh/content/validate-content.sh
2026-07-10 03:44:13 +01:00

373 lines
12 KiB
Bash
Executable file
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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