Skip to content

feat(cli): shelldocs build prerenders every URL into a static site - #24

Merged
Shewart merged 6 commits into
mainfrom
feat/build-static-prerender
Aug 28, 2026
Merged

feat(cli): shelldocs build prerenders every URL into a static site#24
Shewart merged 6 commits into
mainfrom
feat/build-static-prerender

Conversation

@Shewart

@Shewart Shewart commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes the "the output isn't actually static" gap. Before this PR, shelldocs build ran dotnet publish on the (Blazor Server) scaffold and copied wwwroot/ verbatim — which for a Server-interactive project has no index.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-docs hit this trying to ship to shellicons.shellui.dev (README blocker line).

New flow: dotnet publish → mirror content/ into publish (safety net for consumer csproj mistakes) → launch the published app on a loopback ephemeral port → walk NavigationGraph.AllUrls to enumerate every route (visible + hidden) plus /HttpClient.GetAsync each URL → save the rendered HTML to <output>/<url>/index.html. Then merge publish/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 init was emitting <Content Update="content/**/*.md;content/**/meta.json" CopyToOutputDirectory="PreserveNewest" /> — wrong verb. Update only modifies existing item metadata, and the Web SDK doesn't auto-include .md (only .json), so the .md files never got copied to bin or publish. Dev worked (source-relative ContentRoot), publish silently shipped an empty content/, prerender rendered every URL as "Page not found." Split-rule fix: Include the .md, Update the auto-included .json, both with CopyToPublishDirectory="PreserveNewest". InitCommand also migrates any pre-0.1.5-alpha broken form on re-run.

What's in

Fixed

  • shelldocs build produces a fully static site deployable to GH Pages / Cloudflare Pages / Netlify without a .NET host. Full flow rewrite in BuildCommand.Run; new PrerenderRunner handles the subprocess dance.
  • --base-href now rewrites <base href> in every prerendered HTML file, not just the root index.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 init scaffolds the correct <Content> items so publish carries content/. Old form used Update with a combined selector; broken because .md never gets auto-included. New form is a split rule (Include for .md, Update for meta.json) with both CopyToOutputDirectory and CopyToPublishDirectory. Existing (broken) form gets auto-migrated on re-run of shelldocs 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.
  • PrerenderRunner internal helper in ShellDocs.CLI — encapsulates the subprocess launch + readiness poll + URL walk + file save. Kept out of BuildCommand so the orchestration reads linearly. Guarantees subprocess cleanup on every exit path (finally + ProcessExit + CancelKeyPress).
  • BuildCommand content mirror safety net — always copies source content/ into publish/content/ before launching. Insurance against any consumer csproj that got the copy rules wrong (like every consumer on <= 0.1.4-alpha did).

Known limitations

  • URL discovery is content-folder-based, not runtime-based. PrerenderRunner walks URLs via NavigationGraphBuilder.Build(contentRoot) — same logic the running app uses — so file-based routes are complete. Custom routes registered outside content/ (bespoke @page directives) won't be prerendered and need to be added manually. Not a regression from 0.1.4-alpha (which prerendered nothing).
  • Static output is a snapshot of the initial render. Hydration on the client picks up from there via <script src="_framework/blazor.web.js">. Deep-linking hits the static HTML; theme toggle, search, tabs, code copy work via the shipped shelldocs.js on 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 flow
  • src/ShellDocs.CLI/Commands/InitCommand.csAddContentCopyIfMissing produces + migrates split rules
  • src/ShellDocs.Core/NavigationGraph.csAllUrls property added
  • tests/ShellDocs.Tests/BuildCommandTests.cs — updated to new API surface (RewriteBaseHrefInAllHtml, CopyDirectoryMerging), added coverage for tree-walk base-href and merge-vs-overwrite semantics
  • CHANGELOG.md — new [0.1.5-alpha] section
  • Directory.Build.props0.1.4-alpha0.1.5-alpha

New — 1 file:

  • src/ShellDocs.CLI/Commands/PrerenderRunner.cs — subprocess launcher + URL walker

Consumer-side companion change (not in this PR)

shelldocs-docs/ShellDocs.Site.csproj needs 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 your shelldocs-docs branch already has locally. Any consumer on <= 0.1.4-alpha that upgrades to 0.1.5-alpha will get it done automatically by re-running shelldocs init --attach (the migration path in AddContentCopyIfMissing picks it up).

… 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.
Copilot AI lite review requested due to automatic review settings August 28, 2026 17:40
@Shewart
Shewart merged commit 1d4706c into main Aug 28, 2026
2 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 build to publish, run the published app locally, crawl all doc URLs, and write prerendered HTML per-route; then merge wwwroot/ assets and rewrite <base href> across all generated HTML.
  • Fixed shelldocs init’s content-copy MSBuild items by splitting .md vs meta.json rules and adding migration for the previously broken form.
  • Added NavigationGraph.AllUrls and 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 thread Directory.Build.props
Comment on lines 20 to 22
<PropertyGroup>
<Version>0.1.4-alpha</Version>
<Version>0.1.5-alpha</Version>
<Authors>ShellUI</Authors>
@Shewart
Shewart deleted the feat/build-static-prerender branch August 29, 2026 17:22
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