370 lines
11 KiB
Bash
370 lines
11 KiB
Bash
|
|
#!/bin/bash
|
|||
|
|
|
|||
|
|
# Content ID Consistency Validator
|
|||
|
|
# This script validates that content IDs are consistent between language versions
|
|||
|
|
# It should be run during content generation to catch ID mismatches early
|
|||
|
|
|
|||
|
|
set -e
|
|||
|
|
|
|||
|
|
# Configuration
|
|||
|
|
CONTENT_DIR="content"
|
|||
|
|
CONTENT_KINDS_FILE="content/content-kinds.toml"
|
|||
|
|
|
|||
|
|
# Supported languages
|
|||
|
|
LANGUAGES=("en" "es")
|
|||
|
|
|
|||
|
|
# Colors for output
|
|||
|
|
RED='\033[0;31m'
|
|||
|
|
GREEN='\033[0;32m'
|
|||
|
|
YELLOW='\033[1;33m'
|
|||
|
|
BLUE='\033[0;34m'
|
|||
|
|
CYAN='\033[0;36m'
|
|||
|
|
NC='\033[0m' # No Color
|
|||
|
|
|
|||
|
|
# Get enabled content types from content-kinds.toml
|
|||
|
|
get_content_types() {
|
|||
|
|
if [ ! -f "$CONTENT_KINDS_FILE" ]; then
|
|||
|
|
echo "blog recipes" # fallback
|
|||
|
|
return
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Extract content type names from TOML where enabled = true
|
|||
|
|
grep -A 10 "^\[\[content_kinds\]\]" "$CONTENT_KINDS_FILE" | \
|
|||
|
|
awk '
|
|||
|
|
/^\[\[content_kinds\]\]/ { in_section = 1; name = ""; enabled = ""; next }
|
|||
|
|
/^name = / { if (in_section) { name = $3; gsub(/["'"'"']/, "", name) } }
|
|||
|
|
/^enabled = / { if (in_section) { enabled = $3; gsub(/["'"'"']/, "", enabled) } }
|
|||
|
|
/^$/ || /^\[/ {
|
|||
|
|
if (in_section && name && enabled == "true") print name
|
|||
|
|
in_section = 0; name = ""; enabled = ""
|
|||
|
|
}
|
|||
|
|
END { if (in_section && name && enabled == "true") print name }
|
|||
|
|
' | tr '\n' ' '
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Helper functions
|
|||
|
|
log_error() {
|
|||
|
|
echo -e "${RED}❌ ERROR: $1${NC}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log_warning() {
|
|||
|
|
echo -e "${YELLOW}⚠️ WARNING: $1${NC}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log_success() {
|
|||
|
|
echo -e "${GREEN}✅ $1${NC}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log_info() {
|
|||
|
|
echo -e "${BLUE}ℹ️ $1${NC}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
log_step() {
|
|||
|
|
echo -e "${CYAN}🔧 $1${NC}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Extract ID from markdown frontmatter
|
|||
|
|
extract_id() {
|
|||
|
|
local file="$1"
|
|||
|
|
|
|||
|
|
if [ ! -f "$file" ]; then
|
|||
|
|
echo ""
|
|||
|
|
return
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Extract ID from YAML frontmatter
|
|||
|
|
if head -1 "$file" | grep -q "^---$"; then
|
|||
|
|
awk '/^---$/{flag++; next} flag==1 && /^---$/{flag++} flag==1 && /^id:/{print $2; exit}' "$file" | sed 's/["\047]//g'
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Extract filename without extension
|
|||
|
|
get_filename() {
|
|||
|
|
local file="$1"
|
|||
|
|
basename "$file" .md
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Validate ID consistency for a content type
|
|||
|
|
validate_content_type() {
|
|||
|
|
local content_type="$1"
|
|||
|
|
local errors=0
|
|||
|
|
local warnings=0
|
|||
|
|
|
|||
|
|
log_step "Validating $content_type ID consistency..."
|
|||
|
|
|
|||
|
|
# Get the primary language directory (English)
|
|||
|
|
local primary_lang="en"
|
|||
|
|
local primary_dir="${CONTENT_DIR}/${content_type}/${primary_lang}"
|
|||
|
|
|
|||
|
|
if [ ! -d "$primary_dir" ]; then
|
|||
|
|
log_error "Primary language directory not found: $primary_dir"
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Create temporary files for tracking
|
|||
|
|
local temp_dir="/tmp/id_validation_$$"
|
|||
|
|
mkdir -p "$temp_dir"
|
|||
|
|
|
|||
|
|
# Process each markdown file in primary language
|
|||
|
|
for primary_file in "$primary_dir"/*.md; do
|
|||
|
|
if [ ! -f "$primary_file" ]; then
|
|||
|
|
continue
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
local filename=$(get_filename "$primary_file")
|
|||
|
|
local primary_id=$(extract_id "$primary_file")
|
|||
|
|
|
|||
|
|
if [ -z "$primary_id" ]; then
|
|||
|
|
log_warning "No ID found in primary file: $primary_file"
|
|||
|
|
warnings=$((warnings + 1))
|
|||
|
|
continue
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
log_info "Checking $content_type: $filename (ID: $primary_id)"
|
|||
|
|
|
|||
|
|
# Check corresponding files in other languages
|
|||
|
|
for lang in "${LANGUAGES[@]}"; do
|
|||
|
|
if [ "$lang" = "$primary_lang" ]; then
|
|||
|
|
continue
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
local lang_dir="${CONTENT_DIR}/${content_type}/${lang}"
|
|||
|
|
local lang_file="${lang_dir}/${filename}.md"
|
|||
|
|
|
|||
|
|
if [ ! -f "$lang_file" ]; then
|
|||
|
|
log_warning "Missing $lang translation for $filename"
|
|||
|
|
warnings=$((warnings + 1))
|
|||
|
|
continue
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
local lang_id=$(extract_id "$lang_file")
|
|||
|
|
|
|||
|
|
if [ -z "$lang_id" ]; then
|
|||
|
|
log_error "No ID found in $lang file: $lang_file"
|
|||
|
|
errors=$((errors + 1))
|
|||
|
|
continue
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
if [ "$primary_id" != "$lang_id" ]; then
|
|||
|
|
log_error "ID mismatch for $filename:"
|
|||
|
|
log_error " $primary_lang: '$primary_id'"
|
|||
|
|
log_error " $lang: '$lang_id'"
|
|||
|
|
log_error " Files: $primary_file <-> $lang_file"
|
|||
|
|
errors=$((errors + 1))
|
|||
|
|
|
|||
|
|
# Store mismatch for summary
|
|||
|
|
echo "$content_type,$filename,$primary_lang,$primary_id,$lang,$lang_id" >> "$temp_dir/mismatches.csv"
|
|||
|
|
else
|
|||
|
|
log_success " ✓ $lang: $lang_id"
|
|||
|
|
fi
|
|||
|
|
done
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
# Generate summary
|
|||
|
|
if [ -f "$temp_dir/mismatches.csv" ]; then
|
|||
|
|
echo ""
|
|||
|
|
log_error "ID MISMATCHES FOUND:"
|
|||
|
|
echo -e "${RED}Content Type | Filename | ${primary_lang} ID | Other Lang | Other ID${NC}"
|
|||
|
|
echo -e "${RED}-------------|----------|--------|-----------|--------${NC}"
|
|||
|
|
while IFS=',' read -r content_type filename primary_lang primary_id other_lang other_id; do
|
|||
|
|
echo -e "${RED}$content_type | $filename | $primary_id | $other_lang | $other_id${NC}"
|
|||
|
|
done < "$temp_dir/mismatches.csv"
|
|||
|
|
echo ""
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Cleanup
|
|||
|
|
rm -rf "$temp_dir"
|
|||
|
|
|
|||
|
|
if [ $errors -gt 0 ]; then
|
|||
|
|
log_error "$content_type validation failed with $errors errors and $warnings warnings"
|
|||
|
|
return 1
|
|||
|
|
elif [ $warnings -gt 0 ]; then
|
|||
|
|
log_warning "$content_type validation completed with $warnings warnings"
|
|||
|
|
return 0
|
|||
|
|
else
|
|||
|
|
log_success "$content_type validation passed - all IDs are consistent"
|
|||
|
|
return 0
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Generate fix suggestions
|
|||
|
|
generate_fix_suggestions() {
|
|||
|
|
local content_type="$1"
|
|||
|
|
local primary_lang="en"
|
|||
|
|
local primary_dir="${CONTENT_DIR}/${content_type}/${primary_lang}"
|
|||
|
|
|
|||
|
|
echo ""
|
|||
|
|
log_step "Generating fix suggestions for $content_type..."
|
|||
|
|
|
|||
|
|
for primary_file in "$primary_dir"/*.md; do
|
|||
|
|
if [ ! -f "$primary_file" ]; then
|
|||
|
|
continue
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
local filename=$(get_filename "$primary_file")
|
|||
|
|
local primary_id=$(extract_id "$primary_file")
|
|||
|
|
|
|||
|
|
if [ -z "$primary_id" ]; then
|
|||
|
|
continue
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
for lang in "${LANGUAGES[@]}"; do
|
|||
|
|
if [ "$lang" = "$primary_lang" ]; then
|
|||
|
|
continue
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
local lang_file="${CONTENT_DIR}/${content_type}/${lang}/${filename}.md"
|
|||
|
|
|
|||
|
|
if [ ! -f "$lang_file" ]; then
|
|||
|
|
continue
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
local lang_id=$(extract_id "$lang_file")
|
|||
|
|
|
|||
|
|
if [ -n "$lang_id" ] && [ "$primary_id" != "$lang_id" ]; then
|
|||
|
|
echo ""
|
|||
|
|
log_info "Fix command for $filename ($lang):"
|
|||
|
|
echo " sed -i 's/^id: \"$lang_id\"/id: \"$primary_id\"/' \"$lang_file\""
|
|||
|
|
echo " # OR use your editor to change ID from '$lang_id' to '$primary_id'"
|
|||
|
|
fi
|
|||
|
|
done
|
|||
|
|
done
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Check for duplicate IDs within a language
|
|||
|
|
check_duplicate_ids() {
|
|||
|
|
local content_type="$1"
|
|||
|
|
local lang="$2"
|
|||
|
|
local content_dir="${CONTENT_DIR}/${content_type}/${lang}"
|
|||
|
|
|
|||
|
|
if [ ! -d "$content_dir" ]; then
|
|||
|
|
return 0
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
local temp_file="/tmp/ids_${content_type}_${lang}_$$"
|
|||
|
|
|
|||
|
|
# Extract all IDs
|
|||
|
|
for md_file in "$content_dir"/*.md; do
|
|||
|
|
if [ -f "$md_file" ]; then
|
|||
|
|
local id=$(extract_id "$md_file")
|
|||
|
|
if [ -n "$id" ]; then
|
|||
|
|
echo "$id|$(basename "$md_file")" >> "$temp_file"
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
if [ ! -f "$temp_file" ]; then
|
|||
|
|
return 0
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Check for duplicates
|
|||
|
|
local duplicates=$(cut -d'|' -f1 "$temp_file" | sort | uniq -d)
|
|||
|
|
|
|||
|
|
if [ -n "$duplicates" ]; then
|
|||
|
|
log_error "Duplicate IDs found in $content_type ($lang):"
|
|||
|
|
echo "$duplicates" | while read -r duplicate_id; do
|
|||
|
|
log_error " ID '$duplicate_id' used in:"
|
|||
|
|
grep "^$duplicate_id|" "$temp_file" | cut -d'|' -f2 | while read -r filename; do
|
|||
|
|
log_error " - $filename"
|
|||
|
|
done
|
|||
|
|
done
|
|||
|
|
rm -f "$temp_file"
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
rm -f "$temp_file"
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Main validation function
|
|||
|
|
main() {
|
|||
|
|
echo "🔍 Content ID Consistency Validator"
|
|||
|
|
echo "===================================="
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
# Get enabled content types dynamically
|
|||
|
|
local content_types=($(get_content_types))
|
|||
|
|
log_info "Enabled content types: ${content_types[*]}"
|
|||
|
|
echo ""
|
|||
|
|
|
|||
|
|
local total_errors=0
|
|||
|
|
local has_suggestions=false
|
|||
|
|
|
|||
|
|
# Check for duplicate IDs within each language
|
|||
|
|
log_step "Checking for duplicate IDs within languages..."
|
|||
|
|
for content_type in "${content_types[@]}"; do
|
|||
|
|
for lang in "${LANGUAGES[@]}"; do
|
|||
|
|
if ! check_duplicate_ids "$content_type" "$lang"; then
|
|||
|
|
total_errors=$((total_errors + 1))
|
|||
|
|
fi
|
|||
|
|
done
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
# Validate cross-language ID consistency
|
|||
|
|
for content_type in "${content_types[@]}"; do
|
|||
|
|
if ! validate_content_type "$content_type"; then
|
|||
|
|
total_errors=$((total_errors + 1))
|
|||
|
|
has_suggestions=true
|
|||
|
|
fi
|
|||
|
|
done
|
|||
|
|
|
|||
|
|
# Generate fix suggestions if there were errors
|
|||
|
|
if [ "$has_suggestions" = true ]; then
|
|||
|
|
for content_type in "${content_types[@]}"; do
|
|||
|
|
generate_fix_suggestions "$content_type"
|
|||
|
|
done
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
echo ""
|
|||
|
|
echo "======================================"
|
|||
|
|
|
|||
|
|
if [ $total_errors -eq 0 ]; then
|
|||
|
|
log_success "🎉 All content IDs are consistent across languages!"
|
|||
|
|
echo ""
|
|||
|
|
log_info "✅ Content integrity validated"
|
|||
|
|
log_info "✅ No duplicate IDs found"
|
|||
|
|
log_info "✅ Cross-language ID consistency confirmed"
|
|||
|
|
exit 0
|
|||
|
|
else
|
|||
|
|
log_error "❌ Content validation failed with $total_errors issue(s)"
|
|||
|
|
echo ""
|
|||
|
|
log_error "REQUIRED ACTIONS:"
|
|||
|
|
log_error "1. Fix ID mismatches using the suggested commands above"
|
|||
|
|
log_error "2. Ensure all content has consistent IDs across languages"
|
|||
|
|
log_error "3. Re-run this validator to confirm fixes"
|
|||
|
|
log_error "4. Update your content generation process to prevent future mismatches"
|
|||
|
|
echo ""
|
|||
|
|
log_info "💡 TIP: Use 'id' for cross-language lookup and 'slug' for URLs"
|
|||
|
|
log_info "💡 TIP: Add this script to your CI/CD pipeline to catch issues early"
|
|||
|
|
exit 1
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Show usage if no arguments
|
|||
|
|
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
|
|||
|
|
echo "Content ID Consistency Validator"
|
|||
|
|
echo ""
|
|||
|
|
echo "Usage: $0 [OPTIONS]"
|
|||
|
|
echo ""
|
|||
|
|
echo "This script validates that content IDs are consistent between language versions."
|
|||
|
|
echo "It checks all enabled content types in all supported languages."
|
|||
|
|
echo ""
|
|||
|
|
echo "Options:"
|
|||
|
|
echo " --help, -h Show this help message"
|
|||
|
|
echo ""
|
|||
|
|
echo "What it checks:"
|
|||
|
|
echo " ✓ ID consistency across language versions"
|
|||
|
|
echo " ✓ Duplicate IDs within the same language"
|
|||
|
|
echo " ✓ Missing translations"
|
|||
|
|
echo " ✓ Frontmatter format validation"
|
|||
|
|
echo ""
|
|||
|
|
echo "Integration:"
|
|||
|
|
echo " Add to justfile: just validate-content-ids"
|
|||
|
|
echo " Add to CI/CD pipeline for automated validation"
|
|||
|
|
echo " Run before content deployment"
|
|||
|
|
echo ""
|
|||
|
|
exit 0
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# Run main validation
|
|||
|
|
main "$@"
|