Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/gh-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,21 @@ jobs:
curl -sSL $url | tar -xz --directory=./mdbook
echo `pwd`/mdbook >> $GITHUB_PATH

- name: Deploy GitHub Pages
- name: Build Documentation
run: |
cd wiki
mdbook build

# mdBook rewrites .md links to .html without verifying the target exists, so a
# renamed or removed page builds cleanly and 404s in production. This also checks
# that the legacy wiki redirect map still resolves.
- name: Check Links
shell: pwsh
run: ./wiki/tools/Test-Links.ps1 -Book ./wiki/book -MapPath ./wiki/tools/wiki-redirect-map.tsv

- name: Deploy GitHub Pages
run: |
cd wiki
git worktree add gh-pages
git config user.name "GitHub Pages from CI"
git config user.email ""
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[![.NET Foundation](https://img.shields.io/badge/.NET%20Foundation-blueviolet.svg)](https://dotnetfoundation.org/projects/project-detail/asp.net-api-versioning)
[![.NET Foundation](https://img.shields.io/badge/.NET%20Foundation-blueviolet.svg)][dnf]
[![MIT License](https://img.shields.io/github/license/dotnet/aspnet-api-versioning?color=%230b0&style=flat-square)](https://github.com/dotnet/aspnet-api-versioning/blob/main/LICENSE.txt)
[![Build Status](https://dev.azure.com/aspnet-api-versioning/build/_apis/build/status/dotnet.aspnet-api-versioning?branchName=main)](https://dev.azure.com/aspnet-api-versioning/build/_build/latest?definitionId=1&branchName=main)

Expand Down Expand Up @@ -153,10 +153,12 @@ This project is licensed under the [MIT](LICENSE.TXT) license.

## .NET Foundation

[<img align="right" width="100px" style="margin:-70px 0px 0px 0px" src="https://dotnetfoundation.org/img/logo_v4.svg" />](https://dotnetfoundation.org/projects/aspnet-api-versioning)
[<img align="right" width="100px" style="margin:-70px 0px 0px 0px" src="dnf.svg" />][dnf]
This project is supported by the [.NET Foundation](https://dotnetfoundation.org).

----
> If you are an existing user, please makes sure you review the [release notes](../../releases) between all major and minor package releases.

<div style="text-align:center;margin-top:32px;font-size:small">Logo by <a href="https://sacramento-design.com" target="_blank">Sacramento Design Works</a></div>

[dnf]: https://dotnetfoundation.org/projects/project-detail/asp.net-api-versioning
1 change: 1 addition & 0 deletions asp.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<File Path=".gitattributes" />
<File Path=".gitignore" />
<File Path="azure-pipelines.yml" />
<File Path="dnf.svg" />
<File Path="global.json" />
<File Path="LICENSE.txt" />
<File Path="logo.svg" />
Expand Down
1 change: 1 addition & 0 deletions dnf.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
99 changes: 99 additions & 0 deletions wiki/tools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Wiki Tools

Tooling for the legacy GitHub wiki, which was superseded by
<https://dotnet.github.io/aspnet-api-versioning>.

The wiki is intentionally kept online rather than disabled. Articles, training material, and
blog posts link to it, and those links should keep resolving. Every page has been replaced with
a short stub pointing at the equivalent page on the new site.

## Why stubs instead of redirects

GitHub wikis cannot issue an HTTP redirect. There is no `_redirects` or `.htaccess`, and
GitHub's Markdown sanitizer strips `<meta http-equiv="refresh">`, `<script>`, and
`<link rel="canonical">`. A stub page is the only available "soft redirect": it keeps human
visitors moving to the right place, and search engines derank the thin duplicate pages over
time. It does not transfer ranking signal the way a 301 would.

## Files

| File | Purpose |
|:-----|:--------|
| `wiki-redirect-map.tsv` | Old wiki page &rarr; new site path(s). Tab-separated. |
| `make-wiki-stubs.ps1` | Rewrites every wiki page into a stub from that map. |
| `Test-Links.ps1` | Validates relative links and anchors in the built site. |

## Link checking

`Test-Links.ps1` runs in CI as the **Check Links** step of `.github/workflows/gh-pages.yml`,
between the build and the deploy, so a broken link fails the workflow instead of shipping.

mdBook rewrites `.md` links to `.html` but never verifies the target exists &mdash; a link to a
renamed or deleted page builds cleanly and 404s in production. The script resolves every
relative `href`/`src` in the generated HTML against the filesystem and checks that each
`#fragment` matches a real `id` on the target page.

Passing `-MapPath` also verifies every target in `wiki-redirect-map.tsv`. If the site is
restructured, the build fails with the stale entries listed, rather than the legacy wiki
quietly pointing at pages that no longer exist.

```powershell
cd wiki
mdbook build
./tools/Test-Links.ps1 -Book ./book -MapPath ./tools/wiki-redirect-map.tsv
```

It exits non-zero when anything is broken, and reports `MISSING FILE` or `MISSING ANCHOR` per
link. mdBook's `404.html` is skipped: its links are deliberately site-absolute via `<base href>`
and cannot be resolved on disk.

## Usage

The wiki is a git repository. Clone it with **full history** &mdash; a shallow clone can be
rejected on push with `shallow update not allowed`:

```powershell
git clone https://github.com/dotnet/aspnet-api-versioning.wiki.git

# preview (default: writes nothing)
./make-wiki-stubs.ps1 -WikiPath ./aspnet-api-versioning.wiki -MapPath ./wiki-redirect-map.tsv

# write the stubs
./make-wiki-stubs.ps1 -WikiPath ./aspnet-api-versioning.wiki -MapPath ./wiki-redirect-map.tsv -Apply
```

The script never pushes. Review the diff and push yourself:

```powershell
cd ./aspnet-api-versioning.wiki
git diff --stat
git add -A && git commit -m "Redirect wiki to https://dotnet.github.io/aspnet-api-versioning"
git push
```

## Maintaining the map

If the documentation site is restructured, update `wiki-redirect-map.tsv` and re-run the
script. The map is reconciled against the wiki on every run: pages present in the wiki but
missing from the map are reported and skipped, and map rows with no matching page are reported
as stale.

Columns are `OldPage`, `AspNetCorePath`, `AspNetPath`. Use `-` when a topic exists for only one
platform, and `(root)` to point at the site root. Paths are relative to the site base URL and
may include a fragment, for example `aspnet-core/docs/odata-options.html#query-options`.

The separator must be a literal tab. An editor that expands tabs to spaces will cause a
`malformed row` error rather than a silent misfire.

## Previewing a stub

Local Markdown previewers do not use GitHub's sanitizer. To see exactly what the wiki will
render:

```powershell
$md = Get-Content -Raw ./aspnet-api-versioning.wiki/Home.md
$body = @{ text = $md; mode = 'gfm' } | ConvertTo-Json -Compress
$tmp = [System.IO.Path]::GetTempFileName()
[System.IO.File]::WriteAllText($tmp, $body, [System.Text.UTF8Encoding]::new($false))
gh api --method POST /markdown --input $tmp
```
142 changes: 142 additions & 0 deletions wiki/tools/Test-Links.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<#
.SYNOPSIS
Validates relative links and anchors in a built mdBook site, and optionally the
legacy wiki redirect map.

.DESCRIPTION
mdBook rewrites .md links to .html but does not verify the target exists, so a link to a
page that was renamed or removed builds cleanly and 404s in production. This script walks
the generated HTML, resolves every relative href/src against the filesystem, and verifies
that any #fragment matches a real id on the target page.

When -MapPath is supplied, it also confirms every target in the wiki redirect map still
resolves. If the documentation is restructured, this fails the build instead of silently
leaving the legacy wiki pointing at pages that no longer exist.

Exits 1 when anything is broken so it can gate a deployment.

.PARAMETER Book
Path to the generated book directory (the mdbook build output).

.PARAMETER MapPath
Optional path to wiki-redirect-map.tsv. When given, every mapped target is verified.

.EXAMPLE
./Test-Links.ps1 -Book ./wiki/book
./Test-Links.ps1 -Book ./wiki/book -MapPath ./wiki/tools/wiki-redirect-map.tsv
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$Book,
[string]$MapPath
)

$ErrorActionPreference = 'Stop'

if (-not (Test-Path -LiteralPath $Book)) { throw "book directory not found: $Book" }
$Book = (Resolve-Path $Book).Path

$pages = Get-ChildItem -LiteralPath $Book -Recurse -Filter *.html -File
if (-not $pages) { throw "no HTML found under $Book - did mdbook build run?" }

# --- index every anchor id in the generated site ---------------------------
$ids = @{}
foreach ($p in $pages) {
$text = Get-Content -Raw -LiteralPath $p.FullName
$set = [System.Collections.Generic.HashSet[string]]::new()
foreach ($m in [regex]::Matches($text, '\bid="([^"]+)"')) { [void]$set.Add($m.Groups[1].Value) }
$ids[$p.FullName.ToLowerInvariant()] = $set
}

$broken = New-Object System.Collections.Generic.List[object]
$seen = [System.Collections.Generic.HashSet[string]]::new()
$checked = 0

foreach ($p in $pages) {
$relPage = $p.FullName.Substring($Book.Length).TrimStart('\', '/').Replace('\', '/')

# mdBook's 404 page is deliberately site-absolute via <base href>; its links
# cannot be resolved on disk and are not a defect.
if ($relPage -eq '404.html') { continue }

$text = Get-Content -Raw -LiteralPath $p.FullName
foreach ($m in [regex]::Matches($text, '(?:href|src)="([^"]+)"')) {
$url = [System.Net.WebUtility]::HtmlDecode($m.Groups[1].Value)

# relative links only: skip scheme-qualified, protocol-relative,
# root-absolute, and same-page anchors
if ($url -match '^([a-zA-Z][a-zA-Z0-9+.-]*:|//|/|#)') { continue }

$parts = $url.Split('#', 2)
$path = [System.Uri]::UnescapeDataString($parts[0])
$frag = if ($parts.Count -gt 1) { [System.Uri]::UnescapeDataString($parts[1]) } else { '' }
if ([string]::IsNullOrEmpty($path)) { continue }

$checked++
$target = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($p.DirectoryName, $path))

$why = $null
if (-not (Test-Path -LiteralPath $target)) {
$why = 'MISSING FILE'
} elseif ($frag -and $target.EndsWith('.html')) {
$key = $target.ToLowerInvariant()
if (-not ($ids.ContainsKey($key) -and $ids[$key].Contains($frag))) { $why = 'MISSING ANCHOR' }
}

if ($why) {
$k = "$relPage|$url"
if ($seen.Add($k)) { $broken.Add([pscustomobject]@{ Why = $why; Page = $relPage; Url = $url }) }
}
}
}

Write-Output "checked $checked relative link(s) across $($pages.Count) page(s)"

# --- validate the legacy wiki redirect map ---------------------------------
$mapBroken = New-Object System.Collections.Generic.List[object]
if ($MapPath) {
if (-not (Test-Path -LiteralPath $MapPath)) { throw "map not found: $MapPath" }
$mapChecked = 0
foreach ($line in Get-Content -LiteralPath $MapPath) {
if ($line -match '^\s*(#|$)') { continue }
$cols = $line -split "`t"
if ($cols.Count -lt 2) { throw "malformed row (expected 2-3 tab-separated columns): $line" }
$page = $cols[0].Trim()
foreach ($t in @($cols[1], $(if ($cols.Count -ge 3) { $cols[2] } else { '-' }))) {
$t = $t.Trim()
if ($t -eq '-' -or $t -eq '(root)' -or -not $t) { continue }
$mapChecked++
$file = ($t -split '#', 2)[0]
if (-not (Test-Path -LiteralPath (Join-Path $Book $file))) {
$mapBroken.Add([pscustomobject]@{ Page = $page; Url = $t })
}
}
}
Write-Output "checked $mapChecked wiki redirect target(s)"
}

# --- report ----------------------------------------------------------------
$failed = $false

if ($broken.Count -gt 0) {
$failed = $true
Write-Output ''
Write-Output "BROKEN LINKS: $($broken.Count)"
foreach ($b in ($broken | Sort-Object Why, Page, Url)) {
Write-Output (" [{0}] {1} -> {2}" -f $b.Why, $b.Page, $b.Url)
}
}

if ($mapBroken.Count -gt 0) {
$failed = $true
Write-Output ''
Write-Output "STALE WIKI REDIRECT TARGETS: $($mapBroken.Count)"
Write-Output ' (update wiki-redirect-map.tsv and re-run make-wiki-stubs.ps1)'
foreach ($b in $mapBroken) {
Write-Output (" {0} -> {1}" -f $b.Page, $b.Url)
}
}

if ($failed) { exit 1 }

Write-Output 'OK - no broken relative links'
Loading
Loading