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
+