psturtle.com/build.ps1

<#
.SYNOPSIS
    Builds the website.
.DESCRIPTION
    Builds a static site using PowerShell.

    * The site will be configured using any `config.*` files.
    * Functions and filters will be loaded from any `functions.*` or `filters.*` files.
    * All files will be processed using `buildFile.ps1` (any `*.*.ps1` file should be run).
.EXAMPLE
    ./build.ps1
#>

param(
[string[]]
$FilePath,

[string]
$Root = $PSScriptRoot
)

# Push into the script root directory
if ($PSScriptRoot) { Push-Location $PSScriptRoot }

# Creation of a sitewide object to hold configuration information.
$Site = [Ordered]@{}
$Site.Files = 
    if ($filePath) { Get-ChildItem -Recurse -File -Path $FilePath } 
    else { Get-ChildItem -Recurse -File }

$Site.PSScriptRoot = "$PSScriptRoot"

#region Common Functions and Filters
$functionFileNames = 'functions', 'function', 'filters', 'filter'
$functionPattern   = "(?>$($functionFileNames -join '|'))\.ps1$"
$functionFiles     = Get-ChildItem -Path $Site.PSScriptRoot |
    Where-Object Name -Match $functionPattern

foreach ($file in $functionFiles) {
    # If we have a file with the name function or functions, we'll use it to set the site configuration.
    . $file.FullName
}
#endregion Common Functions and Filters

# Set an alias to buildPage.ps1
Set-Alias BuildPage ./buildPage.ps1

# If we have an event path,
$gitHubEvent =
    if ($env:GITHUB_EVENT_PATH) {
        # all we need to do to serve it is copy it.
        Copy-Item $env:GITHUB_EVENT_PATH .\gitHubEvent.json

        # and we can assign it to a variable, so you we can use it in any files we build.
        Get-Content -Path .\gitHubEvent.json -Raw | ConvertFrom-Json
    }

# If we have a CNAME, read it, trim it, and update the site object.
if (Test-Path 'CNAME') {
    $Site.CNAME = $CNAME = (Get-Content -Path 'CNAME' -Raw).Trim()
    $Site.RootUrl = "https://$CNAME/"
} elseif (
    ($site.PSScriptRoot | Split-Path -Leaf) -like '*.*'
) {
    $site.CNAME = $CNAME = ($site.PSScriptRoot | Split-Path -Leaf)
    $site.RootUrl = "https://$CNAME/"
}

# If we have a config.json file, it can be used to set the site configuration.
if (Test-Path 'config.json') {
    $siteConfig = Get-Content -Path 'config.json' -Raw | ConvertFrom-Json
    foreach ($property in $siteConfig.psobject.properties) {
        $Site[$property.Name] = $property.Value
    }
}

# If we have a config.psd1 file, we'll use it to set the site configuration.
if (Test-Path 'config.psd1') {
    $siteConfig = Import-LocalizedData -FileName 'config.psd1' -BaseDirectory $PSScriptRoot
    foreach ($property in $siteConfig.GetEnumerator()) {
        $Site[$property.Key] = $property.Value
    }
}

# If we have a config yaml, things
if (Test-Path 'config.yaml') {
    $siteConfig = Get-Item 'config.yaml' | from_yaml
    foreach ($property in $siteConfig.GetEnumerator()) {
        $Site[$property.Name] = $property.Value
    }
}

# If we have a config.ps1 file,
if (Test-Path 'config.ps1') {
    # Get the script command
    $configScript = Get-Command -Name './config.ps1'
    # and install any requirements it has.
    $configScript | RequireModule
    # run it, and let it configure anything it chooses to.
    . $configScript
}

# Start the clock
$site['LastBuildTime'] = $lastBuildTime = [DateTime]::Now
#region Build Files

# Start the clock on the build process
$buildStart = [DateTime]::Now
# pipe every file we find to buildFile
$Site.Files | . buildPage
# and stop the clock
$buildEnd = [DateTime]::Now

#endregion Build Files

# If we have changed directories, we need to push back into the script root directory.
if ($PSScriptRoot -and "$PSScriptRoot" -ne "$pwd") {
    Push-Location $psScriptRoot
}

#region lastBuild.json
# We create a new object each time, so we can use it to compare to the last build.
$newLastBuild = [Ordered]@{
    LastBuildTime = $lastBuildTime
    BuildDuration = $buildEnd - $buildStart
    Message = 
        if ($gitHubEvent.commits) { 
            $gitHubEvent.commits[-1].Message
        } elseif ($gitHubEvent.schedule) {
            "Ran at $([DateTime]::Now.ToString('o')) on $($gitHubEvent.schedule)"
        } else {
            'On Demand'
        }
}

# If we have a CNAME, we can use it to get the last build time from the server.
$lastBuild =
    try {
        Invoke-RestMethod -Uri "https://$CNAME/lastBuild.json" -ErrorAction Ignore
    } catch {
        Write-Verbose ($_ | Out-String)
    }

# If we could get the last build time, we can use it to calculate the time since the last build.
if ($lastBuild) {
    $newLastBuild.TimeSinceLastBuild = $lastBuildTime - $lastBuild.LastBuildTime
}

# Save the build time to a file.
$newLastBuild | ConvertTo-Json -Depth 2 > lastBuild.json
#endregion lastBuild.json

#region sitemap.xml
if (-not $Site.NoSitemap) {
    $siteMapXml = @(
        '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
        :nextPage foreach ($key in $site.PagesByUrl.Keys | Sort-Object { "$_".Length}) {
            $keyUri = $key -as [Uri]
            $page = $site.PagesByUrl[$key]
            if ($site.Disallow) {
                foreach ($disallow in $site.Disallow) {
                    if ($keyUri.LocalPath -like "*$disallow*") { continue nextPage }
                    if ($keyUri.AbsoluteUri -like "*$disallow*") { continue nextPage }
                }
            }
            if ($page.NoIndex) { continue }
            if ($page.NoSitemap) { continue }
            if ($page.OutputFile.Extension -ne '.html') { continue }
            "<url>"
            "<loc>$key</loc>"
            if ($site.PagesByUrl[$key].Date -is [DateTime]) {
                "<lastmod>$($site.PagesByUrl[$key].Date.ToString('yyyy-MM-dd'))</lastmod>"
            }
            "</url>"
        }
        '</urlset>'
    ) -join ' ' -as [xml]
    if ($siteMapXml) {
        $siteMapXml.Save((
            Join-Path $site.PSScriptRoot sitemap.xml
        ))
    }
}
#endregion sitemap.xml

#region index.rss
if (-not $Site.NoRss) {
    $pagesByDate = @($site.PagesByUrl.GetEnumerator() | 
        Sort-Object { $_.Value.Date } -Descending)
    $lastPubDate = if ($pagesByDate.Values.Date) {
        $pagesByDate[0].Value.Date.ToString('R')
    } else {
        $lastBuildTime.ToString('R')
    }
    $rssXml = @(
        '<rss version="2.0">'
            '<channel>'
            "<title>$([Security.SecurityElement]::Escape($(
                if ($site.Title) { $site.Title } else { $site.CNAME }
            )))</title>"

            "<link>$($site.RootUrl)</link>"        
            "<description>$([Security.SecurityElement]::Escape($(
                if ($site.Description) { $site.Description } else { $site.Title }
            )))</description>"

            "<pubDate>$($lastPubDate)</pubDate>"
            "<lastBuildDate>$($lastBuildTime.ToString('R'))</lastBuildDate>"
            "<language>$([Security.SecurityElement]::Escape($site.Language))</language>"        
            :nextPage foreach ($keyValue in $pagesByDate) {
                $key = $keyValue.Key
                $keyUri = $key -as [Uri]
                $page = $keyValue.Value
                if ($site.Disallow) {
                    foreach ($disallow in $site.Disallow) {
                        if ($keyUri.LocalPath -like "*$disallow*") { continue nextPage }
                        if ($keyUri.AbsoluteUri -like "*$disallow*") { continue nextPage }
                    }
                }
                if ($site.PagesByUrl[$key].NoIndex) { continue }
                if ($site.PagesByUrl[$key].NoSitemap) { continue }
                if ($site.PagesByUrl[$key].OutputFile.Extension -ne '.html') { continue }
                "<item>"
                "<title>$([Security.SecurityElement]::Escape($(
                    if ($page.Title) { $page.Title }
                    elseif ($site.Title) { $site.Title }
                    else { $site.CNAME }
                )))</title>"

                if ($site.PagesByUrl[$key].Date -is [DateTime]) {
                    "<pubDate>$($site.PagesByUrl[$key].Date.ToString('R'))</pubDate>"
                }
                "<description>$([Security.SecurityElement]::Escape($(
                    if ($page.Description) { $page.Description }
                    elseif ($site.Description) { $site.Description }
                )))</description>"

                "<link>$key</link>"
                "<guid isPermaLink='true'>$key</guid>"
                "</item>"
            }
            '</channel>'
        '</rss>'
    ) -join ' ' -as [xml]
    
    if ($rssXml) {
        $rssOutputPath = Join-Path $site.PSScriptRoot 'RSS' | Join-Path -ChildPath 'index.rss'
        if (-not (Test-Path $rssOutputPath)) {
            # Create the file if it doesn't exist
            $null = New-Item -ItemType File -Force $rssOutputPath
        }
        $rssXml.Save($rssOutputPath)
    }
}


