374 lines
12 KiB
Bash
374 lines
12 KiB
Bash
|
|
#!/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 "$@"
|