#!/usr/bin/env nu # Unified Content Management Script # Nushell version of content-manager.sh # Replaces: generate-index.sh, validate-id-consistency.sh, validate-content.sh def main [command?: string, ...args] { print $"(ansi blue)📝 Unified Content Management Tool(ansi reset)" # Load configuration let config = load_content_config if ($command | is-empty) { show_content_usage exit 0 } match $command { "generate-indices" | "indices" => { cmd_generate_indices $config } "validate-ids" | "ids" => { cmd_validate_ids $config } "validate-content" | "validate" => { cmd_validate_content $config } "validate-consistency" | "consistency" => { cmd_validate_consistency $config } "show-stats" | "stats" => { cmd_show_stats $config } "help" | "-h" | "--help" => { show_content_usage } _ => { print $"(ansi red)❌ Unknown command: ($command)(ansi reset)" show_content_usage exit 1 } } } # Load content configuration from environment and files def load_content_config [] { let content_dir = ($env.SITE_CONTENT_PATH? | default "site/content") let languages = ["en", "es"] { content_dir: $content_dir, languages: $languages, content_types: (get_enabled_content_types $content_dir), templates_dir: "scripts/content/templates" } } # Show usage information def show_content_usage [] { print "Usage: nu content-manager.nu [COMMAND] [OPTIONS]" print "" print "Commands:" print " generate-indices Generate JSON indices from markdown frontmatter" print " validate-ids Validate ID consistency across languages" print " validate-content Validate content structure and fields" print " validate-consistency Validate content consistency across languages" print " show-stats Show content statistics" print " help Show this help message" print "" print "Examples:" print " nu content-manager.nu generate-indices" print " nu content-manager.nu validate-content" print " nu content-manager.nu show-stats" } # Get enabled content types from content-kinds.toml def get_enabled_content_types [content_dir: string] { let content_kinds_file = $"($content_dir)/content-kinds.toml" if not ($content_kinds_file | path exists) { print $"(ansi yellow)⚠️ Warning: content-kinds.toml not found, using default types(ansi reset)" return ["blog", "recipes"] } # Parse TOML file for enabled content types (simplified version) mut enabled_types = [] let content = (open $content_kinds_file | lines) mut in_content_kinds_section = false mut current_directory = "" mut current_enabled = false for line in $content { if ($line | str starts-with "#") or ($line | str trim | is-empty) { continue } if $line =~ "^\\[\\[content_kinds\\]\\]" { # Process previous section if enabled if $in_content_kinds_section and $current_enabled and ($current_directory | is-not-empty) { let dir_path = $"($content_dir)/($current_directory)" if ($dir_path | path exists) { $enabled_types = ($enabled_types | append $current_directory) } } $in_content_kinds_section = true $current_directory = "" $current_enabled = false continue } if $in_content_kinds_section { if $line =~ "^\\[\\[" { # Process current section before moving to next if $current_enabled and ($current_directory | is-not-empty) { let dir_path = $"($content_dir)/($current_directory)" if ($dir_path | path exists) { $enabled_types = ($enabled_types | append $current_directory) } } $in_content_kinds_section = false continue } if $line =~ 'directory\\s*=\\s*"([^"]+)"' { let matches = ($line | parse --regex 'directory\\s*=\\s*"([^"]+)"') if ($matches | length) > 0 { $current_directory = ($matches | first | get capture0) } } else if $line =~ 'enabled\\s*=\\s*true' { $current_enabled = true } } } # Process final section if $in_content_kinds_section and $current_enabled and ($current_directory | is-not-empty) { let dir_path = $"($content_dir)/($current_directory)" if ($dir_path | path exists) { $enabled_types = ($enabled_types | append $current_directory) } } if ($enabled_types | is-empty) { return ["blog", "recipes"] } $enabled_types } # Generate JSON indices from markdown frontmatter def cmd_generate_indices [config] { print $"(ansi blue)🔧 Generating content indices...(ansi reset)" mut total_errors = 0 for content_type in $config.content_types { for language in $config.languages { let content_dir = $"($config.content_dir)/($content_type)/($language)" if not ($content_dir | path exists) { print $"(ansi yellow)⚠️ Content directory not found: ($content_dir)(ansi reset)" continue } print $"(ansi yellow)🔧 Generating ($content_type) index for language: ($language)(ansi reset)" let index_file = $"($content_dir)/index.json" mut entries = [] # Process each markdown file let md_files = (ls $"($content_dir)/**/*.md" | where type == file) for file_info in $md_files { let md_file = $file_info.name try { let entry = (process_markdown_file $md_file $content_type $language) $entries = ($entries | append $entry) } catch { print $"(ansi yellow)⚠️ Failed to process: ($md_file)(ansi reset)" } } # Determine array name (keep legacy naming for recipes) let array_name = if $content_type == "recipes" { "prescriptions" } else { $"($content_type)_posts" } # Build final index.json let index_content = { ($array_name): $entries, language: $language } try { $index_content | to json | save $index_file print $"(ansi green)✅ Generated valid ($content_type) index (($language)): ($entries | length) entries(ansi reset)" } catch { print $"(ansi red)❌ Failed to generate JSON for ($content_type) (($language))(ansi reset)" $total_errors = ($total_errors + 1) } } } if $total_errors == 0 { print $"(ansi green)🎉 Index generation completed successfully!(ansi reset)" } else { print $"(ansi red)❌ Index generation completed with ($total_errors) errors(ansi reset)" exit 1 } } # Process a single markdown file and extract metadata def process_markdown_file [md_file: string, content_type: string, language: string] { let content = (open $md_file | lines) let filename = ($md_file | path basename | path parse | get stem) # Extract frontmatter let frontmatter = extract_frontmatter_from_lines $content # Extract basic fields with fallbacks let title = (extract_frontmatter_field $frontmatter "title" $filename) let description = (extract_frontmatter_field $frontmatter "description" "") let author = (extract_frontmatter_field $frontmatter "author" "") let date = (extract_frontmatter_field $frontmatter "date" "") let category = (extract_frontmatter_field $frontmatter "category" "") let tags = (extract_frontmatter_array $frontmatter "tags") let published = (extract_frontmatter_field $frontmatter "published" "true") # Only include published content if $published != "true" { return null } # Create content entry { id: $filename, title: $title, description: $description, author: $author, date: $date, category: $category, tags: $tags, filename: $filename, html_file: $"($filename).html" } } # Extract frontmatter from content lines def extract_frontmatter_from_lines [content: list] { if ($content | length) == 0 or ($content | first) != "---" { return [] } mut frontmatter_lines = [] mut in_frontmatter = false mut line_count = 0 for line in $content { $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 } # Extract array values from frontmatter def extract_frontmatter_array [frontmatter: list, key: string] { for line in $frontmatter { if $line =~ $"($key):\\s*\\[(.+)\\]" { let matches = ($line | parse --regex $"($key):\\s*\\[(.+)\\]") if ($matches | length) > 0 { let array_content = ($matches | first | get capture0) return ($array_content | split row "," | each { |item| $item | str trim | str replace -a '"' "" }) } } } [] } # Validate ID consistency across languages def cmd_validate_ids [config] { print $"(ansi blue)🔧 Validating ID consistency across languages...(ansi reset)" mut total_errors = 0 for content_type in $config.content_types { print $"(ansi yellow)Validating ($content_type) IDs...(ansi reset)" # Get IDs for each language mut language_ids = {} for language in $config.languages { let content_dir = $"($config.content_dir)/($content_type)/($language)" if ($content_dir | path exists) { let md_files = (ls $"($content_dir)/**/*.md" | where type == file) let ids = ($md_files | each { |file| $file.name | path basename | path parse | get stem }) $language_ids = ($language_ids | upsert $language $ids) } } # Compare IDs across languages let languages_with_content = ($language_ids | columns) if ($languages_with_content | length) >= 2 { let primary_lang = ($languages_with_content | first) let primary_ids = ($language_ids | get $primary_lang) for lang in ($languages_with_content | skip 1) { let lang_ids = ($language_ids | get $lang) # Find missing IDs let missing_in_lang = ($primary_ids | where {|id| $id not-in $lang_ids}) let missing_in_primary = ($lang_ids | where {|id| $id not-in $primary_ids}) if ($missing_in_lang | length) > 0 { print $"(ansi red)❌ ($content_type): Missing in ($lang): ($missing_in_lang | str join ', ')(ansi reset)" $total_errors = ($total_errors + ($missing_in_lang | length)) } if ($missing_in_primary | length) > 0 { print $"(ansi red)❌ ($content_type): Missing in ($primary_lang): ($missing_in_primary | str join ', ')(ansi reset)" $total_errors = ($total_errors + ($missing_in_primary | length)) } } if ($missing_in_lang | length) == 0 and ($missing_in_primary | length) == 0 { print $"(ansi green)✅ ($content_type): ID consistency validated across languages(ansi reset)" } } } if $total_errors == 0 { print $"(ansi green)🎉 ID validation completed successfully!(ansi reset)" } else { print $"(ansi red)❌ ID validation completed with ($total_errors) inconsistencies(ansi reset)" exit 1 } } # Validate content structure and required fields def cmd_validate_content [config] { print $"(ansi blue)🔧 Validating content structure and fields...(ansi reset)" mut total_errors = 0 for content_type in $config.content_types { for language in $config.languages { let content_dir = $"($config.content_dir)/($content_type)/($language)" if not ($content_dir | path exists) { continue } print $"(ansi yellow)Validating ($content_type) content (($language))...(ansi reset)" let md_files = (ls $"($content_dir)/**/*.md" | where type == file) for file_info in $md_files { let md_file = $file_info.name let filename = ($md_file | path basename) try { let content = (open $md_file | lines) let frontmatter = (extract_frontmatter_from_lines $content) # Validate required fields let title = (extract_frontmatter_field $frontmatter "title" "") if ($title | is-empty) { print $"(ansi red)❌ ($filename): Missing required field 'title'(ansi reset)" $total_errors = ($total_errors + 1) } # Validate content has body (more than just frontmatter) let content_lines = ($content | length) let frontmatter_lines = ($frontmatter | length) if ($content_lines - $frontmatter_lines - 2) < 3 { # Account for --- delimiters print $"(ansi yellow)⚠️ ($filename): Very short content (may be empty)(ansi reset)" } } catch { print $"(ansi red)❌ ($filename): Failed to parse content(ansi reset)" $total_errors = ($total_errors + 1) } } # Validate index.json exists and is valid let index_file = $"($content_dir)/index.json" if ($index_file | path exists) { try { open $index_file | from json | ignore print $"(ansi green)✅ ($content_type) (($language)): Valid index.json(ansi reset)" } catch { print $"(ansi red)❌ ($content_type) (($language)): Invalid index.json(ansi reset)" $total_errors = ($total_errors + 1) } } else { print $"(ansi yellow)⚠️ ($content_type) (($language)): Missing index.json(ansi reset)" } } } if $total_errors == 0 { print $"(ansi green)🎉 Content validation completed successfully!(ansi reset)" } else { print $"(ansi red)❌ Content validation completed with ($total_errors) errors(ansi reset)" exit 1 } } # Validate content consistency across languages def cmd_validate_consistency [config] { print $"(ansi blue)🔧 Validating content consistency across languages...(ansi reset)" cmd_validate_ids $config cmd_validate_content $config } # Show content statistics def cmd_show_stats [config] { print $"(ansi blue)📊 Content Statistics(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: ($config.content_types | str join ', ')(ansi reset)" print "" for content_type in $config.content_types { print $"(ansi cyan)📝 ($content_type | str title-case):(ansi reset)" 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) let index_exists = ($"($content_dir)/index.json" | path exists) let index_status = if $index_exists { "✅" } else { "❌" } print $" ($language): ($md_count) files, index: ($index_status)" } else { print $" ($language): directory not found" } } print "" } }