Skip to content

Commit e7ca2ee

Browse files
authored
Merge pull request #47 from shellui-dev/fix/base-href-links
fix(cli): put --base-href in front of root-relative links
2 parents 53c675b + 3e2580b commit e7ca2ee

6 files changed

Lines changed: 61 additions & 4 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to ShellDocs land here. Format follows [Keep a Changelog](ht
44

55
## [Unreleased]
66

7+
### Fixed
8+
9+
- **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 `<base href>`, 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.
10+
711
## [0.1.17-alpha] — 2026-10-14
812

913
llms.txt for AI agents, and five fixes found while writing the ShellDocs docs site.

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Already have a Blazor project? Run `shelldocs init --attach` inside it: it adds
5858
```
5959
Your components win name collisions with ShellDocs' built-ins, which stay available as `<DocsCard>`, `<DocsCallout>`, `<DocsTabs>`, and so on.
6060
- **Content primitives.** `Callout`, `Card` / `CardGrid` / `LinkCard`, `Steps`, `FileTree`, `Tabs`, `CodeGroup`, `TypeTable` / `AutoTypeTable`, `ComponentPreview`, `DemoPreview`.
61-
- **Static export.** `shelldocs build` prerenders every page to static HTML for GitHub Pages, Cloudflare Pages, Netlify or S3. Optional flags rewrite `<base href>` (`--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 (`<DocsNotFound />` in the docs page) and `build` writes the same view as `404.html`.
61+
- **Static export.** `shelldocs build` prerenders every page to static HTML for GitHub Pages, Cloudflare Pages, Netlify or S3. Optional flags rewrite `<base href>` 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 (`<DocsNotFound />` in the docs page) and `build` writes the same view as `404.html`.
6262

6363
## Versioned docs
6464

‎docs/ARCHITECTURE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ One neutral, shadcn-shaped palette of CSS variables in `ShellDocs.Tokens/tokens.
266266
6. Copies the app's search index to `_shelldocs/search.json`.
267267
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`.
268268
8. Merges the published `wwwroot/` into the output without overwriting prerendered HTML.
269-
9. Optionally rewrites `<base href>` 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.
269+
9. Optionally rewrites `<base href>` in every HTML file and puts the base in front of root-relative `href`, `src` and `data-src` values (they ignore `<base href>`; 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.
270270

271271
---
272272

‎examples/ShellDocs.Preview/content/docs/installation.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ shelldocs build
5151

5252
`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:
5353

54-
- `--base-href /my-repo/` rewrites `<base href>` for sites served from a subpath.
54+
- `--base-href /my-repo/` rewrites `<base href>` for sites served from a subpath, and puts `/my-repo` in front of root-relative links (`/docs/introduction` becomes `/my-repo/docs/introduction`).
5555
- `--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.
5656
- `--site-url https://shelldocs.shellui.dev` emits `sitemap.xml`, `robots.txt` and `og:` meta tags.
5757

‎src/ShellDocs.CLI/Commands/BuildCommand.cs‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,16 @@ internal static void CopyDirectoryMerging(string source, string dest)
153153
}
154154
}
155155

156-
// `baseHref` must include leading + trailing slashes ("/repo-name/").
156+
/* `baseHref` must include leading + trailing slashes ("/repo-name/"). Root-relative
157+
links ignore <base href>, so the sidebar, breadcrumb, header, content links and
158+
favicons ("/docs/intro") get the base put in front too ("/repo-name/docs/intro"). */
157159
internal static int RewriteBaseHrefInAllHtml(string outputDir, string baseHref)
158160
{
159161
var count = 0;
162+
var prefix = baseHref.TrimStart('/');
163+
var rootRelative = new Regex(
164+
@"(\s(?:href|src|data-src)\s*=\s*([""']))/(?!/)" + (prefix.Length > 0 ? "(?!" + Regex.Escape(prefix) + ")" : ""),
165+
RegexOptions.IgnoreCase);
160166
foreach (var html in Directory.EnumerateFiles(outputDir, "*.html", SearchOption.AllDirectories))
161167
{
162168
var original = File.ReadAllText(html);
@@ -165,6 +171,7 @@ internal static int RewriteBaseHrefInAllHtml(string outputDir, string baseHref)
165171
@"<base\s+href\s*=\s*(""[^""]*""|'[^']*'|[^\s>]+)\s*/?>",
166172
$"<base href=\"{baseHref}\" />",
167173
RegexOptions.IgnoreCase);
174+
if (prefix.Length > 0) patched = rootRelative.Replace(patched, "$1" + baseHref);
168175
if (patched != original)
169176
{
170177
File.WriteAllText(html, patched);

‎tests/ShellDocs.Tests/BuildCommandTests.cs‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,52 @@ public void RewriteBaseHref_HandlesQuoteVariants(string original, string href, s
7272
Assert.Contains(expected, File.ReadAllText(path));
7373
}
7474

75+
// #40: root-relative links ignore <base href>, so they get the base too.
76+
[Fact]
77+
public void RewriteBaseHref_PrefixesRootRelativeLinks()
78+
{
79+
var path = Path.Combine(_tempDir, "index.html");
80+
File.WriteAllText(path, """
81+
<html><head><base href="/" /><link rel="icon" href="/favicon.png" /></head><body>
82+
<a class="sidebar-item" href="/docs/introduction">Intro</a>
83+
<a class="docs-header-brand" href='/'>Home</a>
84+
<img src="/img/logo.svg" />
85+
<div data-iframe-preview data-src="/preview/index.html"></div>
86+
<a href="/repo/docs/already">Already</a>
87+
<a href="//cdn.shellui.dev/x.js">CDN</a>
88+
<a href="https://shellui.dev/docs">External</a>
89+
<a href="docs/relative">Relative</a>
90+
<a href="#install">Anchor</a>
91+
<pre><code>&lt;a href=&quot;/docs/code&quot;&gt;</code></pre>
92+
</body></html>
93+
""");
94+
95+
RewriteBaseHrefInAllHtml(_tempDir, "/repo/");
96+
var html = File.ReadAllText(path);
97+
98+
Assert.Contains("<base href=\"/repo/\" />", html);
99+
Assert.Contains("href=\"/repo/favicon.png\"", html);
100+
Assert.Contains("href=\"/repo/docs/introduction\"", html);
101+
Assert.Contains("href='/repo/'", html);
102+
Assert.Contains("src=\"/repo/img/logo.svg\"", html);
103+
Assert.Contains("data-src=\"/repo/preview/index.html\"", html);
104+
Assert.Contains("href=\"/repo/docs/already\"", html);
105+
Assert.Contains("href=\"//cdn.shellui.dev/x.js\"", html);
106+
Assert.Contains("href=\"https://shellui.dev/docs\"", html);
107+
Assert.Contains("href=\"docs/relative\"", html);
108+
Assert.Contains("href=\"#install\"", html);
109+
Assert.Contains("&lt;a href=&quot;/docs/code&quot;&gt;", html);
110+
}
111+
112+
[Fact]
113+
public void RewriteBaseHref_AtTheRoot_LeavesLinksAlone()
114+
{
115+
var path = Path.Combine(_tempDir, "index.html");
116+
File.WriteAllText(path, "<html><head><base href=\"/old/\" /></head><body><a href=\"/docs/x\">x</a></body></html>");
117+
RewriteBaseHrefInAllHtml(_tempDir, "/");
118+
Assert.Contains("<a href=\"/docs/x\">", File.ReadAllText(path));
119+
}
120+
75121
[Fact]
76122
public void RewriteBaseHref_LeavesOtherMarkupUntouched()
77123
{

0 commit comments

Comments
 (0)