jekyll-theme-zer0 0.7.2 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -0
  3. data/README.md +33 -4
  4. data/_plugins/preview_image_generator.rb +258 -0
  5. data/_plugins/theme_version.rb +88 -0
  6. data/assets/images/previews/git-workflow-best-practices-for-modern-teams.png +0 -0
  7. data/scripts/README.md +443 -0
  8. data/scripts/analyze-commits.sh +313 -0
  9. data/scripts/build +115 -0
  10. data/scripts/build.sh +33 -0
  11. data/scripts/build.sh.legacy +174 -0
  12. data/scripts/example-usage.sh +102 -0
  13. data/scripts/fix-markdown-format.sh +265 -0
  14. data/scripts/gem-publish.sh +42 -0
  15. data/scripts/gem-publish.sh.legacy +700 -0
  16. data/scripts/generate-preview-images.sh +846 -0
  17. data/scripts/install-preview-generator.sh +531 -0
  18. data/scripts/lib/README.md +263 -0
  19. data/scripts/lib/changelog.sh +313 -0
  20. data/scripts/lib/common.sh +154 -0
  21. data/scripts/lib/gem.sh +226 -0
  22. data/scripts/lib/git.sh +205 -0
  23. data/scripts/lib/preview_generator.py +646 -0
  24. data/scripts/lib/test/run_tests.sh +140 -0
  25. data/scripts/lib/test/test_changelog.sh +87 -0
  26. data/scripts/lib/test/test_gem.sh +68 -0
  27. data/scripts/lib/test/test_git.sh +82 -0
  28. data/scripts/lib/test/test_validation.sh +72 -0
  29. data/scripts/lib/test/test_version.sh +96 -0
  30. data/scripts/lib/validation.sh +139 -0
  31. data/scripts/lib/version.sh +178 -0
  32. data/scripts/release +240 -0
  33. data/scripts/release.sh +33 -0
  34. data/scripts/release.sh.legacy +342 -0
  35. data/scripts/setup.sh +155 -0
  36. data/scripts/test-auto-version.sh +260 -0
  37. data/scripts/test-mermaid.sh +251 -0
  38. data/scripts/test.sh +156 -0
  39. data/scripts/version.sh +152 -0
  40. metadata +37 -1
