From 3e2580b16d4bd6c9ea117397f5efd8bc62804e8f Mon Sep 17 00:00:00 2001 From: Shewart Date: Wed, 14 Oct 2026 15:38:29 +0200 Subject: [PATCH] fix(cli): put --base-href in front of root-relative links Root-relative links (/docs/introduction) ignore , so the sidebar, breadcrumb, header, prev/next, content links and favicons of an export built with --base-href /my-repo/ pointed outside the site. shelldocs build now prefixes root-relative href, src and data-src values in every exported page. Fixes #40 --- CHANGELOG.md | 4 ++ README.md | 2 +- docs/ARCHITECTURE.md | 2 +- .../content/docs/installation.md | 2 +- src/ShellDocs.CLI/Commands/BuildCommand.cs | 9 +++- tests/ShellDocs.Tests/BuildCommandTests.cs | 46 +++++++++++++++++++ 6 files changed, 61 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f044720..fab9d3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to ShellDocs land here. Format follows [Keep a Changelog](ht ## [Unreleased] +### Fixed + +- **Links broke on sites served from a sub-path** ([#40](https://github.com/shellui-dev/shelldocs/issues/40)). Sidebar, breadcrumb, header, prev/next, content and favicon links are root-relative (`/docs/introduction`), which ignores ``, so an export built with `--base-href /my-repo/` linked outside the site. `shelldocs build --base-href` now also puts the base in front of root-relative `href`, `src` and `data-src` values in every exported page. + ## [0.1.17-alpha] — 2026-10-14 llms.txt for AI agents, and five fixes found while writing the ShellDocs docs site. diff --git a/README.md b/README.md index 7342d4b..c926943 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Already have a Blazor project? Run `shelldocs init --attach` inside it: it adds ``` Your components win name collisions with ShellDocs' built-ins, which stay available as ``, ``, ``, and so on. - **Content primitives.** `Callout`, `Card` / `CardGrid` / `LinkCard`, `Steps`, `FileTree`, `Tabs`, `CodeGroup`, `TypeTable` / `AutoTypeTable`, `ComponentPreview`, `DemoPreview`. -- **Static export.** `shelldocs build` prerenders every page to static HTML for GitHub Pages, Cloudflare Pages, Netlify or S3. Optional flags rewrite `` (`--base-href`), add a SPA `404.html` (`--spa-fallback`), and write sitemap / robots / `og:` meta (`--site-url`). Navigation, sidebar sections and collapse, the mobile menu, selectors, tabs, preview toolbars, the TOC, code copy, the theme toggle and search work there through `shelldocs.js`. Only stateful demos need a running Blazor app. Unknown pages answer 404 (`` in the docs page) and `build` writes the same view as `404.html`. +- **Static export.** `shelldocs build` prerenders every page to static HTML for GitHub Pages, Cloudflare Pages, Netlify or S3. Optional flags rewrite `` and root-relative links for a sub-path (`--base-href`), add a SPA `404.html` (`--spa-fallback`), and write sitemap / robots / `og:` meta (`--site-url`). Navigation, sidebar sections and collapse, the mobile menu, selectors, tabs, preview toolbars, the TOC, code copy, the theme toggle and search work there through `shelldocs.js`. Only stateful demos need a running Blazor app. Unknown pages answer 404 (`` in the docs page) and `build` writes the same view as `404.html`. ## Versioned docs diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6e2bf8f..19197fb 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -266,7 +266,7 @@ One neutral, shadcn-shaped palette of CSS variables in `ShellDocs.Tokens/tokens. 6. Copies the app's search index to `_shelldocs/search.json`. 7. With `EnableLlmsTxt`, reads `/_shelldocs/llms.json` and writes each listed file (`llms.txt`, `llms-full.txt`, every page's `.md`, the agent files) at its path. Links use `--site-url`, then the app's `SiteUrl`, else they're root-relative under `--base-href`. 8. Merges the published `wwwroot/` into the output without overwriting prerendered HTML. -9. Optionally rewrites `` in every HTML file, copies `index.html` to `404.html` (replacing the not-found page), and with `--site-url` writes `sitemap.xml`, `robots.txt` and `og:*` meta. +9. Optionally rewrites `` in every HTML file and puts the base in front of root-relative `href`, `src` and `data-src` values (they ignore ``; protocol-relative and already-prefixed ones are left alone), copies `index.html` to `404.html` (replacing the not-found page), and with `--site-url` writes `sitemap.xml`, `robots.txt` and `og:*` meta. --- diff --git a/examples/ShellDocs.Preview/content/docs/installation.md b/examples/ShellDocs.Preview/content/docs/installation.md index a2d62ca..3e79bd6 100644 --- a/examples/ShellDocs.Preview/content/docs/installation.md +++ b/examples/ShellDocs.Preview/content/docs/installation.md @@ -51,7 +51,7 @@ shelldocs build `shelldocs build` prerenders every page to static HTML in `publish/` (change it with `--output`). Deploy it to GitHub Pages, Cloudflare Pages, Netlify or S3. Useful flags: -- `--base-href /my-repo/` rewrites `` for sites served from a subpath. +- `--base-href /my-repo/` rewrites `` for sites served from a subpath, and puts `/my-repo` in front of root-relative links (`/docs/introduction` becomes `/my-repo/docs/introduction`). - `--spa-fallback` copies `index.html` to `404.html`. Without it, `404.html` is the site's own not-found page, which GitHub Pages, Netlify and Cloudflare Pages serve for unknown URLs. - `--site-url https://shelldocs.shellui.dev` emits `sitemap.xml`, `robots.txt` and `og:` meta tags. diff --git a/src/ShellDocs.CLI/Commands/BuildCommand.cs b/src/ShellDocs.CLI/Commands/BuildCommand.cs index 7174dd1..286d448 100644 --- a/src/ShellDocs.CLI/Commands/BuildCommand.cs +++ b/src/ShellDocs.CLI/Commands/BuildCommand.cs @@ -153,10 +153,16 @@ internal static void CopyDirectoryMerging(string source, string dest) } } - // `baseHref` must include leading + trailing slashes ("/repo-name/"). + /* `baseHref` must include leading + trailing slashes ("/repo-name/"). Root-relative + links ignore , so the sidebar, breadcrumb, header, content links and + favicons ("/docs/intro") get the base put in front too ("/repo-name/docs/intro"). */ internal static int RewriteBaseHrefInAllHtml(string outputDir, string baseHref) { var count = 0; + var prefix = baseHref.TrimStart('/'); + var rootRelative = new Regex( + @"(\s(?:href|src|data-src)\s*=\s*([""']))/(?!/)" + (prefix.Length > 0 ? "(?!" + Regex.Escape(prefix) + ")" : ""), + RegexOptions.IgnoreCase); foreach (var html in Directory.EnumerateFiles(outputDir, "*.html", SearchOption.AllDirectories)) { var original = File.ReadAllText(html); @@ -165,6 +171,7 @@ internal static int RewriteBaseHrefInAllHtml(string outputDir, string baseHref) @"]+)\s*/?>", $"", RegexOptions.IgnoreCase); + if (prefix.Length > 0) patched = rootRelative.Replace(patched, "$1" + baseHref); if (patched != original) { File.WriteAllText(html, patched); diff --git a/tests/ShellDocs.Tests/BuildCommandTests.cs b/tests/ShellDocs.Tests/BuildCommandTests.cs index f5e2d82..caa32b4 100644 --- a/tests/ShellDocs.Tests/BuildCommandTests.cs +++ b/tests/ShellDocs.Tests/BuildCommandTests.cs @@ -72,6 +72,52 @@ public void RewriteBaseHref_HandlesQuoteVariants(string original, string href, s Assert.Contains(expected, File.ReadAllText(path)); } + // #40: root-relative links ignore , so they get the base too. + [Fact] + public void RewriteBaseHref_PrefixesRootRelativeLinks() + { + var path = Path.Combine(_tempDir, "index.html"); + File.WriteAllText(path, """ + + Intro + Home + +
+ Already + CDN + External + Relative + Anchor +
<a href="/docs/code">
+ + """); + + RewriteBaseHrefInAllHtml(_tempDir, "/repo/"); + var html = File.ReadAllText(path); + + Assert.Contains("", html); + Assert.Contains("href=\"/repo/favicon.png\"", html); + Assert.Contains("href=\"/repo/docs/introduction\"", html); + Assert.Contains("href='/repo/'", html); + Assert.Contains("src=\"/repo/img/logo.svg\"", html); + Assert.Contains("data-src=\"/repo/preview/index.html\"", html); + Assert.Contains("href=\"/repo/docs/already\"", html); + Assert.Contains("href=\"//cdn.shellui.dev/x.js\"", html); + Assert.Contains("href=\"https://shellui.dev/docs\"", html); + Assert.Contains("href=\"docs/relative\"", html); + Assert.Contains("href=\"#install\"", html); + Assert.Contains("<a href="/docs/code">", html); + } + + [Fact] + public void RewriteBaseHref_AtTheRoot_LeavesLinksAlone() + { + var path = Path.Combine(_tempDir, "index.html"); + File.WriteAllText(path, "x"); + RewriteBaseHrefInAllHtml(_tempDir, "/"); + Assert.Contains("", File.ReadAllText(path)); + } + [Fact] public void RewriteBaseHref_LeavesOtherMarkupUntouched() {