#endregion index.rss

#region robots.txt
if (-not $Site.NoRobots) {
    @(
        "User-agent: *"
        if ($site.Disallow) {
            foreach ($disallow in $site.Disallow) {
                "Disallow: $disallow"
            }
        }
        if ($site.Allow) {
            foreach ($allow in $site.Allow) {
                "Allow: $allow"
            }
        }
        if ($site.CNAME -and -not $site.NoSitemap) {
            "Sitemap: https://$($site.CNAME)/sitemap.xml"
        }
    ) > robots.txt
}
#endregion robots.txt

#region index.json
if (-not $Site.NoIndex) {
    $fileIndex =
        if ($filePath) { Get-ChildItem -Recurse -File -Path $FilePath }
        else { Get-ChildItem -Recurse -File }    

    $replacement = 
        if ($filePath) {
            "^" + ([regex]::Escape($filePath) -replace '\*','.{0,}?')
        } else {
            "^" + [regex]::Escape("$pwd")
        }

    $indexObject    = [Ordered]@{}
    $gitCommand     = $ExecutionContext.SessionState.InvokeCommand.GetCommand('git', 'Application')
    foreach ($file in $fileIndex) {
        $gitDates = 
            try { 
                (& $gitCommand log --follow --format=%ci --date default $file.FullName *>&1) -as [datetime[]]
            } catch {
                $null
            }
        $LASTEXITCODE = 0
        
        $indexObject[$file.FullName -replace $replacement] = [Ordered]@{
            Name        = $file.Name            
            Length      = $file.Length
            Extension   = $file.Extension
            CreatedAt   = 
                if ($gitDates) {
                    $gitDates[-1]
                } else {
                     $file.CreationTime
                }
            LastWriteTime = 
                if ($gitDates) {
                    $gitDates[0]
                } else {
                    $file.LastWriteTime
                }
        }
    }
        
    foreach ($indexKey in $indexObject.Keys) {
        if (-not $indexObject[$indexKey].CreatedAt) {
            if ($indexObject["$indexKey.ps1"].CreatedAt) {
                $indexObject[$indexKey].CreatedAt = $indexObject["$indexKey.ps1"].CreatedAt
            }
        }
        if (-not $indexObject[$indexKey].LastWriteTime) {
            if ($indexObject["$indexKey.ps1"].LastWriteTime) {
                $indexObject[$indexKey].LastWriteTime = $indexObject["$indexKey.ps1"].LastWriteTime
            }            
        }
    }
    
    $indexObject | ConvertTo-Json -Depth 4 > index.json
}
#endregion index.json

#region archive.zip
if ($site.Archive) {
    # Create an archive of the current deployment.
    Compress-Archive -Path $pwd -DestinationPath "archive.zip" -CompressionLevel Optimal -Force
}
#endregion archive.zip
if ($PSScriptRoot) { Pop-Location }