@@ -0,0 +1,846 @@
1
+ #!/bin/bash
2
+ #
3
+ # Script Name: generate-preview-images.sh
4
+ # Description: AI-powered preview image generator for Jekyll posts/articles/quests
5
+ # Scans content files, detects missing preview images, and generates
6
+ # images using AI (OpenAI DALL-E, Stable Diffusion, or other providers)
7
+ #
8
+ # Usage: ./scripts/generate-preview-images.sh [options]
9
+ #
10
+ # Options:
11
+ # -h, --help Show this help message
12
+ # -d, --dry-run Preview what would be generated (no actual changes)
13
+ # -v, --verbose Enable verbose output
14
+ # -f, --file FILE Process a specific file only
15
+ # -c, --collection NAME Process specific collection (posts, quickstart, docs)
16
+ # -p, --provider PROVIDER AI provider (openai, stability, local)
17
+ # --output-dir DIR Output directory for images (default: assets/images/previews)
18
+ # --force Regenerate images even if preview exists
19
+ # --list-missing Only list files with missing previews
20
+ #
21
+ # Dependencies:
22
+ # - bash 4.0+
23
+ # - curl (for API calls)
24
+ # - jq (for JSON processing)
25
+ # - yq or python (for YAML parsing)
26
+ #
27
+ # Environment Variables:
28
+ # OPENAI_API_KEY OpenAI API key for DALL-E image generation
29
+ # STABILITY_API_KEY Stability AI API key for Stable Diffusion
30
+ # IMAGE_STYLE Default image style (default: "digital art, professional")
31
+ # IMAGE_SIZE Image dimensions (default: "1024x1024")
32
+ #
33
+ # Examples:
34
+ # ./scripts/generate-preview-images.sh --dry-run
35
+ # ./scripts/generate-preview-images.sh --collection posts
36
+ # ./scripts/generate-preview-images.sh --file pages/_posts/my-post.md
37
+ # ./scripts/generate-preview-images.sh --provider openai --verbose
38
+ #
39
+
40
+ set -euo pipefail
41
+
42
+ # Get script directory and source common utilities
43
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
44
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
45
+
46
+ # Load environment variables from .env file if it exists
47
+ if [[ -f "$PROJECT_ROOT/.env" ]]; then
48
+ # Export variables from .env file, overriding any existing values
49
+ while IFS='=' read -r key value; do
50
+ # Skip comments and empty lines
51
+ [[ -z "$key" || "$key" =~ ^# ]] && continue
52
+ # Remove surrounding quotes from value if present
53
+ value="${value%\"}"
54
+ value="${value#\"}"
55
+ value="${value%\'}"
56
+ value="${value#\'}"
57
+ # Export the variable
58
+ export "$key=$value"
59
+ done < "$PROJECT_ROOT/.env"
60
+ fi
61
+
62
+ # Source common library if available
63
+ if [[ -f "$SCRIPT_DIR/lib/common.sh" ]]; then
64
+ source "$SCRIPT_DIR/lib/common.sh"
65
+ else
66
+ # Fallback logging functions
67
+ RED='\033[0;31m'
68
+ GREEN='\033[0;32m'
69
+ YELLOW='\033[1;33m'
70
+ BLUE='\033[0;34m'
71
+ CYAN='\033[0;36m'
72
+ PURPLE='\033[0;35m'
73
+ NC='\033[0m'
74
+
75
+ log() { echo -e "${GREEN}[LOG]${NC} $1"; }
76
+ info() { echo -e "${BLUE}[INFO]${NC} $1"; }
77
+ step() { echo -e "${CYAN}[STEP]${NC} $1"; }
78
+ success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
79
+ warn() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
80
+ error() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; }
81
+ debug() { [[ "${VERBOSE:-false}" == "true" ]] && echo -e "${PURPLE}[DEBUG]${NC} $1" >&2 || true; }
82
+ print_header() {
83
+ echo ""
84
+ echo -e "${CYAN}════════════════════════════════════════════════════════════════${NC}"
85
+ echo -e " ${GREEN}$1${NC}"
86
+ echo -e "${CYAN}════════════════════════════════════════════════════════════════${NC}"
87
+ echo ""
88
+ }
89
+ fi
90
+
91
+ # =============================================================================
92
+ # Configuration Loading
93
+ # =============================================================================
94
+ # Priority: CLI args > Environment variables > _config.yml > Defaults
95
+
96
+ CONFIG_FILE="$PROJECT_ROOT/_config.yml"
97
+
98
+ # Function to read config value from _config.yml using grep (handles YAML anchors)
99
+ read_config() {
100
+ local key="$1"
101
+ local default="$2"
102
+
103
+ if [[ -f "$CONFIG_FILE" ]]; then
104
+ # Use grep to find the key under preview_images section
105
+ # This is more robust than yq for files with anchors
106
+ local in_section=false
107
+ local value=""
108
+
109
+ while IFS= read -r line; do
110
+ # Check if we're entering the preview_images section
111
+ if [[ "$line" =~ ^preview_images: ]]; then
112
+ in_section=true
113
+ continue
114
+ fi
115
+
116
+ # Check if we're leaving the section (new top-level key)
117
+ if [[ "$in_section" == true && "$line" =~ ^[a-zA-Z_]+: && ! "$line" =~ ^[[:space:]] ]]; then
118
+ break
119
+ fi
120
+
121
+ # Look for the key within the section
122
+ if [[ "$in_section" == true && "$line" =~ ^[[:space:]]+${key}[[:space:]]*:[[:space:]]*(.*) ]]; then
123
+ value="${BASH_REMATCH[1]}"
124
+ # First, remove inline comments (only if not inside quotes)
125
+ # Simple approach: if value starts with quote, find closing quote
126
+ if [[ "$value" =~ ^\'([^\']*)\' ]]; then
127
+ value="${BASH_REMATCH[1]}"
128
+ elif [[ "$value" =~ ^\"([^\"]*)\" ]]; then
129
+ value="${BASH_REMATCH[1]}"
130
+ else
131
+ # No quotes, just trim and remove comment
132
+ value="${value%%#*}"
133
+ # Trim whitespace
134
+ value="${value%"${value##*[![:space:]]}"}"
135
+ value="${value#"${value%%[![:space:]]*}"}"
136
+ fi
137
+ if [[ -n "$value" ]]; then
138
+ echo "$value"
139
+ return
140
+ fi
141
+ fi
142
+ done < "$CONFIG_FILE"
143
+ fi
144
+ echo "$default"
145
+ }
146
+
147
+ # Load defaults from _config.yml, with fallbacks
148
+ CONFIG_PROVIDER=$(read_config "provider" "openai")
149
+ CONFIG_MODEL=$(read_config "model" "dall-e-3")
150
+ CONFIG_SIZE=$(read_config "size" "1792x1024")
151
+ CONFIG_QUALITY=$(read_config "quality" "standard")
152
+ CONFIG_STYLE=$(read_config "style" "retro pixel art, 8-bit video game aesthetic, vibrant colors, nostalgic, clean pixel graphics")
153
+ CONFIG_STYLE_MODIFIERS=$(read_config "style_modifiers" "pixelated, retro gaming style, CRT screen glow effect, limited color palette")
154
+ CONFIG_OUTPUT_DIR=$(read_config "output_dir" "assets/images/previews")
155
+
156
+ # Default configuration (env vars override config file)
157
+ DRY_RUN="${DRY_RUN:-false}"
158
+ VERBOSE="${VERBOSE:-false}"
159
+ FORCE="${FORCE:-false}"
160
+ LIST_ONLY="${LIST_ONLY:-false}"
161
+ SPECIFIC_FILE=""
162
+ COLLECTION=""
163
+ AI_PROVIDER="${AI_PROVIDER:-$CONFIG_PROVIDER}"
164
+ OUTPUT_DIR="${OUTPUT_DIR:-$CONFIG_OUTPUT_DIR}"
165
+ IMAGE_STYLE="${IMAGE_STYLE:-$CONFIG_STYLE}"
166
+ IMAGE_STYLE_MODIFIERS="${IMAGE_STYLE_MODIFIERS:-$CONFIG_STYLE_MODIFIERS}"
167
+ IMAGE_SIZE="${IMAGE_SIZE:-$CONFIG_SIZE}"
168
+ IMAGE_QUALITY="${IMAGE_QUALITY:-$CONFIG_QUALITY}"
169
+ IMAGE_MODEL="${IMAGE_MODEL:-$CONFIG_MODEL}"
170
+
171
+ # Counters
172
+ PROCESSED=0
173
+ GENERATED=0
174
+ SKIPPED=0
175
+ ERRORS=0
176
+
177
+ # Print usage
178
+ show_help() {
179
+ cat << 'EOF'
180
+ Usage: generate-preview-images.sh [OPTIONS]
181
+
182
+ AI-powered preview image generator for Jekyll posts and content.
183
+
184
+ OPTIONS:
185
+ -h, --help Show this help message
186
+ -d, --dry-run Preview what would be generated (no changes)
187
+ -v, --verbose Enable verbose output
188
+ -f, --file FILE Process a specific file only
189
+ -c, --collection NAME Process specific collection (posts, quickstart, docs)
190
+ -p, --provider PROVIDER AI provider: openai, stability, local (default: openai)
191
+ --output-dir DIR Output directory for images (default: assets/images/previews)
192
+ --force Regenerate images even if preview exists
193
+ --list-missing Only list files with missing previews
194
+
195
+ ENVIRONMENT VARIABLES:
196
+ OPENAI_API_KEY Required for OpenAI DALL-E provider
197
+ STABILITY_API_KEY Required for Stability AI provider
198
+ IMAGE_STYLE Override style from _config.yml
199
+ IMAGE_SIZE Override size (default: 1792x1024 landscape)
200
+ IMAGE_MODEL OpenAI model (default: dall-e-3)
201
+
202
+ CONFIGURATION:
203
+ Default settings are loaded from _config.yml under 'preview_images' section.
204
+ Environment variables override config file settings.
205
+
206
+ EXAMPLES:
207
+ # List all files missing preview images
208
+ ./scripts/generate-preview-images.sh --list-missing
209
+
210
+ # Dry run to see what would be generated
211
+ ./scripts/generate-preview-images.sh --dry-run --verbose
212
+
213
+ # Generate images for posts collection
214
+ ./scripts/generate-preview-images.sh --collection posts
215
+
216
+ # Generate image for a specific file
217
+ ./scripts/generate-preview-images.sh -f pages/_posts/my-article.md
218
+
219
+ # Force regenerate all images
220
+ ./scripts/generate-preview-images.sh --force
221
+
222
+ EOF
223
+ }
224
+
225
+ # Parse command line arguments
226
+ parse_args() {
227
+ while [[ $# -gt 0 ]]; do
228
+ case $1 in
229
+ -h|--help)
230
+ show_help
231
+ exit 0
232
+ ;;
233
+ -d|--dry-run)
234
+ DRY_RUN="true"
235
+ ;;
236
+ -v|--verbose)
237
+ VERBOSE="true"
238
+ ;;
239
+ -f|--file)
240
+ SPECIFIC_FILE="$2"
241
+ shift
242
+ ;;
243
+ -c|--collection)
244
+ COLLECTION="$2"
245
+ shift
246
+ ;;
247
+ -p|--provider)
248
+ AI_PROVIDER="$2"
249
+ shift
250
+ ;;
251
+ --output-dir)
252
+ OUTPUT_DIR="$2"
253
+ shift
254
+ ;;
255
+ --force)
256
+ FORCE="true"
257
+ ;;
258
+ --list-missing)
259
+ LIST_ONLY="true"
260
+ ;;
261
+ *)
262
+ error "Unknown option: $1. Use --help for usage."
263
+ ;;
264
+ esac
265
+ shift
266
+ done
267
+ }
268
+
269
+ # Validate environment and dependencies
270
+ validate_environment() {
271
+ step "Validating environment..."
272
+
273
+ # Check for required commands
274
+ local required_cmds=("curl" "jq")
275
+ for cmd in "${required_cmds[@]}"; do
276
+ if ! command -v "$cmd" &> /dev/null; then
277
+ error "Required command not found: $cmd"
278
+ fi
279
+ done
280
+
281
+ # Check for YAML parser (prefer yq, fallback to python)
282
+ if command -v yq &> /dev/null; then
283
+ YAML_PARSER="yq"
284
+ debug "Using yq for YAML parsing"
285
+ elif command -v python3 &> /dev/null; then
286
+ YAML_PARSER="python"
287
+ debug "Using python for YAML parsing"
288
+ else
289
+ error "No YAML parser found. Install yq or python3."
290
+ fi
291
+
292
+ # Validate AI provider credentials (unless list-only or dry-run)
293
+ if [[ "$LIST_ONLY" != "true" && "$DRY_RUN" != "true" ]]; then
294
+ case "$AI_PROVIDER" in
295
+ openai)
296
+ if [[ -z "${OPENAI_API_KEY:-}" ]]; then
297
+ error "OPENAI_API_KEY environment variable is required for OpenAI provider"
298
+ fi
299
+ ;;
300
+ stability)
301
+ if [[ -z "${STABILITY_API_KEY:-}" ]]; then
302
+ error "STABILITY_API_KEY environment variable is required for Stability AI provider"
303
+ fi
304
+ ;;
305
+ local)
306
+ info "Using local provider - no API key required"
307
+ ;;
308
+ *)
309
+ error "Unknown AI provider: $AI_PROVIDER"
310
+ ;;
311
+ esac
312
+ fi
313
+
314
+ # Ensure output directory exists
315
+ local full_output_dir="$PROJECT_ROOT/$OUTPUT_DIR"
316
+ if [[ ! -d "$full_output_dir" ]]; then
317
+ if [[ "$DRY_RUN" != "true" ]]; then
318
+ mkdir -p "$full_output_dir"
319
+ debug "Created output directory: $full_output_dir"
320
+ else
321
+ debug "Would create output directory: $full_output_dir"
322
+ fi
323
+ fi
324
+
325
+ success "Environment validation passed"
326
+ }
327
+
328
+ # Extract front matter from a markdown file
329
+ extract_front_matter() {
330
+ local file="$1"
331
+
332
+ # Extract content between --- markers
333
+ sed -n '/^---$/,/^---$/p' "$file" | sed '1d;$d'
334
+ }
335
+
336
+ # Get YAML value using available parser
337
+ get_yaml_value() {
338
+ local yaml="$1"
339
+ local key="$2"
340
+ local result=""
341
+
342
+ if [[ "$YAML_PARSER" == "yq" ]]; then
343
+ # yq v4 syntax - read from stdin and get specific key
344
+ result=$(echo "$yaml" | yq eval ".$key" - 2>/dev/null | head -1)
345
+ # Filter out null values
346
+ if [[ "$result" == "null" || -z "$result" ]]; then
347
+ result=""
348
+ fi
349
+ else
350
+ # Python fallback
351
+ result=$(python3 -c "
352
+ import yaml
353
+ import sys
354
+ try:
355
+ data = yaml.safe_load('''$yaml''')
356
+ if data and '$key' in data:
357
+ val = data['$key']
358
+ if val is not None:
359
+ print(val)
360
+ except:
361
+ pass
362
+ " 2>/dev/null || echo "")
363
+ fi
364
+
365
+ echo "$result"
366
+ }
367
+
368
+ # Extract post content (without front matter)
369
+ extract_content() {
370
+ local file="$1"
371
+
372
+ # Skip front matter and get content
373
+ awk '/^---$/ { if (++count == 2) found=1; next } found { print }' "$file"
374
+ }
375
+
376
+ # Check if preview image exists
377
+ check_preview_exists() {
378
+ local preview_path="$1"
379
+
380
+ if [[ -z "$preview_path" ]]; then
381
+ return 1
382
+ fi
383
+
384
+ # Handle paths starting with /
385
+ local clean_path="${preview_path#/}"
386
+ local full_path="$PROJECT_ROOT/$clean_path"
387
+
388
+ # Also check in assets/images
389
+ if [[ ! -f "$full_path" ]]; then
390
+ full_path="$PROJECT_ROOT/assets/$clean_path"
391
+ fi
392
+
393
+ [[ -f "$full_path" ]]
394
+ }
395
+
396
+ # Generate image prompt from content
397
+ generate_prompt() {
398
+ local title="$1"
399
+ local description="$2"
400
+ local categories="$3"
401
+ local content="$4"
402
+
403
+ # Build a meaningful prompt
404
+ local prompt="Create a blog preview banner image for an article titled '$title'."
405
+
406
+ if [[ -n "$description" ]]; then
407
+ prompt="$prompt The article is about: $description."
408
+ fi
409
+
410
+ if [[ -n "$categories" ]]; then
411
+ prompt="$prompt Categories: $categories."
412
+ fi
413
+
414
+ # Extract key themes from content (first 500 chars)
415
+ local content_excerpt="${content:0:500}"
416
+ if [[ -n "$content_excerpt" ]]; then
417
+ prompt="$prompt Key themes from content: $content_excerpt"
418
+ fi
419
+
420
+ # Add style instructions and modifiers
421
+ prompt="$prompt Art style: $IMAGE_STYLE."
422
+ if [[ -n "$IMAGE_STYLE_MODIFIERS" ]]; then
423
+ prompt="$prompt Additional style: $IMAGE_STYLE_MODIFIERS."
424
+ fi
425
+ prompt="$prompt The image should be suitable as a wide blog header/banner image with clean composition. No text or words in the image."
426
+
427
+ echo "$prompt"
428
+ }
429
+
430
+ # Generate image using OpenAI DALL-E
431
+ generate_image_openai() {
432
+ local prompt="$1"
433
+ local output_file="$2"
434
+
435
+ debug "Generating image with OpenAI DALL-E..."
436
+ debug "Prompt: ${prompt:0:200}..."
437
+
438
+ local response
439
+ response=$(curl -s -X POST "https://api.openai.com/v1/images/generations" \
440
+ -H "Authorization: Bearer $OPENAI_API_KEY" \
441
+ -H "Content-Type: application/json" \
442
+ -d "{
443
+ \"model\": \"$IMAGE_MODEL\",
444
+ \"prompt\": $(echo "$prompt" | jq -Rs .),
445
+ \"n\": 1,
446
+ \"size\": \"$IMAGE_SIZE\",
447
+ \"quality\": \"$IMAGE_QUALITY\"
448
+ }")
449
+
450
+ # Check for errors
451
+ local error_msg
452
+ error_msg=$(echo "$response" | jq -r '.error.message // empty')
453
+ if [[ -n "$error_msg" ]]; then
454
+ warn "OpenAI API error: $error_msg"
455
+ return 1
456
+ fi
457
+
458
+ # Extract image URL
459
+ local image_url
460
+ image_url=$(echo "$response" | jq -r '.data[0].url // empty')
461
+
462
+ if [[ -z "$image_url" ]]; then
463
+ warn "No image URL in response"
464
+ debug "Response: $response"
465
+ return 1
466
+ fi
467
+
468
+ # Download image
469
+ debug "Downloading image from: $image_url"
470
+ curl -s -o "$output_file" "$image_url"
471
+
472
+ if [[ -f "$output_file" ]]; then
473
+ success "Image saved to: $output_file"
474
+ return 0
475
+ else
476
+ warn "Failed to save image"
477
+ return 1
478
+ fi
479
+ }
480
+
481
+ # Generate image using Stability AI
482
+ generate_image_stability() {
483
+ local prompt="$1"
484
+ local output_file="$2"
485
+
486
+ debug "Generating image with Stability AI..."
487
+
488
+ local response
489
+ response=$(curl -s -X POST "https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image" \
490
+ -H "Authorization: Bearer $STABILITY_API_KEY" \
491
+ -H "Content-Type: application/json" \
492
+ -d "{
493
+ \"text_prompts\": [{\"text\": $(echo "$prompt" | jq -Rs .)}],
494
+ \"cfg_scale\": 7,
495
+ \"height\": 1024,
496
+ \"width\": 1024,
497
+ \"samples\": 1,
498
+ \"steps\": 30
499
+ }")
500
+
501
+ # Check for errors
502
+ local error_msg
503
+ error_msg=$(echo "$response" | jq -r '.message // empty')
504
+ if [[ -n "$error_msg" ]]; then
505
+ warn "Stability API error: $error_msg"
506
+ return 1
507
+ fi
508
+
509
+ # Extract and decode base64 image
510
+ local base64_image
511
+ base64_image=$(echo "$response" | jq -r '.artifacts[0].base64 // empty')
512
+
513
+ if [[ -z "$base64_image" ]]; then
514
+ warn "No image data in response"
515
+ return 1
516
+ fi
517
+
518
+ echo "$base64_image" | base64 -d > "$output_file"
519
+
520
+ if [[ -f "$output_file" ]]; then
521
+ success "Image saved to: $output_file"
522
+ return 0
523
+ else
524
+ warn "Failed to save image"
525
+ return 1
526
+ fi
527
+ }
528
+
529
+ # Generate placeholder for local provider (for testing)
530
+ generate_image_local() {
531
+ local prompt="$1"
532
+ local output_file="$2"
533
+
534
+ warn "Local provider: No actual image generation. Creating placeholder..."
535
+ debug "Would generate image with prompt: ${prompt:0:200}..."
536
+
537
+ # Create a simple placeholder file
538
+ echo "PLACEHOLDER: $prompt" > "$output_file.txt"
539
+
540
+ info "Placeholder created: $output_file.txt"
541
+ return 0
542
+ }
543
+
544
+ # Generate image using selected provider
545
+ generate_image() {
546
+ local prompt="$1"
547
+ local output_file="$2"
548
+
549
+ case "$AI_PROVIDER" in
550
+ openai)
551
+ generate_image_openai "$prompt" "$output_file"
552
+ ;;
553
+ stability)
554
+ generate_image_stability "$prompt" "$output_file"
555
+ ;;
556
+ local)
557
+ generate_image_local "$prompt" "$output_file"
558
+ ;;
559
+ *)
560
+ error "Unknown provider: $AI_PROVIDER"
561
+ ;;
562
+ esac
563
+ }
564
+
565
+ # Update front matter with new preview path
566
+ update_front_matter() {
567
+ local file="$1"
568
+ local preview_path="$2"
569
+
570
+ debug "Updating front matter in: $file"
571
+
572
+ if [[ "$DRY_RUN" == "true" ]]; then
573
+ info "[DRY RUN] Would update preview in $file to: $preview_path"
574
+ return 0
575
+ fi
576
+
577
+ # Create backup
578
+ cp "$file" "$file.bak"
579
+
580
+ # Always use sed for reliability (yq can fail on complex YAML)
581
+ # Check if preview field exists
582
+ if grep -q "^preview:" "$file"; then
583
+ # Update existing preview field using sed
584
+ if [[ "$(uname)" == "Darwin" ]]; then
585
+ # macOS sed requires empty string for -i
586
+ sed -i '' "s|^preview:.*|preview: $preview_path|" "$file"
587
+ else
588
+ sed -i "s|^preview:.*|preview: $preview_path|" "$file"
589
+ fi
590
+ else
591
+ # Add preview field after description or title
592
+ if grep -q "^description:" "$file"; then
593
+ if [[ "$(uname)" == "Darwin" ]]; then
594
+ sed -i '' "/^description:/a\\
595
+ preview: $preview_path" "$file"
596
+ else
597
+ sed -i "/^description:/a\\
598
+ preview: $preview_path" "$file"
599
+ fi
600
+ else
601
+ if [[ "$(uname)" == "Darwin" ]]; then
602
+ sed -i '' "/^title:/a\\
603
+ preview: $preview_path" "$file"
604
+ else
605
+ sed -i "/^title:/a\\
606
+ preview: $preview_path" "$file"
607
+ fi
608
+ fi
609
+ fi
610
+
611
+ # Remove backup on success
612
+ rm -f "$file.bak"
613
+
614
+ success "Updated front matter with preview: $preview_path"
615
+ }
616
+
617
+ # Process a single file
618
+ process_file() {
619
+ local file="$1"
620
+
621
+ PROCESSED=$((PROCESSED + 1))
622
+
623
+ debug "Processing file: $file"
624
+
625
+ # Extract front matter
626
+ local front_matter
627
+ front_matter=$(extract_front_matter "$file")
628
+
629
+ if [[ -z "$front_matter" ]]; then
630
+ debug "No front matter found in: $file"
631
+ SKIPPED=$((SKIPPED + 1))
632
+ return 0
633
+ fi
634
+
635
+ # Get metadata
636
+ local title description categories preview
637
+ title=$(get_yaml_value "$front_matter" "title")
638
+ description=$(get_yaml_value "$front_matter" "description")
639
+ categories=$(get_yaml_value "$front_matter" "categories")
640
+ preview=$(get_yaml_value "$front_matter" "preview")
641
+
642
+ debug "Title: $title"
643
+ debug "Preview: $preview"
644
+
645
+ # Check if preview exists and is valid
646
+ if [[ -n "$preview" ]] && check_preview_exists "$preview"; then
647
+ if [[ "$FORCE" != "true" ]]; then
648
+ debug "Preview already exists and is valid: $preview"
649
+ SKIPPED=$((SKIPPED + 1))
650
+ return 0
651
+ else
652
+ info "Force mode: regenerating preview for $title"
653
+ fi
654
+ fi
655
+
656
+ # Report missing preview
657
+ if [[ "$LIST_ONLY" == "true" ]]; then
658
+ echo -e "${YELLOW}Missing preview:${NC} $file"
659
+ echo -e " Title: $title"
660
+ if [[ -n "$preview" ]]; then
661
+ echo -e " Current preview (not found): $preview"
662
+ fi
663
+ echo ""
664
+ return 0
665
+ fi
666
+
667
+ info "Generating preview for: $title"
668
+
669
+ # Generate filename from title
670
+ local safe_filename
671
+ safe_filename=$(echo "$title" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//')
672
+ safe_filename="${safe_filename:0:50}" # Limit length
673
+
674
+ local output_file="$PROJECT_ROOT/$OUTPUT_DIR/${safe_filename}.png"
675
+ # Preview path should NOT include /assets/ prefix since the template adds it
676
+ local preview_path="/images/previews/${safe_filename}.png"
677
+
678
+ # Extract content for prompt generation
679
+ local content
680
+ content=$(extract_content "$file")
681
+
682
+ # Generate prompt
683
+ local prompt
684
+ prompt=$(generate_prompt "$title" "$description" "$categories" "$content")
685
+
686
+ debug "Generated prompt: ${prompt:0:500}..."
687
+
688
+ if [[ "$DRY_RUN" == "true" ]]; then
689
+ info "[DRY RUN] Would generate image:"
690
+ echo " Output: $output_file"
691
+ echo " Preview path: $preview_path"
692
+ echo " Prompt: ${prompt:0:400}..."
693
+ echo ""
694
+ GENERATED=$((GENERATED + 1))
695
+ return 0
696
+ fi
697
+
698
+ # Generate image
699
+ if generate_image "$prompt" "$output_file"; then
700
+ # Update front matter with new preview path
701
+ update_front_matter "$file" "$preview_path"
702
+ GENERATED=$((GENERATED + 1))
703
+ else
704
+ warn "Failed to generate image for: $title"
705
+ ERRORS=$((ERRORS + 1))
706
+ fi
707
+ }
708
+
709
+ # Find and process content files
710
+ process_collection() {
711
+ local collection_path="$1"
712
+ local pattern="${2:-*.md}"
713
+
714
+ debug "Processing collection: $collection_path with pattern: $pattern"
715
+
716
+ if [[ ! -d "$collection_path" ]]; then
717
+ warn "Collection directory not found: $collection_path"
718
+ return 1
719
+ fi
720
+
721
+ while IFS= read -r -d '' file; do
722
+ process_file "$file"
723
+ done < <(find "$collection_path" -name "$pattern" -type f -print0)
724
+ }
725
+
726
+ # Main function
727
+ main() {
728
+ print_header "🎨 Preview Image Generator"
729
+
730
+ # Validate environment
731
+ validate_environment
732
+
733
+ # Display configuration
734
+ info "Configuration:"
735
+ echo " AI Provider: $AI_PROVIDER"
736
+ echo " Output Dir: $OUTPUT_DIR"
737
+ echo " Image Size: $IMAGE_SIZE"
738
+ echo " Dry Run: $DRY_RUN"
739
+ echo " Force: $FORCE"
740
+ echo " List Only: $LIST_ONLY"
741
+ echo ""
742
+
743
+ # Get configured collections from _config.yml
744
+ get_configured_collections() {
745
+ local collections=()
746
+ local in_preview_images=false
747
+ local in_collections=false
748
+
749
+ while IFS= read -r line; do
750
+ if [[ "$line" =~ ^preview_images: ]]; then
751
+ in_preview_images=true
752
+ continue
753
+ fi
754
+ if [[ "$in_preview_images" == true && "$line" =~ ^[[:space:]]+collections: ]]; then
755
+ in_collections=true
756
+ continue
757
+ fi
758
+ if [[ "$in_collections" == true && "$line" =~ ^[[:space:]]+- ]]; then
759
+ local collection="${line#*- }"
760
+ collection="${collection%%#*}"
761
+ collection="${collection%"${collection##*[![:space:]]}"}"
762
+ collections+=("$collection")
763
+ elif [[ "$in_collections" == true && ! "$line" =~ ^[[:space:]]+- && ! "$line" =~ ^[[:space:]]*$ ]]; then
764
+ break
765
+ fi
766
+ if [[ "$in_preview_images" == true && "$line" =~ ^[a-zA-Z_]+: && ! "$line" =~ ^[[:space:]] ]]; then
767
+ break
768
+ fi
769
+ done < "$CONFIG_FILE"
770
+
771
+ if [[ ${#collections[@]} -eq 0 ]]; then
772
+ collections=("posts" "quickstart" "docs")
773
+ fi
774
+
775
+ echo "${collections[@]}"
776
+ }
777
+
778
+ # Process a collection by name
779
+ process_collection_by_name() {
780
+ local name="$1"
781
+ local path="$PROJECT_ROOT/pages/_${name}"
782
+
783
+ if [[ -d "$path" ]]; then
784
+ step "Processing ${name} collection..."
785
+ process_collection "$path"
786
+ else
787
+ warn "Collection directory not found: $path"
788
+ fi
789
+ }
790
+
791
+ # Process files
792
+ if [[ -n "$SPECIFIC_FILE" ]]; then
793
+ # Process single file
794
+ if [[ ! -f "$PROJECT_ROOT/$SPECIFIC_FILE" ]]; then
795
+ error "File not found: $SPECIFIC_FILE"
796
+ fi
797
+ process_file "$PROJECT_ROOT/$SPECIFIC_FILE"
798
+ elif [[ -n "$COLLECTION" ]]; then
799
+ # Process specific collection
800
+ if [[ "$COLLECTION" == "all" ]]; then
801
+ step "Processing all configured collections..."
802
+ for col in $(get_configured_collections); do
803
+ process_collection_by_name "$col"
804
+ done
805
+ else
806
+ # Check if collection directory exists
807
+ local collection_path="$PROJECT_ROOT/pages/_${COLLECTION}"
808
+ if [[ -d "$collection_path" ]]; then
809
+ process_collection_by_name "$COLLECTION"
810
+ else
811
+ local available=$(get_configured_collections | tr ' ' ', ')
812
+ error "Unknown collection: $COLLECTION. Available: $available, all"
813
+ fi
814
+ fi
815
+ else
816
+ # Process all configured collections by default
817
+ step "Processing all configured collections..."
818
+ for col in $(get_configured_collections); do
819
+ process_collection_by_name "$col"
820
+ done
821
+ fi
822
+
823
+ # Print summary
824
+ echo ""
825
+ print_header "📊 Summary"
826
+ echo " Files processed: $PROCESSED"
827
+ echo " Images generated: $GENERATED"
828
+ echo " Files skipped: $SKIPPED"
829
+ echo " Errors: $ERRORS"
830
+ echo ""
831
+
832
+ if [[ "$DRY_RUN" == "true" ]]; then
833
+ info "This was a dry run. No actual changes were made."
834
+ fi
835
+
836
+ if [[ "$ERRORS" -gt 0 ]]; then
837
+ warn "Some files had errors. Check the output above."
838
+ exit 1
839
+ fi
840
+
841
+ success "Preview image generation complete!"
842
+ }
843
+
844
+ # Parse arguments and run
845
+ parse_args "$@"
846
+ main