website-htmx-rustelo-code/scripts/content/content-manager.nu

451 lines
17 KiB
Text
Raw Permalink Normal View History

2026-07-10 03:44:13 +01:00
#!/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<string>] {
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<string>, 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<string>, 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 ""
}
}