feat(cli): shelldocs build prerenders every URL into a static site - #24
Merged
Conversation
… footer fixes, improved error handling for unknown components in PreviewSlot, and enhanced visibility of layout options in scaffolded templates
…endering support - Added validation for the presence of a 'content/' directory in the project root. - Implemented prerendering functionality to generate static HTML for discovered URLs. - Improved asset handling by copying 'wwwroot/' contents into the output directory. - Introduced new methods for directory copying with merging and overwriting capabilities.
…ccess - Introduced AllUrls property to expose both visible and hidden pages. - This enhancement supports static prerendering by ensuring all routes are accessible at runtime.
…e functionality - Renamed methods to reflect updated functionality: RewriteBaseHref to RewriteBaseHrefInAllHtml and CopyDirectory to CopyDirectoryMerging. - Added tests for RewriteBaseHrefInAllHtml to ensure it processes all HTML files recursively. - Enhanced CopyDirectoryMerging to prevent overwriting existing files and to copy missing files from the source directory.
- Updated `shelldocs build` to produce a fully static site deployable to platforms like GH Pages, Cloudflare Pages, and Netlify without requiring a .NET host. - Implemented prerendering of all known URLs, ensuring every route is accessible and returns valid HTML. - Added `NavigationGraph.AllUrls` for comprehensive URL enumeration and introduced `PrerenderRunner` for improved subprocess management during the build process. - Enhanced `--base-href` functionality to rewrite `<base href>` in all prerendered HTML files, addressing issues with deep-linked routes. - Documented known limitations regarding URL discovery and interactivity in the static output.
There was a problem hiding this comment.
Pull request overview
This PR updates shelldocs build to generate a truly static site by prerendering every known route into /<output>/<url>/index.html, then merging the published wwwroot/ assets on top—making the output deployable to basic static hosts (GH Pages/Netlify/Cloudflare Pages) without a .NET runtime.
Changes:
- Reworked
shelldocs buildto publish, run the published app locally, crawl all doc URLs, and write prerendered HTML per-route; then mergewwwroot/assets and rewrite<base href>across all generated HTML. - Fixed
shelldocs init’s content-copy MSBuild items by splitting.mdvsmeta.jsonrules and adding migration for the previously broken form. - Added
NavigationGraph.AllUrlsand expanded tests for base-href recursion and merge-vs-overwrite directory copying semantics.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/ShellDocs.Tests/BuildCommandTests.cs | Updates tests to target the new helpers and adds coverage for recursive base-href rewriting and merge semantics. |
| src/ShellDocs.Markdown/SlotExtractor.cs | Comment-only adjustment clarifying the unknown-component behavior rationale. |
| src/ShellDocs.Core/NavigationGraph.cs | Adds AllUrls enumeration to expose both visible + hidden routable pages for prerendering. |
| src/ShellDocs.CLI/Commands/PrerenderRunner.cs | New helper to start the published app, detect its bound URL, crawl routes, and write HTML files. |
| src/ShellDocs.CLI/Commands/InitCommand.cs | Fixes and migrates content copy rules so publish output contains markdown content. |
| src/ShellDocs.CLI/Commands/BuildCommand.cs | Implements the new publish → prerender → asset-merge → base-href rewrite pipeline. |
| Directory.Build.props | Bumps package version to 0.1.5-alpha. |
| CHANGELOG.md | Documents the new static prerender build behavior and related fixes/limitations. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
28
to
29
| var outputAbs = Path.GetFullPath(Path.Combine(root, output)); | ||
| var publishStage = Path.Combine(root, "obj", "shelldocs-publish"); |
Comment on lines
+45
to
+50
| var publishContent = Path.Combine(publishStage, "content"); | ||
| if (!Directory.Exists(publishContent)) | ||
| { | ||
| AnsiConsole.MarkupLine("[dim]content:[/] publish output has no content/ — mirroring from source"); | ||
| CopyDirectoryOverwriting(contentRoot, publishContent); | ||
| } |
Comment on lines
+165
to
+169
| var patched = Regex.Replace( | ||
| original, | ||
| @"<base\s+href\s*=\s*(""[^""]*""|'[^']*'|[^\s>]+)\s*/?>", | ||
| $"<base href=\"{baseHref}\" />", | ||
| RegexOptions.IgnoreCase); |
Comment on lines
+93
to
+105
| var response = http.GetAsync(url).GetAwaiter().GetResult(); | ||
| if (!response.IsSuccessStatusCode) | ||
| { | ||
| AnsiConsole.MarkupLine($" [yellow]warn:[/] [cyan]{url}[/] returned [yellow]{(int)response.StatusCode}[/]"); | ||
| failed++; | ||
| continue; | ||
| } | ||
| var html = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); | ||
| var outPath = UrlToFilePath(url, outputDir); | ||
| Directory.CreateDirectory(Path.GetDirectoryName(outPath)!); | ||
| File.WriteAllText(outPath, html); | ||
| rendered++; | ||
| } |
Comment on lines
20
to
22
| <PropertyGroup> | ||
| <Version>0.1.4-alpha</Version> | ||
| <Version>0.1.5-alpha</Version> | ||
| <Authors>ShellUI</Authors> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the "the output isn't actually static" gap. Before this PR,
shelldocs buildrandotnet publishon the (Blazor Server) scaffold and copiedwwwroot/verbatim — which for a Server-interactive project has noindex.html, no client-side runtime, nothing a static host can serve at the root URL. The output silently deployed to GH Pages / Cloudflare Pages / Netlify as a blank shell; deep links 404'd.shellicons-docshit this trying to ship toshellicons.shellui.dev(README blocker line).New flow:
dotnet publish→ mirrorcontent/into publish (safety net for consumer csproj mistakes) → launch the published app on a loopback ephemeral port → walkNavigationGraph.AllUrlsto enumerate every route (visible + hidden) plus/→HttpClient.GetAsynceach URL → save the rendered HTML to<output>/<url>/index.html. Then mergepublish/wwwroot/(_content/,_framework/blazor.web.js,app.css,favicon.png, tokens CSS) on top. Result is a directory any dumb file host can serve — every URL is a real HTML file with content in the response body.Also drags along a second bug the prerender exposed:
shelldocs initwas emitting<Content Update="content/**/*.md;content/**/meta.json" CopyToOutputDirectory="PreserveNewest" />— wrong verb.Updateonly modifies existing item metadata, and the Web SDK doesn't auto-include.md(only.json), so the.mdfiles never got copied to bin or publish. Dev worked (source-relativeContentRoot), publish silently shipped an emptycontent/, prerender rendered every URL as "Page not found." Split-rule fix:Includethe.md,Updatethe auto-included.json, both withCopyToPublishDirectory="PreserveNewest".InitCommandalso migrates any pre-0.1.5-alphabroken form on re-run.What's in
Fixed
shelldocs buildproduces a fully static site deployable to GH Pages / Cloudflare Pages / Netlify without a .NET host. Full flow rewrite inBuildCommand.Run; newPrerenderRunnerhandles the subprocess dance.--base-hrefnow rewrites<base href>in every prerendered HTML file, not just the rootindex.html. Pre-fix pass only touched one file, so subpath deploys (GH user-repo pages at/repo/) worked for the home page but broke every deep-linked doc route.shelldocs initscaffolds the correct<Content>items so publish carriescontent/. Old form usedUpdatewith a combined selector; broken because.mdnever gets auto-included. New form is a split rule (Include for .md, Update for meta.json) with bothCopyToOutputDirectoryandCopyToPublishDirectory. Existing (broken) form gets auto-migrated on re-run ofshelldocs init.Added
NavigationGraph.AllUrls— public enumeration of every indexed URL (visible + hidden). Needed by the prerender walk so hidden pages still land in the static output (they route at runtime and would 404 on the static site otherwise). One-line addition, no API break.PrerenderRunnerinternal helper inShellDocs.CLI— encapsulates the subprocess launch + readiness poll + URL walk + file save. Kept out ofBuildCommandso the orchestration reads linearly. Guarantees subprocess cleanup on every exit path (finally +ProcessExit+CancelKeyPress).BuildCommandcontent mirror safety net — always copies sourcecontent/intopublish/content/before launching. Insurance against any consumer csproj that got the copy rules wrong (like every consumer on<= 0.1.4-alphadid).Known limitations
PrerenderRunnerwalks URLs viaNavigationGraphBuilder.Build(contentRoot)— same logic the running app uses — so file-based routes are complete. Custom routes registered outsidecontent/(bespoke@pagedirectives) won't be prerendered and need to be added manually. Not a regression from0.1.4-alpha(which prerendered nothing).<script src="_framework/blazor.web.js">. Deep-linking hits the static HTML; theme toggle, search, tabs, code copy work via the shippedshelldocs.json top. Full SignalR-backed Blazor Server interactivity doesn't survive the static build — but a docs site doesn't need it.Files
Modified — 4 source + 1 test + 2 metadata:
src/ShellDocs.CLI/Commands/BuildCommand.cs— full rewrite of the flowsrc/ShellDocs.CLI/Commands/InitCommand.cs—AddContentCopyIfMissingproduces + migrates split rulessrc/ShellDocs.Core/NavigationGraph.cs—AllUrlsproperty addedtests/ShellDocs.Tests/BuildCommandTests.cs— updated to new API surface (RewriteBaseHrefInAllHtml,CopyDirectoryMerging), added coverage for tree-walk base-href and merge-vs-overwrite semanticsCHANGELOG.md— new[0.1.5-alpha]sectionDirectory.Build.props—0.1.4-alpha→0.1.5-alphaNew — 1 file:
src/ShellDocs.CLI/Commands/PrerenderRunner.cs— subprocess launcher + URL walkerConsumer-side companion change (not in this PR)
shelldocs-docs/ShellDocs.Site.csprojneeds the same split-rule content-copy fix. Not in this PR because it lives in a different repo, but it's a one-diff change yourshelldocs-docsbranch already has locally. Any consumer on<= 0.1.4-alphathat upgrades to0.1.5-alphawill get it done automatically by re-runningshelldocs init --attach(the migration path inAddContentCopyIfMissingpicks it up).