param() $ErrorActionPreference = 'Stop' function Get-TrackedPaths { param( [string]$RepoRoot, [string]$Pattern ) $output = & git -C $RepoRoot ls-files $Pattern if ($LASTEXITCODE -ne 0) { throw "git ls-files failed for pattern: $Pattern" } return @($output | Where-Object { $_ -and $_.Trim() }) } function Normalize-Text { param([string]$Value) if ([string]::IsNullOrWhiteSpace($Value)) { return '' } $decoded = [System.Net.WebUtility]::HtmlDecode($Value) # Windows PowerShell leaves some supplementary-plane numeric emoji encoded. $decoded = [regex]::Replace($decoded, '&#(?\d+);', { param($match) [char]::ConvertFromUtf32([int]$match.Groups['decimal'].Value) }) $decoded = [regex]::Replace($decoded, '&#x(?[0-9a-f]+);', { param($match) [char]::ConvertFromUtf32([Convert]::ToInt32($match.Groups['hex'].Value, 16)) }, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) return ([regex]::Replace($decoded, '\s+', ' ')).Trim() } function Resolve-WebPath { param( [string]$BasePath, [string]$RelativePath ) $path = $RelativePath.Trim() if ($path.StartsWith('./')) { $path = $path.Substring(2) } if ($path.StartsWith('/')) { return $path.TrimStart('/') } return "$BasePath/$path" -replace '\\', '/' -replace '/+', '/' } $repoRoot = Split-Path -Parent $PSScriptRoot $gamesRoot = Join-Path $repoRoot 'games' $catalogPath = Join-Path $repoRoot 'js\game-catalog.js' $sitemapPath = Join-Path $repoRoot 'sitemap.xml' $siteRoot = 'https://supagamesai.com' $trackedLevelIndexes = Get-TrackedPaths -RepoRoot $repoRoot -Pattern 'games/lvl*/index.html' $trackedGameFiles = Get-TrackedPaths -RepoRoot $repoRoot -Pattern 'games/lvl*/*.html' $trackedLearnFiles = Get-TrackedPaths -RepoRoot $repoRoot -Pattern 'learn/*.html' $trackedGameSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) foreach ($trackedPath in $trackedGameFiles) { if ($trackedPath -notlike '*/index.html') { [void]$trackedGameSet.Add($trackedPath.Replace('\', '/')) } } $catalog = New-Object System.Collections.Generic.List[object] $levelEntries = New-Object System.Collections.Generic.List[object] $cardPattern = '[^"]+)"\s+class="[^"]*\bgame-card\b[^"]*"[^>]*>\s*(?.*?).*?]*)?>(?.*?)</h3>\s*<p(?:\s+[^>]*)?>(?<description>.*?)</p>\s*</a>' foreach ($relativeIndex in $trackedLevelIndexes) { $indexPath = Join-Path $repoRoot $relativeIndex $html = Get-Content $indexPath -Raw -Encoding UTF8 $levelId = [regex]::Match($relativeIndex, 'games/(?<level>lvl\d+)/index\.html').Groups['level'].Value $levelNumber = [int]([regex]::Match($levelId, '\d+').Value) $indexDir = Split-Path $relativeIndex -Parent $headingMatch = [regex]::Match($html, '<h1>(?<heading>[^<]+)</h1>', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) $heading = Normalize-Text $headingMatch.Groups['heading'].Value $theme = $heading if ($heading -match ':\s*(.+)$') { $theme = $matches[1].Trim() } $cards = [regex]::Matches( $html, $cardPattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase -bor [System.Text.RegularExpressions.RegexOptions]::Singleline ) $validCardCount = 0 foreach ($card in $cards) { $gamePath = Resolve-WebPath -BasePath $indexDir.Replace('\', '/') -RelativePath $card.Groups['href'].Value if (-not $trackedGameSet.Contains($gamePath)) { continue } $validCardCount++ $catalog.Add([pscustomobject]@{ url = $gamePath title = Normalize-Text $card.Groups['title'].Value description = Normalize-Text $card.Groups['description'].Value icon = Normalize-Text $card.Groups['icon'].Value levelId = $levelId levelNumber = $levelNumber levelLabel = ('Level {0}' -f $levelNumber) theme = $theme }) | Out-Null } if ($validCardCount -gt 0) { $levelEntries.Add([pscustomobject]@{ path = $relativeIndex.Replace('\', '/') levelId = $levelId levelNumber = $levelNumber }) | Out-Null } } $catalog = $catalog | Sort-Object levelNumber, url | ForEach-Object { [pscustomobject]@{ url = $_.url title = $_.title description = $_.description icon = $_.icon levelId = $_.levelId levelNumber = $_.levelNumber levelLabel = $_.levelLabel theme = $_.theme } } $catalogJson = $catalog | ConvertTo-Json -Depth 4 $catalogOutput = @( '// Generated by scripts/build-home-data.ps1', 'window.SUPAGAMES_CATALOG = ' + $catalogJson + ';' ) -join [Environment]::NewLine Set-Content -Path $catalogPath -Value $catalogOutput -Encoding utf8 $sitemapEntries = New-Object System.Collections.Generic.List[object] $sitemapEntries.Add([pscustomobject]@{ loc = '/'; priority = '1.0'; changefreq = 'daily' }) | Out-Null $sitemapEntries.Add([pscustomobject]@{ loc = '/games/'; priority = '0.9'; changefreq = 'daily' }) | Out-Null $sitemapEntries.Add([pscustomobject]@{ loc = '/community-games.html'; priority = '0.6'; changefreq = 'weekly' }) | Out-Null $sitemapEntries.Add([pscustomobject]@{ loc = '/info.html'; priority = '0.4'; changefreq = 'monthly' }) | Out-Null $sitemapEntries.Add([pscustomobject]@{ loc = '/changelog.html'; priority = '0.6'; changefreq = 'weekly' }) | Out-Null $sitemapEntries.Add([pscustomobject]@{ loc = '/privacy.html'; priority = '0.3'; changefreq = 'monthly' }) | Out-Null $sitemapEntries.Add([pscustomobject]@{ loc = '/terms.html'; priority = '0.3'; changefreq = 'monthly' }) | Out-Null $sitemapEntries.Add([pscustomobject]@{ loc = '/contact.html'; priority = '0.3'; changefreq = 'monthly' }) | Out-Null foreach ($learnPath in ($trackedLearnFiles | Sort-Object)) { $learnLoc = '/' + $learnPath.Replace('\', '/') if ($learnLoc -eq '/learn/index.html') { $learnLoc = '/learn/' } $sitemapEntries.Add([pscustomobject]@{ loc = $learnLoc priority = '0.7' changefreq = 'monthly' }) | Out-Null } foreach ($levelEntry in ($levelEntries | Sort-Object levelNumber)) { $sitemapEntries.Add([pscustomobject]@{ loc = '/' + $levelEntry.path priority = '0.8' changefreq = 'weekly' }) | Out-Null } foreach ($game in $catalog) { $sitemapEntries.Add([pscustomobject]@{ loc = '/' + $game.url priority = '0.5' changefreq = 'monthly' }) | Out-Null } $xmlSettings = New-Object System.Xml.XmlWriterSettings $xmlSettings.Encoding = [System.Text.UTF8Encoding]::new($false) $xmlSettings.Indent = $true $xmlSettings.NewLineChars = "`n" $xmlSettings.NewLineHandling = [System.Xml.NewLineHandling]::Replace $writer = [System.Xml.XmlWriter]::Create($sitemapPath, $xmlSettings) try { $writer.WriteStartDocument() $writer.WriteStartElement('urlset', 'http://www.sitemaps.org/schemas/sitemap/0.9') foreach ($entry in $sitemapEntries) { $writer.WriteStartElement('url') $writer.WriteElementString('loc', $siteRoot + $entry.loc) $writer.WriteElementString('priority', $entry.priority) $writer.WriteElementString('changefreq', $entry.changefreq) $writer.WriteEndElement() } $writer.WriteEndElement() $writer.WriteEndDocument() } finally { $writer.Dispose() } Write-Output ("Generated catalog with {0} games across {1} active levels." -f $catalog.Count, $levelEntries.Count)