Skip to content

Inline documentation in NuGet package README to fix broken links#388

Merged
sensslen merged 7 commits into
mainfrom
copilot/fix-nuget-documentation-links
Jan 15, 2026
Merged

Inline documentation in NuGet package README to fix broken links#388
sensslen merged 7 commits into
mainfrom
copilot/fix-nuget-documentation-links

Conversation

Copy link
Copy Markdown

Copilot AI commented Jan 15, 2026

Fix NuGet Package Documentation Links

The issue is that when the package is published to nuget.org, the README contains relative links to documentation files (e.g., docs/input-json.md). While these docs are included in the NuGet package, NuGet only displays the README file and doesn't make the other markdown files accessible, causing the links to fail.

Solution Summary:

This PR implements a build-time script that automatically inlines all documentation into the NuGet package README. When users view the package on nuget.org, they'll see a single, comprehensive README with all documentation sections included and fully functional anchor links.

Changes Made:

  • Created PowerShell script (scripts/Inline-Readme.ps1) that:

    • Reads the original README.md
    • Finds all links to docs/*.md files (7 documentation files)
    • Replaces ALL links to each file (regardless of link text variation) with anchor links
    • Properly escapes special characters in replacement text (handles $, backticks, etc.)
    • Correctly removes first heading from doc files using multiline regex mode
    • Appends the full content of all referenced docs at the end of the README
    • Outputs the result to intermediate output path (gitignored)
  • Updated src/NuGetLicenseCore/NuGetLicenseCore.csproj:

    • Added a GenerateInlinedReadme target that runs before pack
    • The target executes the inline script and includes the generated README in the package
    • Uses IntermediateOutputPath with proper path combining for temporary file generation
    • Removed the separate docs files from the package (no longer needed)
    • Removed the original README from the package (replaced with inlined version)
  • Updated global.json to use SDK 10.0.101 (available SDK version)

  • Addressed code review feedback:

    • Improved readability by breaking MSBuild command into separate properties
    • Enhanced anchor ID generation to match GitHub's behavior exactly:
      • Changed from [^\w\s-] to [^a-z0-9\s-] to properly strip underscores and Unicode
      • Handles consecutive/trailing hyphens correctly
    • Used IntermediateOutputPath instead of hardcoded obj directory
    • Fixed path concatenation using System.IO.Path.Combine() to handle trailing slashes
    • Fixed replacement logic to replace ALL link variations (not just exact match)
    • Properly escape special regex characters in replacement text (especially $)
    • Fixed heading removal to use RegexOptions.Multiline for correct behavior

Testing Completed:

  • ✅ Built the package successfully with dotnet pack
  • ✅ Verified the generated package contains README-nuget.md (402 lines) with all 7 documentation sections inlined
  • ✅ Confirmed all doc links are converted to anchor links pointing to sections in the same file
  • ✅ Tested anchor generation with special characters (underscores, Unicode, hyphens) - matches GitHub behavior
  • ✅ Verified replacement handles multiple link variations to the same file
  • ✅ Tested special character escaping in replacement text
  • ✅ Verified heading removal works correctly with multiline content
  • ✅ Ran unit tests successfully (91 passed) - no regressions
  • ✅ Code review completed - all feedback addressed
  • ✅ Security scan completed - no vulnerabilities found

Result:

Users viewing the package on nuget.org will now see a complete README with all documentation properly linked and accessible in a single page.

Original prompt

When published to nuget the documentation that are linked from the Readme do not work and always fall back to the readme. Is there a way to include multiple documentation pages in the nuget package. If so find a way to fix the published package. If not, find another way (e.g. a build step) to fixup the documentation so that what's published to nuget is the full documentation


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Summary by CodeRabbit

  • Chores

    • Updated the project to use a different .NET SDK patch version.
    • Packaging now includes a generated inlined README that bundles docs into the package.
  • New Features

    • Added a build-time script to inline repository docs into the packaged README automatically.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 15, 2026

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This PR changes global.json to set the .NET SDK version from 10.0.102 to 10.0.101 (rollForward remains "latestMinor"). It adds scripts/Inline-Readme.ps1, a PowerShell script that inlines docs/*.md into a README by extracting each doc's first heading, generating GitHub-style anchors, replacing links, and appending an "Additional Documentation" section. The NuGetLicenseCore.csproj removes PackageReadmeFile and packaged README/docs content entries, and adds a GenerateInlinedReadme MSBuild target (BeforeTargets="_GetPackageFiles") that runs the PowerShell script and declares the generated inlined README for packaging.

Possibly related PRs

  • sensslen/nuget-license PR 325: Modifies src/NuGetLicenseCore/NuGetLicenseCore.csproj (adds net10.0 to TargetFrameworks), touching the same project file adjusted here.
  • sensslen/nuget-license PR 336: Modifies global.json SDK configuration, overlapping this PR's SDK version change.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main objective: inlining documentation in the NuGet package README to fix broken links, which aligns with the PR's core purpose of solving link breakage in nuget.org.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI and others added 2 commits January 15, 2026 20:30
Co-authored-by: sensslen <3428860+sensslen@users.noreply.github.com>
Co-authored-by: sensslen <3428860+sensslen@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix documentation links in nuget package Inline documentation in NuGet package README to fix broken links Jan 15, 2026
Copilot AI requested a review from sensslen January 15, 2026 20:37
@sensslen
Copy link
Copy Markdown
Owner

@copilot Make sure to use IntermediateOutputPath to generate a temporary path for the generated readme file

Co-authored-by: sensslen <3428860+sensslen@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Jan 15, 2026

@copilot Make sure to use IntermediateOutputPath to generate a temporary path for the generated readme file

Updated to use IntermediateOutputPath instead of hardcoded obj directory. The generated README now uses the proper intermediate output path that includes the build configuration (e.g., obj/Release/README-nuget.md). (commit 5f516d6)

@sensslen
Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@sensslen
Copy link
Copy Markdown
Owner

Walkthrough

The PR makes three main changes to the project. First, the global.json file updates the .NET SDK version from 10.0.102 to 10.0.101. Second, a new PowerShell script (scripts/Inline-Readme.ps1) is introduced that inlines documentation from markdown files into a README by detecting doc links, extracting content and titles, generating GitHub-style anchors, and appending referenced documentation as an "Additional Documentation" section. Third, the NuGetLicenseCore.csproj removes static README packaging configuration and adds a new MSBuild target that runs the inline script before packaging to generate a README-nuget.md file for the NuGet package.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the changeset: introducing a build-time solution to inline documentation into the NuGet package README to fix broken links when published on nuget.org.
✏️ Tip: You can configure your own custom pre-merge checks in the settings.

🧹 Recent nitpick comments

scripts/Inline-Readme.ps1 (1)> 57-61: Potential anchor mismatch for titles with underscores or Unicode.

The \w character class in .NET includes underscores and Unicode letters, but GitHub's anchor generation strips underscores and most Unicode characters. For example, a title like My_Config_File would produce anchor my_config_file here but myconfigfile on GitHub.
Consider using a more restrictive pattern to match GitHub's behavior more closely:

♻️ Suggested fix

-        $anchorId = $anchorId -replace '[^\w\s-]', ''  # Remove special chars except word chars, spaces, hyphens
+        $anchorId = $anchorId -replace '[^a-z0-9\s-]', ''  # Remove special chars except lowercase alphanumeric, spaces, hyphens

src/NuGetLicenseCore/NuGetLicenseCore.csproj (1)> 36-36: Potential path concatenation issue with IntermediateOutputPath.

$(IntermediateOutputPath) may or may not include a trailing backslash depending on the SDK version and context. Direct concatenation could produce paths like obj\Debug\net8.0README-nuget.md instead of obj\Debug\net8.0\README-nuget.md.
Consider using $([System.IO.Path]::Combine(...)) or explicitly adding a path separator:

♻️ Suggested fix

-      <InlinedReadmePath>$(IntermediateOutputPath)README-nuget.md</InlinedReadmePath>
+      <InlinedReadmePath>$(IntermediateOutputPath)\README-nuget.md</InlinedReadmePath>

Or more robustly:

<InlinedReadmePath>$([System.IO.Path]::Combine('$(IntermediateOutputPath)', 'README-nuget.md'))</InlinedReadmePath>

📜 Recent review details
Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits
Reviewing files that changed from the base of the PR and between ce45d93 and 5f516d6.

📒 Files selected for processing (3)

  • global.json
  • scripts/Inline-Readme.ps1
  • src/NuGetLicenseCore/NuGetLicenseCore.csproj

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)

  • GitHub Check: check_licenses (net9.0, Tests)
  • GitHub Check: check_licenses (net10.0, App)
  • GitHub Check: check_licenses (net10.0, ProjectWithReferenceContainingLicenseExpression)
  • GitHub Check: check_licenses (net10.0, ProjectWithReferenceContainingFileLicense)
  • GitHub Check: test_windows (net472)
  • GitHub Check: check_licenses_net472 (ProjectWithReferenceContainingFileLicense)
  • GitHub Check: test (ubuntu-latest, net10.0)
  • GitHub Check: check_licenses_net472 (ProjectWithReferenceContainingWindowsSpecificPath)
  • GitHub Check: test_windows (net8.0)
  • GitHub Check: check_licenses_net472 (App)
  • GitHub Check: test (ubuntu-latest, net8.0)
  • GitHub Check: test_file_license_matching (ubuntu-latest)
  • GitHub Check: check_licenses_net472 (Tests)
  • GitHub Check: test_windows (net9.0)
  • GitHub Check: test_file_license_matching (windows-latest)
  • GitHub Check: check_licenses_net472 (ProjectWithReferenceContainingLicenseExpression)
  • GitHub Check: test (macos-latest, net8.0)
  • GitHub Check: test (macos-latest, net10.0)
  • GitHub Check: test (ubuntu-latest, net9.0)
  • GitHub Check: test (macos-latest, net9.0)

🔇 Additional comments (5)

global.json (1)> 6-6: Verify the SDK version downgrade.

The SDK version is being changed from 10.0.102 to 10.0.101, which appears to be a downgrade. While rollForward: latestMinor allows flexibility, please confirm this change is intentional and not an accidental regression.

scripts/Inline-Readme.ps1 (2)> 1-14: Well-structured script with clear parameters.

Good use of mandatory parameters and clear documentation comments explaining the script's purpose.

99-108: LGTM!
Good defensive coding - creating the output directory if it doesn't exist and using -NoNewline to preserve exact content formatting.

src/NuGetLicenseCore/NuGetLicenseCore.csproj (2)> 39-39: pwsh dependency may fail on build agents without PowerShell Core.

The Exec command uses pwsh directly, which requires PowerShell Core (7+) to be installed. Some CI/CD agents or developer machines may only have Windows PowerShell (powershell.exe).
Consider one of these approaches:

  1. Document the PowerShell Core requirement
  2. Use a conditional to fall back to powershell.exe on Windows
  3. Ensure CI/CD pipelines have PowerShell Core installed

Verify that all target build environments have pwsh available.

30-47: Well-designed MSBuild target for README inlining.
Good implementation choices:

  • Using BeforeTargets="_GetPackageFiles" ensures the README is generated before packaging
  • Properties are cleanly separated in a PropertyGroup
  • The generated file is correctly marked with Pack=true and Visible=false
  • Using IntermediateOutputPath follows the review feedback

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@copilot address nitpick comments

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 15, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@sensslen, I'll help you address those nitpick comments. Let me first examine the current implementation and then plan the necessary fixes.


🏁 Script executed:

#!/bin/bash
# Read the PowerShell script to see the anchor generation code
cat scripts/Inline-Readme.ps1

Length of output: 4191


🏁 Script executed:

#!/bin/bash
# Read the project file to see the path concatenation
cat src/NuGetLicenseCore/NuGetLicenseCore.csproj

Length of output: 2559


Plan: Address nitpick comments

I'll address both nitpick comments:

  1. Fix anchor generation in scripts/Inline-Readme.ps1 to match GitHub's behavior more closely by using [^a-z0-9\s-] instead of [^\w\s-]
  2. Fix path concatenation in src/NuGetLicenseCore/NuGetLicenseCore.csproj using System.IO.Path::Combine for robust handling of IntermediateOutputPath

💡 Use @coderabbitai plan to create a plan for code changes

Co-authored-by: sensslen <3428860+sensslen@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Jan 15, 2026

Walkthrough

The PR makes three main changes to the project. First, the global.json file updates the .NET SDK version from 10.0.102 to 10.0.101. Second, a new PowerShell script (scripts/Inline-...

Addressed both nitpick comments:

  1. Anchor generation: Changed from [^\w\s-] to [^a-z0-9\s-] to match GitHub's behavior - now properly strips underscores and Unicode characters (e.g., My_Config_Filemyconfigfile)

  2. Path concatenation: Updated to use System.IO.Path.Combine() to properly handle IntermediateOutputPath regardless of trailing slashes

(commit 880cad2)

@sensslen
Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@scripts/Inline-Readme.ps1`:
- Around line 46-52: The heading removal regex is not using multiline mode so
the -replace '^\s*#\s+.+$' won't match the title when $docContent contains
multiple lines; update the replacement to run in multiline mode (e.g., apply the
(?m) inline modifier or use [regex]::Replace with RegexOptions.Multiline) when
computing $docContentWithoutTitle so the first heading is removed correctly
while keeping the existing $titleMatch/$docTitle logic intact.
- Around line 83-85: The replacement loop currently only matches the exact
$replacement.OriginalLink and therefore misses links with different link text
and mis-handles literal '$' in $replacement.NewLink; change the replacement to
use a regex that matches markdown links by filename (e.g. build a pattern that
targets "\[[^\]]+\]\(\s*<escaped-filename>\s*\)" using $replacement.Filename or
similar) and call [regex]::Replace on $inlinedReadme with that pattern so any
link text is preserved; when constructing the replacement string use the
captured group for the link text and ensure you escape any '$' in
$replacement.NewLink (replace '$' with '$$' or use a MatchEvaluator delegate) so
'$' is not interpreted as a backreference.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f516d6 and 880cad2.

📒 Files selected for processing (2)
  • scripts/Inline-Readme.ps1
  • src/NuGetLicenseCore/NuGetLicenseCore.csproj
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/NuGetLicenseCore/NuGetLicenseCore.csproj
🔇 Additional comments (1)
scripts/Inline-Readme.ps1 (1)

99-103: Output directory creation is solid.

Good defensive guard before writing the file.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread scripts/Inline-Readme.ps1
Comment thread scripts/Inline-Readme.ps1
@sensslen
Copy link
Copy Markdown
Owner

@copilot In @scripts/Inline-Readme.ps1 around lines 83 - 85, The replacement loop
currently only matches the exact $replacement.OriginalLink and therefore misses
links with different link text and mis-handles literal '$' in
$replacement.NewLink; change the replacement to use a regex that matches
markdown links by filename (e.g. build a pattern that targets
"[[^\]]+](\s*\s*)" using $replacement.Filename or similar)
and call [regex]::Replace on $inlinedReadme with that pattern so any link text
is preserved; when constructing the replacement string use the captured group
for the link text and ensure you escape any '$' in $replacement.NewLink (replace
'$' with '$$' or use a MatchEvaluator delegate) so '$' is not interpreted as a
backreference.

@sensslen
Copy link
Copy Markdown
Owner

@copilot In @scripts/Inline-Readme.ps1 around lines 46 - 52, The heading removal regex is
not using multiline mode so the -replace '^\s*#\s+.+$' won't match the title
when $docContent contains multiple lines; update the replacement to run in
multiline mode (e.g., apply the (?m) inline modifier or use [regex]::Replace
with RegexOptions.Multiline) when computing $docContentWithoutTitle so the first
heading is removed correctly while keeping the existing $titleMatch/$docTitle
logic intact.

Co-authored-by: sensslen <3428860+sensslen@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Jan 15, 2026

@copilot In @scripts/Inline-Readme.ps1 around lines 83 - 85, The replacement loop
currently only matches the exact $replacement.OriginalLink and therefore misses
links with different link text and...

Fixed the replacement logic to:

  1. Match all link variations: Now uses a regex pattern \[any text\](docs/filename.md) to replace ALL links to the same file, regardless of link text
  2. Properly escape special characters: Escapes $ characters in the replacement text to prevent them being interpreted as backreferences (e.g., $1, $2)

The replacement now handles cases like:

  • [docs/input-json.md](docs/input-json.md)
  • [click here](docs/input-json.md)
  • [see documentation](docs/input-json.md)

All get replaced with the same anchor link. (commit 6ca21d9)

@sensslen
Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Co-authored-by: sensslen <3428860+sensslen@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Jan 15, 2026

@copilot In @scripts/Inline-Readme.ps1 around lines 46 - 52, The heading removal regex is
not using multiline mode so the -replace '^\s*#\s+.+$' won't match the title
when $docContent contains mul...

Fixed heading removal to use [regex]::Replace() with RegexOptions.Multiline. The -replace operator doesn't use multiline mode by default, so ^ and $ only match the start/end of the entire string. Now the first heading is correctly removed from multi-line doc content while keeping the title extraction logic intact. (commit f296b4c)

@sonarqubecloud
Copy link
Copy Markdown

@sensslen
Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 15, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants