From 97db133536f9f300225e3f9c880c9111732c92ef Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 9 Jul 2026 13:41:07 +0200 Subject: [PATCH 1/5] Publish group overview pages as the section index (index.md) A /.md overview page is now emitted as /index.md so it renders as the group's landing page (with navigation.indexes) instead of a page nested under the group. Nested groups supported; other pages unchanged. Adds an Action-Test assertion. Addresses PSModule/Process-PSModule#371. --- .github/workflows/Action-Test.yml | 17 +++++++++++++++++ src/helpers/Build-PSModuleDocumentation.ps1 | 10 ++++++++++ 2 files changed, 27 insertions(+) diff --git a/.github/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml index ec94308..3d0d7df 100644 --- a/.github/workflows/Action-Test.yml +++ b/.github/workflows/Action-Test.yml @@ -60,6 +60,23 @@ jobs: Name: PSModuleTest WorkingDirectory: tests/srcTestRepo + - name: Verify group overview pages publish as section index + shell: pwsh + run: | + $docs = 'tests/srcTestRepo/outputs/docs' + $expected = @('PSModule/index.md', 'SomethingElse/index.md') + $unexpected = @('PSModule/PSModule.md', 'SomethingElse/SomethingElse.md') + $failed = $false + foreach ($rel in $expected) { + if (Test-Path (Join-Path $docs $rel)) { Write-Host "OK present: $rel" } + else { Write-Host "ERR missing: $rel"; $failed = $true } + } + foreach ($rel in $unexpected) { + if (Test-Path (Join-Path $docs $rel)) { Write-Host "ERR should not exist: $rel"; $failed = $true } + else { Write-Host "OK absent: $rel" } + } + if ($failed) { throw 'Group overview pages were not published as section index (index.md).' } + - name: Lint documentation uses: super-linter/super-linter/slim@4ce20838b8ab83717e78138c5b3a1407148e0918 # v8.7.0 env: diff --git a/src/helpers/Build-PSModuleDocumentation.ps1 b/src/helpers/Build-PSModuleDocumentation.ps1 index e6387c7..62e723f 100644 --- a/src/helpers/Build-PSModuleDocumentation.ps1 +++ b/src/helpers/Build-PSModuleDocumentation.ps1 @@ -231,6 +231,16 @@ $(($successfulCommands | ForEach-Object { "- ``$($_.CommandName)`` `n" }) -join Write-Host " Path: $file" $docsFilePath = ($file.FullName).Replace($PublicFunctionsFolder.FullName, $docsOutputFolder) + + # A group overview page named after its folder (e.g. Auth/Auth.md) is published as the + # section landing page (Auth/index.md) so the navigation shows it when the group is + # selected, instead of listing it as a separate page nested under the group. + $parentFolderName = Split-Path -Path (Split-Path -Path $file.FullName -Parent) -Leaf + if ($file.BaseName -eq $parentFolderName) { + $docsFilePath = Join-Path -Path (Split-Path -Path $docsFilePath -Parent) -ChildPath 'index.md' + Write-Host ' Group overview page detected - publishing as section index (index.md)' + } + Write-Host " MD path: $docsFilePath" $docsFolderPath = Split-Path -Path $docsFilePath -Parent $null = New-Item -Path $docsFolderPath -ItemType Directory -Force From 465e0742922f7a712c72c134b8b3992b6d6ed1d7 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 9 Jul 2026 14:00:04 +0200 Subject: [PATCH 2/5] Support an explicit index.md group overview page The section index can now be authored either as /.md (mapped to index.md) or as /index.md directly (published as-is). If a folder has both, the explicit index.md wins and the .md stays a normal page. Fixture: SomethingElse now uses index.md to cover the passthrough path (PSModule still covers the mapped path). --- src/helpers/Build-PSModuleDocumentation.ps1 | 19 ++++++++++++++----- .../{SomethingElse.md => index.md} | 0 2 files changed, 14 insertions(+), 5 deletions(-) rename tests/srcTestRepo/src/functions/public/SomethingElse/{SomethingElse.md => index.md} (100%) diff --git a/src/helpers/Build-PSModuleDocumentation.ps1 b/src/helpers/Build-PSModuleDocumentation.ps1 index 62e723f..59d3bf5 100644 --- a/src/helpers/Build-PSModuleDocumentation.ps1 +++ b/src/helpers/Build-PSModuleDocumentation.ps1 @@ -224,6 +224,12 @@ $(($successfulCommands | ForEach-Object { "- ``$($_.CommandName)`` `n" }) -join } Write-Host '::group::Build docs - Move markdown files from public functions folder to docs output folder' + # Folders that already provide an explicit index.md; a sibling .md in such a folder + # stays a normal page so it never overwrites the author-provided index.md. + $explicitIndexFolders = @( + Get-ChildItem -Path $PublicFunctionsFolder -Recurse -Force -Filter 'index.md' | + ForEach-Object { $_.Directory.FullName } + ) Get-ChildItem -Path $PublicFunctionsFolder -Recurse -Force -Include '*.md' | ForEach-Object { $file = $_ $relPath = [System.IO.Path]::GetRelativePath($PublicFunctionsFolder.FullName, $file.FullName) @@ -232,11 +238,14 @@ $(($successfulCommands | ForEach-Object { "- ``$($_.CommandName)`` `n" }) -join $docsFilePath = ($file.FullName).Replace($PublicFunctionsFolder.FullName, $docsOutputFolder) - # A group overview page named after its folder (e.g. Auth/Auth.md) is published as the - # section landing page (Auth/index.md) so the navigation shows it when the group is - # selected, instead of listing it as a separate page nested under the group. - $parentFolderName = Split-Path -Path (Split-Path -Path $file.FullName -Parent) -Leaf - if ($file.BaseName -eq $parentFolderName) { + # A group's overview page becomes the section landing page (index.md) so the navigation + # shows it when the group is selected, instead of a page nested under the group. Authors + # can either name it after the folder (e.g. Auth/Auth.md) or provide Auth/index.md directly. + $parentFolder = Split-Path -Path $file.FullName -Parent + $parentFolderName = Split-Path -Path $parentFolder -Leaf + if ($file.Name -eq 'index.md') { + Write-Host ' Section index page (index.md) - publishing as-is' + } elseif ($file.BaseName -eq $parentFolderName -and $parentFolder -notin $explicitIndexFolders) { $docsFilePath = Join-Path -Path (Split-Path -Path $docsFilePath -Parent) -ChildPath 'index.md' Write-Host ' Group overview page detected - publishing as section index (index.md)' } diff --git a/tests/srcTestRepo/src/functions/public/SomethingElse/SomethingElse.md b/tests/srcTestRepo/src/functions/public/SomethingElse/index.md similarity index 100% rename from tests/srcTestRepo/src/functions/public/SomethingElse/SomethingElse.md rename to tests/srcTestRepo/src/functions/public/SomethingElse/index.md From 9d55145cf75c4de59e5b03965e2137be34de110c Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 9 Jul 2026 16:11:06 +0200 Subject: [PATCH 3/5] Address review: case-insensitive index detection, normalize casing, warn on collision Detect explicit index pages case-insensitively (Where-Object -ieq) with de-duplication for case-sensitive runners; normalize any casing of index.md (Index.md/INDEX.md) to index.md in the output so MkDocs treats it as the section index; and emit a warning (instead of a silent overwrite) when a .md and an index.md coexist. Adds a Widgets/Index.md fixture to cover the casing path on Linux. --- .github/workflows/Action-Test.yml | 6 +++-- src/helpers/Build-PSModuleDocumentation.ps1 | 26 +++++++++++++------ .../src/functions/public/Widgets/Index.md | 4 +++ 3 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 tests/srcTestRepo/src/functions/public/Widgets/Index.md diff --git a/.github/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml index 3d0d7df..fa34cd0 100644 --- a/.github/workflows/Action-Test.yml +++ b/.github/workflows/Action-Test.yml @@ -64,8 +64,10 @@ jobs: shell: pwsh run: | $docs = 'tests/srcTestRepo/outputs/docs' - $expected = @('PSModule/index.md', 'SomethingElse/index.md') - $unexpected = @('PSModule/PSModule.md', 'SomethingElse/SomethingElse.md') + # PSModule: /.md mapped to index.md. SomethingElse: explicit index.md + # passthrough. Widgets: Index.md normalized to index.md (case-insensitive). + $expected = @('PSModule/index.md', 'SomethingElse/index.md', 'Widgets/index.md') + $unexpected = @('PSModule/PSModule.md', 'SomethingElse/SomethingElse.md', 'Widgets/Index.md') $failed = $false foreach ($rel in $expected) { if (Test-Path (Join-Path $docs $rel)) { Write-Host "OK present: $rel" } diff --git a/src/helpers/Build-PSModuleDocumentation.ps1 b/src/helpers/Build-PSModuleDocumentation.ps1 index 59d3bf5..2604a3b 100644 --- a/src/helpers/Build-PSModuleDocumentation.ps1 +++ b/src/helpers/Build-PSModuleDocumentation.ps1 @@ -224,11 +224,14 @@ $(($successfulCommands | ForEach-Object { "- ``$($_.CommandName)`` `n" }) -join } Write-Host '::group::Build docs - Move markdown files from public functions folder to docs output folder' - # Folders that already provide an explicit index.md; a sibling .md in such a folder - # stays a normal page so it never overwrites the author-provided index.md. + # Folders that already provide an explicit index page (any casing of index.md); a sibling + # .md in such a folder stays a normal page so it never overwrites the author-provided + # index page. Detected case-insensitively because the runner filesystem is case-sensitive. $explicitIndexFolders = @( - Get-ChildItem -Path $PublicFunctionsFolder -Recurse -Force -Filter 'index.md' | - ForEach-Object { $_.Directory.FullName } + Get-ChildItem -Path $PublicFunctionsFolder -Recurse -Force -File | + Where-Object { $_.Name -ieq 'index.md' } | + ForEach-Object { $_.Directory.FullName } | + Sort-Object -Unique ) Get-ChildItem -Path $PublicFunctionsFolder -Recurse -Force -Include '*.md' | ForEach-Object { $file = $_ @@ -243,11 +246,18 @@ $(($successfulCommands | ForEach-Object { "- ``$($_.CommandName)`` `n" }) -join # can either name it after the folder (e.g. Auth/Auth.md) or provide Auth/index.md directly. $parentFolder = Split-Path -Path $file.FullName -Parent $parentFolderName = Split-Path -Path $parentFolder -Leaf - if ($file.Name -eq 'index.md') { - Write-Host ' Section index page (index.md) - publishing as-is' - } elseif ($file.BaseName -eq $parentFolderName -and $parentFolder -notin $explicitIndexFolders) { + if ($file.Name -ieq 'index.md') { + # Normalize any casing (Index.md/INDEX.md) to index.md so it is treated as the + # section index on case-sensitive filesystems. $docsFilePath = Join-Path -Path (Split-Path -Path $docsFilePath -Parent) -ChildPath 'index.md' - Write-Host ' Group overview page detected - publishing as section index (index.md)' + Write-Host ' Section index page detected - publishing as index.md' + } elseif ($file.BaseName -eq $parentFolderName) { + if ($parentFolder -in $explicitIndexFolders) { + Write-Warning "Group overview page '$relPath' is not used as the section index because the folder already has an explicit index.md; publishing it as a normal page." + } else { + $docsFilePath = Join-Path -Path (Split-Path -Path $docsFilePath -Parent) -ChildPath 'index.md' + Write-Host ' Group overview page detected - publishing as section index (index.md)' + } } Write-Host " MD path: $docsFilePath" diff --git a/tests/srcTestRepo/src/functions/public/Widgets/Index.md b/tests/srcTestRepo/src/functions/public/Widgets/Index.md new file mode 100644 index 0000000..f3eaf5b --- /dev/null +++ b/tests/srcTestRepo/src/functions/public/Widgets/Index.md @@ -0,0 +1,4 @@ +# Widgets + +This is the section overview for the Widgets group, authored as `Index.md` to verify +case-insensitive detection and normalization to `index.md`. From ae99c2a1f90ef7f5e952355d6c18b3798d3b20c8 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 9 Jul 2026 16:15:43 +0200 Subject: [PATCH 4/5] Fix PSAvoidLongLines on the collision warning Shorten the Write-Warning message to a single line under the 150-char PSScriptAnalyzer limit configured in .github/linters/.powershell-psscriptanalyzer.psd1. --- src/helpers/Build-PSModuleDocumentation.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/Build-PSModuleDocumentation.ps1 b/src/helpers/Build-PSModuleDocumentation.ps1 index 2604a3b..5497091 100644 --- a/src/helpers/Build-PSModuleDocumentation.ps1 +++ b/src/helpers/Build-PSModuleDocumentation.ps1 @@ -253,7 +253,7 @@ $(($successfulCommands | ForEach-Object { "- ``$($_.CommandName)`` `n" }) -join Write-Host ' Section index page detected - publishing as index.md' } elseif ($file.BaseName -eq $parentFolderName) { if ($parentFolder -in $explicitIndexFolders) { - Write-Warning "Group overview page '$relPath' is not used as the section index because the folder already has an explicit index.md; publishing it as a normal page." + Write-Warning "Ignoring group overview '$relPath' as the section index; folder already has an explicit index.md." } else { $docsFilePath = Join-Path -Path (Split-Path -Path $docsFilePath -Parent) -ChildPath 'index.md' Write-Host ' Group overview page detected - publishing as section index (index.md)' From ef130125e860694b55484c7b8ad9bf93e3cb0d8a Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 9 Jul 2026 16:22:30 +0200 Subject: [PATCH 5/5] Fail fast on ambiguous duplicate index pages; single tree walk Enumerate public markdown once and derive the explicit-index folder set from it (no second tree walk). Group index pages per folder and throw a clear error when a folder has two that differ only by case (index.md + Index.md), instead of letting Move-Item -Force silently overwrite one. Addresses the remaining Copilot review comment. --- src/helpers/Build-PSModuleDocumentation.ps1 | 30 ++++++++++++--------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/helpers/Build-PSModuleDocumentation.ps1 b/src/helpers/Build-PSModuleDocumentation.ps1 index 5497091..5b8ab20 100644 --- a/src/helpers/Build-PSModuleDocumentation.ps1 +++ b/src/helpers/Build-PSModuleDocumentation.ps1 @@ -224,17 +224,23 @@ $(($successfulCommands | ForEach-Object { "- ``$($_.CommandName)`` `n" }) -join } Write-Host '::group::Build docs - Move markdown files from public functions folder to docs output folder' - # Folders that already provide an explicit index page (any casing of index.md); a sibling - # .md in such a folder stays a normal page so it never overwrites the author-provided - # index page. Detected case-insensitively because the runner filesystem is case-sensitive. - $explicitIndexFolders = @( - Get-ChildItem -Path $PublicFunctionsFolder -Recurse -Force -File | - Where-Object { $_.Name -ieq 'index.md' } | - ForEach-Object { $_.Directory.FullName } | - Sort-Object -Unique - ) - Get-ChildItem -Path $PublicFunctionsFolder -Recurse -Force -Include '*.md' | ForEach-Object { - $file = $_ + # Enumerate the public markdown once and reuse it (no second tree walk). + $publicMarkdownFiles = @(Get-ChildItem -Path $PublicFunctionsFolder -Recurse -Force -File -Include '*.md') + + # Folders that provide an explicit index page. Detected case-insensitively because the runner + # filesystem is case-sensitive. Two index pages that differ only by case (index.md and + # Index.md) are ambiguous, so fail fast instead of silently overwriting one. + $explicitIndexFolders = [System.Collections.Generic.HashSet[string]]::new() + $indexGroups = $publicMarkdownFiles | Where-Object { $_.Name -ieq 'index.md' } | Group-Object { $_.Directory.FullName } + foreach ($group in $indexGroups) { + if ($group.Count -gt 1) { + $names = ($group.Group.Name | Sort-Object) -join ', ' + throw "Ambiguous section index in '$($group.Name)': multiple index pages ($names). Keep only one index.md." + } + [void]$explicitIndexFolders.Add($group.Name) + } + + foreach ($file in $publicMarkdownFiles) { $relPath = [System.IO.Path]::GetRelativePath($PublicFunctionsFolder.FullName, $file.FullName) Write-Host " - $relPath" Write-Host " Path: $file" @@ -252,7 +258,7 @@ $(($successfulCommands | ForEach-Object { "- ``$($_.CommandName)`` `n" }) -join $docsFilePath = Join-Path -Path (Split-Path -Path $docsFilePath -Parent) -ChildPath 'index.md' Write-Host ' Section index page detected - publishing as index.md' } elseif ($file.BaseName -eq $parentFolderName) { - if ($parentFolder -in $explicitIndexFolders) { + if ($explicitIndexFolders.Contains($parentFolder)) { Write-Warning "Ignoring group overview '$relPath' as the section index; folder already has an explicit index.md." } else { $docsFilePath = Join-Path -Path (Split-Path -Path $docsFilePath -Parent) -ChildPath 'index.md'