<# .SYNOPSIS Build script for static website generator .DESCRIPTION This script processes Markdown content files and generates a static HTML website. It dynamically detects content sections, processes blog posts, and generates navigation menus. The output is a fully functional static website in the dist/ folder. Features: - Dynamic section detection from content/ folder - Markdown to HTML conversion - Blog post processing with front matter - Automatic image handling - Responsive navigation generation .REQUIREMENTS - PowerShell 7+ (for better UTF-8 handling) - Markdown files in content/ folder - Images in images/ folder .NOTES Output: Generated files are placed in dist/ directory #> # ============================================================================ # CONFIGURATION # ============================================================================ # Get the directory where this script is located $projectRoot = $PSScriptRoot # Define source and output directories $sourceDir = Join-Path $projectRoot "content" # Markdown content source $templateFile = Join-Path $projectRoot "template.html" # Temporary template file $imagesDir = Join-Path $projectRoot "images" # Source images directory # Output directory - can be overridden with OUTPUT_DIR environment variable $outputDir = if ($env:OUTPUT_DIR) { Join-Path $projectRoot $env:OUTPUT_DIR } else { Join-Path $projectRoot "dist" # Default output directory } # Get current year for copyright $year = (Get-Date).Year # Site configuration - customize these values $siteTitle = "My Website" $siteHeader = "Welcome to My Website" $footerText = "My Website" # Temporary output files (will be moved to dist/ at the end) $tempOutputFile = Join-Path $projectRoot "index.html" $tempBlogOutputFile = Join-Path $projectRoot "blog.html" # ============================================================================ # HELPER FUNCTIONS # ============================================================================ <# .SYNOPSIS Finds the background image file in the images directory .DESCRIPTION Searches for any file matching "background.*" pattern and returns the relative path for use in CSS background-image property. .PARAMETER imagesDir Path to the images directory .OUTPUTS String - Relative path to background image (e.g., "images/background.jpg") Empty string if no background image found #> function Get-BackgroundImagePath { param ( [string]$imagesDir ) $backgroundImages = Get-ChildItem (Join-Path $imagesDir "background.*") -ErrorAction SilentlyContinue if ($backgroundImages.Count -gt 0) { return "images/" + $backgroundImages[0].Name } return "" # Return empty string if no background image found } # Create output directory if it doesn't exist if (-not (Test-Path $outputDir)) { New-Item -ItemType Directory -Path $outputDir | Out-Null } # Copy static files to dist first Copy-Item "styles.css" -Destination $outputDir -Force if (Test-Path "images") { Copy-Item "images" -Destination $outputDir -Recurse -Force } # Set output files after copying $outputFile = Join-Path $outputDir "index.html" $blogOutputFile = Join-Path $outputDir "blog.html" # Get background image path $backgroundPath = Get-BackgroundImagePath -imagesDir $imagesDir $backgroundStyle = if ($backgroundPath) { "background-image: url('$backgroundPath');" } else { "" # No background image } <# .SYNOPSIS Generates navigation menu HTML from section IDs .DESCRIPTION Creates an unordered list of navigation links for all detected sections, plus a Blog link. Section names are capitalized for display. .PARAMETER sections Array of section IDs (e.g., @("about", "services", "contact")) .OUTPUTS String - HTML navigation menu items #> function Get-NavigationHtml { param ( [array]$sections ) $navItems = @() foreach ($section in $sections) { # Capitalize first letter of section name $sectionName = $section.Substring(0,1).ToUpper() + $section.Substring(1) $navItems += "
  • $sectionName
  • " } # Always add Blog link $navItems += "
  • Blog
  • " return $navItems -join "`n" } <# .SYNOPSIS Finds all numbered images for a section .DESCRIPTION Searches for images matching the pattern {sectionId}{number}.* (e.g., "about1.webp", "about2.jpg", "services3.png") and returns them as a hashtable indexed by number. .PARAMETER sectionId The section identifier (e.g., "about", "services") .OUTPUTS Hashtable - Images indexed by number (e.g., @{1="about1.webp"; 2="about2.jpg"}) .EXAMPLE Get-SectionImages -sectionId "about" Returns: @{1="about1.webp"; 2="about2.jpg"; 3="about3.jpg"} #> function Get-SectionImages { param ( [string]$sectionId ) # Get all files matching the pattern sectionX.* (e.g., about1.webp, about2.png) $images = Get-ChildItem (Join-Path $imagesDir "$sectionId[0-9]*.*") -ErrorAction SilentlyContinue # Create a hashtable to store images by number $imagesByNumber = @{} foreach ($image in $images) { # Extract the number from the filename using regex # Matches: sectionId followed by one or more digits if ($image.BaseName -match "$sectionId(\d+)") { $number = [int]$matches[1] $imagesByNumber[$number] = $image.Name } } return $imagesByNumber } <# .SYNOPSIS Converts a Markdown file to an HTML section .DESCRIPTION Processes a Markdown file and converts it to a full HTML section with: - Section heading extracted from first # heading - Content blocks separated by "---" dividers - Alternating left/right image alignment - Automatic image assignment from images directory .PARAMETER file Path to the Markdown file to convert .PARAMETER sectionId The section ID to use (e.g., "about", "services") .OUTPUTS String - Complete HTML section markup .NOTES Markdown format: - First line: # Section Title - Content blocks separated by "---" - Each block becomes a section-content div with text and image #> function Convert-MarkdownToSection { param ( [string]$file, [string]$sectionId ) # Read markdown content and split by "---" dividers $content = Get-Content $file -Raw $sections = $content -split "---" | ForEach-Object { $_.Trim() } # Get available numbered images for this section (e.g., about1.webp, about2.jpg) $sectionImages = Get-SectionImages -sectionId $sectionId # Extract title from first section's # heading, or capitalize section ID as fallback $title = if ($sections[0] -match "^#\s+(.+)$") { $matches[1] } else { $sectionId.Substring(0,1).ToUpper() + $sectionId.Substring(1) } # Start building section HTML $sectionHtml = @"

    $title

    "@ # Process each content section (separated by "---") for ($i = 0; $i -lt $sections.Count; $i++) { $text = $sections[$i] # Image selection priority: # 1. Explicit markdown image syntax: ![alt](path) # 2. Numbered section image (e.g., about1.webp for first block) # 3. First available numbered image if exact match not found # 4. Default fallback: sectionId.webp $imageMatch = $text -match '!\[([^\]]*)\]\(([^\)]+)\)' $imageSrc = if ($imageMatch) { $text = $text -replace '!\[([^\]]*)\]\(([^\)]+)\)', '' # Remove image markdown from text $matches[2] # Use specified image path } elseif ($sectionImages.ContainsKey($i + 1)) { "images/" + $sectionImages[$i + 1] # Use numbered section image (e.g., about1.webp) } elseif ($sectionImages.Count -gt 0) { # Use first available image if numbered image doesn't exist $sortedKeys = $sectionImages.Keys | Sort-Object $firstKey = $sortedKeys[0] "images/" + $sectionImages[$firstKey] } else { "images/$sectionId.webp" # Default fallback } # Generate alt text: use markdown alt text if provided, otherwise generate descriptive text $imageAlt = if ($imageMatch) { $matches[1] # Use specified alt text from markdown } else { "$sectionId image $($i + 1)" # Default alt text } # Remove title heading from first section (already extracted above) $text = $text -replace "^#\s+.+`n", "" # Convert markdown links to HTML anchor tags $text = $text -replace '\[([^\]]+)\]\(([^\)]+)\)', '$1' # Alternate alignment: even indices = left-align (image on right), odd = right-align (image on left) $alignment = if ($i % 2 -eq 0) { "left-align" } else { "right-align" } $sectionHtml += @"

    $text

    $imageAlt
    "@ } $sectionHtml += @"
    "@ return $sectionHtml } # ============================================================================ # MAIN BUILD PROCESS - INDEX PAGE # ============================================================================ # Dynamically detect all .md files in content folder (excluding blog subdirectory) # Each .md file becomes a section on the index page $contentHtml = "" $markdownFiles = Get-ChildItem -Path $sourceDir -Filter "*.md" -File | Where-Object { $_.DirectoryName -eq $sourceDir } $sectionIds = @() Write-Host "Found $($markdownFiles.Count) section markdown files:" foreach ($file in $markdownFiles) { $sectionId = $file.BaseName # Use filename (without .md) as section ID $sectionIds += $sectionId Write-Host " - $sectionId" # Convert each markdown file to an HTML section $contentHtml += Convert-MarkdownToSection -file $file.FullName -sectionId $sectionId } # Generate navigation menu HTML from detected sections $navigationHtml = Get-NavigationHtml -sections $sectionIds # Create HTML template with placeholders $template = @" $siteTitle

    $siteHeader

    {{content}}
    "@ # Replace template placeholders with actual content $finalHtml = $template -replace "{{content}}", $contentHtml -replace "{{navigation}}", $navigationHtml $finalHtml | Out-File $tempOutputFile -Encoding utf8 Write-Host "Building site from $projectRoot" Write-Host "Content directory: $sourceDir" Write-Host "Images directory: $imagesDir" Write-Host "Build complete! Output saved to $tempOutputFile" # ============================================================================ # BLOG BUILD PROCESS # ============================================================================ # Blog posts are stored in content/blog/ subdirectory $blogDir = Join-Path $sourceDir "blog" $blogOutputFile = Join-Path $outputDir "blog.html" Write-Host "building blog" Write-Host "Blog dir: $blogDir" Write-Host "Blog output file: $blogOutputFile" # Blog template $blogTemplate = @" $siteTitle - Blog

    $siteTitle

    {{content}}
    {{pagination}}
    "@ # Check if blog directory exists Write-Host "Building blog from: $blogDir" if (Test-Path $blogDir) { Write-Host "Blog directory exists" $files = Get-ChildItem (Join-Path $blogDir "*.md") Write-Host "Found blog files: $($files.Count)" $files | ForEach-Object { Write-Host " - $($_.Name)" } } else { Write-Host "Blog directory does not exist at: $blogDir" # Create the directory New-Item -ItemType Directory -Path $blogDir -Force Write-Host "Created blog directory" } <# .SYNOPSIS Finds the image file for a blog post .DESCRIPTION Searches for blog post images using multiple strategies: 1. Image name specified in front matter 2. Post filename (without extension) .PARAMETER imageName Image name from front matter (e.g., "blog1") .PARAMETER postFileName Name of the blog post file (e.g., "ai-tools.md") .PARAMETER imagesDir Path to images directory .OUTPUTS String - Relative path to image (e.g., "images/blog1.webp") $null if no image found #> function Get-BlogPostImage { param ( [string]$imageName, [string]$postFileName, [string]$imagesDir ) # Build search list: try specified name first, then post filename $searchNames = @() if ($imageName) { $searchNames += $imageName } $postBaseName = [System.IO.Path]::GetFileNameWithoutExtension($postFileName) $searchNames += $postBaseName # Search for image files matching any of the names foreach ($name in $searchNames) { $imageFiles = Get-ChildItem (Join-Path $imagesDir "$name.*") -ErrorAction SilentlyContinue if ($imageFiles.Count -gt 0) { return "images/" + $imageFiles[0].Name } } return $null } <# .SYNOPSIS Converts Markdown text to HTML with proper formatting .DESCRIPTION Processes Markdown syntax and converts to HTML: - Headings (#, ##, ###, ####) - Links [text](url) - Lists (bulleted and numbered) - Code blocks and inline code - Paragraphs .PARAMETER markdown Markdown text to convert .OUTPUTS String - HTML formatted text #> function Convert-MarkdownToHtml { param ( [string]$markdown ) if ([string]::IsNullOrWhiteSpace($markdown)) { return "" } $html = $markdown # Convert code blocks first (before other processing) $html = $html -replace '(?ms)```(\w+)?\r?\n(.*?)```', '
    $2
    ' # Convert inline code (`code`) $html = $html -replace '`([^`]+)`', '$1' # Convert headings (##, ###, etc.) $html = $html -replace '(?m)^####\s+(.+)$', '

    $1

    ' $html = $html -replace '(?m)^###\s+(.+)$', '

    $1

    ' $html = $html -replace '(?m)^##\s+(.+)$', '

    $1

    ' $html = $html -replace '(?m)^#\s+(.+)$', '

    $1

    ' # Convert markdown links [text](url) $html = $html -replace '\[([^\]]+)\]\(([^\)]+)\)', '$1' # Process line by line to handle lists properly $lines = $html -split "`r?`n" $output = @() $inList = $false $listType = "" $listItems = @() foreach ($line in $lines) { $trimmed = $line.Trim() # Check for numbered list if ($trimmed -match '^\d+\.\s+(.+)$') { if (-not $inList -or $listType -ne "ol") { # Close previous list if exists if ($inList) { $output += "" } $inList = $true $listType = "ol" $listItems = @() } $listItems += "
  • $($matches[1])
  • " } # Check for bullet list elseif ($trimmed -match '^[-*]\s+(.+)$') { if (-not $inList -or $listType -ne "ul") { # Close previous list if exists if ($inList) { $output += "" } $inList = $true $listType = "ul" $listItems = @() } $listItems += "
  • $($matches[1])
  • " } # Empty line - close list if open elseif ([string]::IsNullOrWhiteSpace($trimmed)) { if ($inList) { $output += "<$listType>" + ($listItems -join "`n") + "" $inList = $false $listType = "" $listItems = @() } $output += "" } # Regular content line else { # Close list if open if ($inList) { $output += "<$listType>" + ($listItems -join "`n") + "" $inList = $false $listType = "" $listItems = @() } $output += $trimmed } } # Close any open list if ($inList) { $output += "<$listType>" + ($listItems -join "`n") + "" } # Join lines and split into paragraphs $joined = $output -join "`n" $paragraphs = $joined -split "`n`n" | Where-Object { $_.Trim() -ne "" } $processedParagraphs = @() foreach ($para in $paragraphs) { $para = $para.Trim() if ($para -ne "") { # Don't wrap if it's already a block element if ($para -match '^<(pre|h[1-4]|ul|ol|p)') { $processedParagraphs += $para } else { $processedParagraphs += "

    $para

    " } } } return $processedParagraphs -join "`n" } <# .SYNOPSIS Converts a blog post Markdown file to HTML .DESCRIPTION Processes blog posts with front matter (YAML-like metadata between --- markers): - Extracts title, date, author, tags, image - Converts markdown content to HTML - Generates complete blog post article HTML .PARAMETER file Path to blog post Markdown file .PARAMETER imagesDir Path to images directory for blog post images .OUTPUTS String - Complete HTML article markup .NOTES Front matter format: --- title: Post Title date: YYYY-MM-DD author: Author Name tags: [tag1, tag2, tag3] image: imageName --- #> function Convert-BlogPostToHtml { param ( [string]$file, [string]$imagesDir ) Write-Host "Converting file: $file" $content = Get-Content $file -Raw Write-Host "Content length: $($content.Length)" # Updated regex pattern to match exactly three dashes if ($content -match "(?ms)^---[\r\n](.*?)[\r\n]---[\r\n](.*)$") { Write-Host "Found front matter" $frontMatter = $matches[1] $content = $matches[2].Trim() Write-Host "Front matter: $frontMatter" Write-Host "Content after front matter: $content" # Parse front matter into hashtable $metadata = @{} foreach ($line in ($frontMatter -split "`n")) { if ($line -match "^(\w+):\s*(.*)$") { $key = $matches[1] $value = $matches[2].Trim() if ($key -eq "tags") { $value = $value.Trim("[]").Split(",").ForEach({ $_.Trim() }) } $metadata[$key] = $value Write-Host "Parsed metadata: $key = $value" } } # Find blog post image $imagePath = Get-BlogPostImage -imageName $metadata.image -postFileName (Split-Path $file -Leaf) -imagesDir $imagesDir $imageHtml = if ($imagePath) { "
    `"$($metadata.title)`"
    " } else { "" } # Convert tags to HTML $tagsHtml = $metadata.tags | ForEach-Object { "$_" } # Process content sections with proper markdown formatting $sections = $content -split "---" | ForEach-Object { $_.Trim() } $processedContent = $sections | ForEach-Object { if ($_.Trim() -ne "") { Convert-MarkdownToHtml -markdown $_ } } | Where-Object { $_ -ne "" } $html = @"

    $($metadata.title)

    $imageHtml
    $($processedContent -join "`n")
    "@ Write-Host "Generated HTML length: $($html.Length)" return $html } Write-Host "No front matter found in file" return "" } # Process blog posts: read all .md files, extract dates, and sort by date (newest first) $blogPosts = Get-ChildItem (Join-Path $blogDir "*.md") | ForEach-Object { Write-Host "Processing blog post: $($_.Name)" $content = Get-Content $_.FullName -Raw Write-Host "Content read: $($content.Length) characters" # Updated regex pattern to match exactly three dashes if ($content -match "(?ms)^---[\r\n](.*?)[\r\n]---") { Write-Host "Found front matter in $($_.Name)" $frontMatter = $matches[1] Write-Host "Front matter: $frontMatter" if ($frontMatter -match "date:\s*(.*)") { Write-Host "Found date: $($matches[1])" $date = [datetime]::Parse($matches[1]) @{ File = $_ Date = $date } } else { Write-Host "No date found in front matter" } } else { Write-Host "No front matter found in $($_.Name)" } } | Sort-Object { $_.Date } -Descending Write-Host "Sorted blog posts: $($blogPosts.Count)" # Convert each blog post to HTML and combine $blogHtml = "" foreach ($post in $blogPosts) { Write-Host "Converting blog post to HTML: $($post.File.Name)" $html = Convert-BlogPostToHtml -file $post.File.FullName -imagesDir $imagesDir Write-Host "Generated HTML length: $($html.Length)" $blogHtml += $html } Write-Host "Total blog HTML length: $($blogHtml.Length)" # Generate blog navigation (same as main page) $blogNavHtml = Get-NavigationHtml -sections $sectionIds # Generate final blog page HTML by replacing template placeholders $finalBlogHtml = $blogTemplate -replace "{{content}}", $blogHtml -replace "{{pagination}}", "" -replace "{{navigation}}", $blogNavHtml $finalBlogHtml | Out-File $tempBlogOutputFile -Encoding utf8 Write-Host "Blog build complete" # ============================================================================ # FINAL OUTPUT - COPY FILES TO DIST DIRECTORY # ============================================================================ # Create output directory if it doesn't exist if (-not (Test-Path $outputDir)) { New-Item -ItemType Directory -Path $outputDir | Out-Null } # Copy all built files to dist directory Copy-Item $tempOutputFile -Destination (Join-Path $outputDir "index.html") -Force Copy-Item $tempBlogOutputFile -Destination (Join-Path $outputDir "blog.html") -Force Copy-Item "styles.css" -Destination $outputDir -Force # Copy images directory recursively if (Test-Path "images") { Copy-Item "images" -Destination $outputDir -Recurse -Force } # Clean up temporary files created during build Remove-Item $tempOutputFile -Force -ErrorAction SilentlyContinue Remove-Item $tempBlogOutputFile -Force -ErrorAction SilentlyContinue Write-Host "Files copied to dist directory" Write-Host "Build complete! Website is ready in: $outputDir"