#!/usr/bin/env nu # Content Validation Script # Nushell version of validate-content.sh # Validates JSON structure, required fields, and content integrity def main [...args] { let config = { content_dir: ($env.SITE_CONTENT_PATH? | default "site/content"), languages: ["en", "es"] } print $"(ansi blue)🔍 Content Validation Tool(ansi reset)" # Get content types dynamically let content_types = get_content_types $config.content_dir if ($args | length) > 0 and ($args | first) in ["-h", "--help", "help"] { show_validation_usage exit 0 } # Run comprehensive validation mut total_errors = 0 $total_errors = $total_errors + (validate_directory_structure $config $content_types) $total_errors = $total_errors + (validate_json_files $config $content_types) $total_errors = $total_errors + (validate_content_fields $config $content_types) $total_errors = $total_errors + (validate_cross_references $config $content_types) show_validation_summary $config $content_types $total_errors if $total_errors > 0 { exit 1 } } # Show usage information def show_validation_usage [] { print "Usage: nu validate-content.nu [OPTIONS]" print "" print "This script validates all localized content for:" print " • Directory structure consistency" print " • JSON file validity" print " • Required content fields" print " • Cross-reference integrity" print "" print "Options:" print " -h, --help Show this help message" print "" print "Environment Variables:" print " SITE_CONTENT_PATH Content root directory [default: site/content]" } # Get content types from directory or content-kinds.toml def get_content_types [content_dir: string] { let content_kinds_file = $"($content_dir)/content-kinds.toml" if ($content_kinds_file | path exists) { # Extract from TOML file try { let content = (open $content_kinds_file) # Simple extraction for now - can be enhanced with proper TOML parsing $content | lines | where {|line| $line =~ "directory\\s*=" } | each {|line| let matches = ($line | parse --regex 'directory\\s*=\\s*"([^"]+)"') if ($matches | length) > 0 { $matches | first | get capture0 } else { null } } | where {|x| $x != null} } catch { # Fallback to directory discovery ls $content_dir | where type == dir | where name !~ "(locales|themes|menus|footer|tmp|routes)" | get name | path basename } } else { # Discover from directory structure ls $content_dir | where type == dir | where name !~ "(locales|themes|menus|footer|tmp|routes)" | get name | path basename } } # Validate directory structure consistency def validate_directory_structure [config, content_types: list] { print $"(ansi blue)🔧 Validating directory structure...(ansi reset)" mut errors = 0 # Check if content root exists if not ($config.content_dir | path exists) { print $"(ansi red)❌ Content root directory not found: ($config.content_dir)(ansi reset)" return 1 } # Validate each content type has language subdirectories for content_type in $content_types { let content_type_dir = $"($config.content_dir)/($content_type)" if not ($content_type_dir | path exists) { print $"(ansi red)❌ Content type directory not found: ($content_type_dir)(ansi reset)" $errors += 1 continue } print $"(ansi yellow) Checking ($content_type) structure...(ansi reset)" # Check each language directory for language in $config.languages { let lang_dir = $"($content_type_dir)/($language)" if ($lang_dir | path exists) { let md_files = (ls $"($lang_dir)/**/*.md" | where type == file | length) if $md_files == 0 { print $"(ansi yellow)⚠️ No markdown files in ($lang_dir)(ansi reset)" } else { print $"(ansi green) ✅ ($language): ($md_files) markdown files(ansi reset)" } } else { print $"(ansi yellow)⚠️ Missing language directory: ($lang_dir)(ansi reset)" } } } if $errors == 0 { print $"(ansi green)✅ Directory structure validation passed(ansi reset)" } else { print $"(ansi red)❌ Directory structure validation failed with ($errors) errors(ansi reset)" } $errors } # Validate JSON files def validate_json_files [config, content_types: list] { print $"(ansi blue)🔧 Validating JSON files...(ansi reset)" mut errors = 0 for content_type in $content_types { for language in $config.languages { let content_dir = $"($config.content_dir)/($content_type)/($language)" if not ($content_dir | path exists) { continue } # Check index.json let index_file = $"($content_dir)/index.json" if ($index_file | path exists) { try { let index_content = (open $index_file | from json) # Validate structure let has_language = ($index_content | columns | where $it == "language" | length) > 0 if not $has_language { print $"(ansi red)❌ ($content_type)/($language): index.json missing 'language' field(ansi reset)" $errors += 1 } # Check for content array let array_fields = ($index_content | columns | where $it =~ "_posts|prescriptions") if ($array_fields | length) == 0 { print $"(ansi red)❌ ($content_type)/($language): index.json missing content array(ansi reset)" $errors += 1 } else { let array_name = ($array_fields | first) let content_array = ($index_content | get $array_name) print $"(ansi green) ✅ ($content_type)/($language): valid index.json with ($content_array | length) entries(ansi reset)" } } catch { print $"(ansi red)❌ ($content_type)/($language): invalid JSON in index.json(ansi reset)" # Note: Error count handled at validation level } } else { print $"(ansi yellow)⚠️ ($content_type)/($language): missing index.json(ansi reset)" } # Check meta.json if exists let meta_file = $"($content_dir)/meta.json" if ($meta_file | path exists) { try { open $meta_file | from json | ignore print $"(ansi green) ✅ ($content_type)/($language): valid meta.json(ansi reset)" } catch { print $"(ansi red)❌ ($content_type)/($language): invalid JSON in meta.json(ansi reset)" # Note: Error count handled at validation level } } } } if $errors == 0 { print $"(ansi green)✅ JSON validation passed(ansi reset)" } else { print $"(ansi red)❌ JSON validation failed with ($errors) errors(ansi reset)" } $errors } # Validate content fields in markdown files def validate_content_fields [config, content_types: list] { print $"(ansi blue)🔧 Validating content fields...(ansi reset)" mut errors = 0 mut total_files = 0 for content_type in $content_types { for language in $config.languages { let content_dir = $"($config.content_dir)/($content_type)/($language)" if not ($content_dir | path exists) { continue } let md_files = (ls $"($content_dir)/**/*.md" | where type == file) $total_files += ($md_files | length) for file_info in $md_files { let md_file = $file_info.name let filename = ($md_file | path basename) try { let content_lines = (open $md_file | lines) let validation_result = (validate_markdown_file $content_lines $filename $content_type) if $validation_result.errors > 0 { $errors += $validation_result.errors print $"(ansi red)❌ ($filename): ($validation_result.messages | str join ', ')(ansi reset)" } } catch { print $"(ansi red)❌ ($filename): Failed to read or parse file(ansi reset)" # Note: Error count handled at file level } } } } if $errors == 0 { print $"(ansi green)✅ Content fields validation passed \(($total_files) files checked\)(ansi reset)" } else { print $"(ansi red)❌ Content fields validation failed with ($errors) errors in ($total_files) files(ansi reset)" } $errors } # Validate a single markdown file def validate_markdown_file [content_lines: list, filename: string, content_type: string] { mut errors = 0 mut messages = [] # Check if file has frontmatter if ($content_lines | length) == 0 or ($content_lines | first) != "---" { $errors = $errors + 1 $messages = ($messages | append "Missing frontmatter") return {errors: $errors, messages: $messages} } # Extract frontmatter let frontmatter = extract_frontmatter $content_lines # Check required fields let required_fields = ["title"] for field in $required_fields { let field_value = (extract_frontmatter_field $frontmatter $field "") if ($field_value | is-empty) { $errors += 1 $messages = ($messages | append $"Missing required field: ($field)") } } # Check content has body (more than just frontmatter) let frontmatter_lines = ($frontmatter | length) let total_lines = ($content_lines | length) let body_lines = $total_lines - $frontmatter_lines - 2 # Account for --- delimiters if $body_lines < 3 { $messages = ($messages | append "Very short content (may be empty)") } # Validate specific fields for content type if $content_type == "recipes" { let difficulty = (extract_frontmatter_field $frontmatter "difficulty" "") if not ($difficulty | is-empty) and $difficulty not-in ["Beginner", "Intermediate", "Advanced"] { $errors += 1 $messages = ($messages | append "Invalid difficulty level (must be Beginner, Intermediate, or Advanced)") } } {errors: $errors, messages: $messages} } # Extract frontmatter from content lines def extract_frontmatter [content_lines: list] { if ($content_lines | length) == 0 or ($content_lines | first) != "---" { return [] } mut frontmatter_lines = [] mut in_frontmatter = false mut line_count = 0 for line in $content_lines { $line_count = $line_count + 1 if $line_count == 1 and $line == "---" { $in_frontmatter = true continue } if $in_frontmatter and $line == "---" { break } if $in_frontmatter { $frontmatter_lines = ($frontmatter_lines | append $line) } } $frontmatter_lines } # Extract field value from frontmatter def extract_frontmatter_field [frontmatter: list, key: string, default: string] { for line in $frontmatter { if $line =~ $"($key):\\s*(.+)" { let matches = ($line | parse --regex $"($key):\\s*\"?([^\"\\n]+)\"?") if ($matches | length) > 0 { return ($matches | first | get capture0 | str trim) } } } $default } # Validate cross-references between files def validate_cross_references [config, content_types: list] { print $"(ansi blue)🔧 Validating cross-references...(ansi reset)" mut errors = 0 # Check if index.json entries match actual files for content_type in $content_types { for language in $config.languages { let content_dir = $"($config.content_dir)/($content_type)/($language)" let index_file = $"($content_dir)/index.json" if not ($content_dir | path exists) or not ($index_file | path exists) { continue } try { let index_content = (open $index_file | from json) # Get content array let array_fields = ($index_content | columns | where $it =~ "_posts|prescriptions") if ($array_fields | length) == 0 { continue } let array_name = ($array_fields | first) let content_entries = ($index_content | get $array_name) # Get actual markdown files let md_files = (ls $"($content_dir)/**/*.md" | where type == file) let actual_ids = ($md_files | each { |file| $file.name | path basename | path parse | get stem }) # Check index entries exist as files for entry in $content_entries { if "id" in ($entry | columns) { let entry_id = ($entry | get id) if $entry_id not-in $actual_ids { print $"(ansi red)❌ ($content_type)/($language): index.json references missing file: ($entry_id).md(ansi reset)" $errors += 1 } } } # Check files exist in index for file_id in $actual_ids { let index_entries_with_id = ($content_entries | where id == $file_id) if ($index_entries_with_id | length) == 0 { print $"(ansi yellow)⚠️ ($content_type)/($language): file ($file_id).md not in index.json(ansi reset)" } } if $errors == 0 { print $"(ansi green) ✅ ($content_type)/($language): cross-references validated(ansi reset)" } } catch { print $"(ansi red)❌ ($content_type)/($language): Failed to validate cross-references(ansi reset)" # Note: Error count handled at validation level } } } if $errors == 0 { print $"(ansi green)✅ Cross-references validation passed(ansi reset)" } else { print $"(ansi red)❌ Cross-references validation failed with ($errors) errors(ansi reset)" } $errors } # Show validation summary def show_validation_summary [config, content_types: list, total_errors: int] { print "" print $"(ansi blue)📊 Validation Summary(ansi reset)" print $"(ansi blue)Content root: ($config.content_dir)(ansi reset)" print $"(ansi blue)Languages: ($config.languages | str join ', ')(ansi reset)" print $"(ansi blue)Content types: ($content_types | str join ', ')(ansi reset)" print "" # Show file counts mut total_files = 0 for content_type in $content_types { for language in $config.languages { let content_dir = $"($config.content_dir)/($content_type)/($language)" if ($content_dir | path exists) { let md_count = (ls $"($content_dir)/**/*.md" | where type == file | length) $total_files = $total_files + $md_count } } } print $"(ansi blue)Total files validated: ($total_files)(ansi reset)" if $total_errors == 0 { print $"(ansi green)🎉 All validations passed successfully!(ansi reset)" } else { print $"(ansi red)❌ Validation failed with ($total_errors) total errors(ansi reset)" print "" print $"(ansi yellow)💡 To fix errors:(ansi reset)" print " 1. Check missing or invalid frontmatter fields" print " 2. Ensure JSON files have valid syntax" print " 3. Verify all referenced files exist" print " 4. Run content generation to create missing files" } }