Skip to content

Commit ee08ce3

Browse files
authored
Merge pull request #36 from shellui-dev/fix/not-found-and-sidebar-collapse
fix: 404 for unknown pages, 404.html in static builds, static sidebar collapse (0.1.14-alpha)
2 parents cb539a1 + fe6841c commit ee08ce3

18 files changed

Lines changed: 354 additions & 46 deletions

File tree

‎CHANGELOG.md‎

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

55
## [Unreleased]
66

7+
## [0.1.14-alpha] — 2026-10-10
8+
9+
Static sites get the last piece of interactive chrome apart from search, the desktop sidebar collapse, and unknown pages finally answer 404, with a matching `404.html` in static exports.
10+
11+
### Added
12+
13+
- **`<DocsNotFound />`**: the docs page's not-found view, now used by the `init` template. It answers HTTP 404, so search engines skip unknown pages. The status is set as the headers go out, because .NET 10 sends an empty body for a page that is already 404 unless the app has a `NotFoundPage` or status-code re-execution. `Path`, `HomeHref` and `HomeLabel` override the message and the link back (which defaults to where `/docs` leads). Existing sites replace their `else` branch in `DocsPage.razor` with `<DocsNotFound />`.
14+
- **`shelldocs build` writes `404.html`** from the site's own not-found view, for static hosts that serve it for unknown URLs (GitHub Pages, Netlify, Cloudflare Pages). `--spa-fallback` still replaces it with `index.html`.
15+
16+
### Changed
17+
18+
- **Desktop sidebar collapse works without a Blazor runtime.** The collapse and expand buttons are `[data-sidebar-collapse-toggle]` buttons handled by `shelldocs.js`, which flips `[data-sidebar-collapsed]` on the page shell and remembers the choice in `localStorage` across pages. The page shell no longer gets a `docs-sidebar-collapsed` class; style `[data-sidebar-collapsed="true"]` instead. `SidebarCollapseState` is still registered but no longer used.
19+
- **`shelldocs build` fails on a page that renders as not found**, whether it answered 404 or rendered `<DocsNotFound>` with another status.
20+
721
## [0.1.13-alpha] — 2026-10-10
822

923
Code variants for cross-platform libraries: one preview with code tabs per platform (MAUI and Avalonia XAML, say), a page switch that picks the platform site-wide, captions, and live iframe previews. Also fixes one-line component bodies being wrapped in `<p>`.
@@ -400,7 +414,8 @@ Published to NuGet:
400414
- `<TypeTable>` is hand-authored today; XML-doc auto-generation ships in `ShellDocs.Xml` (Phase 4)
401415
- No `<DocsBreadcrumb>` opt-out — currently hides when the trail has ≤ 1 node, otherwise always renders
402416

403-
[Unreleased]: https://github.com/shellui-dev/shelldocs/compare/v0.1.13-alpha...HEAD
417+
[Unreleased]: https://github.com/shellui-dev/shelldocs/compare/v0.1.14-alpha...HEAD
418+
[0.1.14-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.14-alpha
404419
[0.1.13-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.13-alpha
405420
[0.1.12-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.12-alpha
406421
[0.1.11-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.11-alpha

‎Directory.Build.props‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
<!-- Package metadata (applies to any project with IsPackable=true) -->
1919
<PropertyGroup>
20-
<Version>0.1.13-alpha</Version>
20+
<Version>0.1.14-alpha</Version>
2121
<Authors>ShellUI</Authors>
2222
<Company>ShellUI</Company>
2323
<Copyright>Copyright © 2026 ShellUI</Copyright>

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Already have a Blazor project? Run `shelldocs init --attach` inside it: it adds
4141
```
4242
Your components win name collisions with ShellDocs' built-ins, which stay available as `<DocsCard>`, `<DocsCallout>`, `<DocsTabs>`, and so on.
4343
- **Content primitives.** `Callout`, `Card` / `CardGrid` / `LinkCard`, `Steps`, `FileTree`, `Tabs`, `CodeGroup`, `TypeTable` / `AutoTypeTable`, `ComponentPreview`, `DemoPreview`.
44-
- **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, the mobile menu, selectors, tabs, preview toolbars, the TOC and code copy work there through `shelldocs.js`. So does the theme toggle. Search, desktop sidebar collapse and stateful demos need a running Blazor app.
44+
- **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 and the theme toggle work there through `shelldocs.js`. Only search and 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`.
4545

4646
## Versioned docs
4747

‎docs/ARCHITECTURE.md‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,13 @@ Chrome that must work without a Blazor runtime follows one contract:
209209
| Tabs, CodeGroup, preview code tabs, page switch | `[data-tabs]`, `[data-tab-target]`, `[data-tab-panel]` | switch on click / arrows; `[data-tabs-sync]` groups switch together and persist in `localStorage`; a `[data-tabs-missing]` group without the chosen value shows its `[data-tab-missing]` note; with nothing saved yet, groups follow a `[data-tabs-switch]` for their key |
210210
| Iframe previews | `[data-iframe-preview][data-src][data-lazy]` | sets the iframe's `src` (plus `?theme=`) when visible (IntersectionObserver), on `[data-iframe-run]` or at once; posts `{ type: "shelldocs-theme", theme }` to loaded frames on theme change |
211211
| Mobile nav | page shell `[data-mobile-open]` | hamburger toggles; backdrop, Escape, link or navigation closes; page scroll locked on `<html>` |
212+
| Sidebar collapse | page shell `[data-sidebar-collapsed]`, `[data-sidebar-collapse-toggle]` | toggles collapse; remembered in `localStorage` and restored as soon as `shelldocs.js` runs, so the sidebar doesn't flash open |
212213
| TOC | `[data-toc-list][data-toc-ids]` | IntersectionObserver scroll-spy, re-attached on `enhancedload` |
213214
| Theme | `<html class="dark">`, `[data-theme-toggle]` | inline head script applies the saved / system theme; the toggle flips the class and CSS picks its icon; a `MutationObserver` saves any change (including another library's toggle) and pushes it to `ThemeState`; re-applied after enhanced navigation |
214215

215-
Still Blazor-only: search, the desktop sidebar-collapse button.
216+
Still Blazor-only: search.
217+
218+
**Not found.** `<DocsNotFound />` is the docs page's not-found view. It answers 404 by setting the status in `Response.OnStarting`: .NET 10 sends an empty body for a page whose status is already 404 unless the app has a `NotFoundPage` or status-code re-execution, so the status has to change only as the headers go out. It carries `data-shelldocs-not-found`, which `shelldocs build` checks for.
216219

217220
`enhancedload` is a Blazor event (`Blazor.addEventListener`), not a DOM event; `shelldocsOnEnhancedLoad(fn)` attaches once `blazor.web.js` has run.
218221

@@ -240,10 +243,11 @@ One neutral, shadcn-shaped palette of CSS variables in `ShellDocs.Tokens/tokens.
240243

241244
1. `dotnet publish -c Release` into `obj/shelldocs-publish`, then copies in any `content/` file publish left out (a csproj that copies `meta.json` but not `.md`).
242245
2. Builds the navigation graph and collects every URL (visible and hidden) plus `/`.
243-
3. `PrerenderRunner` starts the published app on a free port, requests each URL and writes `<output>/<path>/index.html`.
246+
3. `PrerenderRunner` starts the published app on a free port, requests each URL and writes `<output>/<path>/index.html`. A URL that answers 404, or renders `<DocsNotFound>`, fails the build.
244247
4. Reads the app's redirect map and writes a redirect page for each source URL without a prerendered page (meta refresh plus `location.replace`, relative to `<base href>`).
245-
5. Merges the published `wwwroot/` into the output without overwriting prerendered HTML.
246-
6. Optionally rewrites `<base href>` in every HTML file, copies `index.html` to `404.html`, and with `--site-url` writes `sitemap.xml`, `robots.txt` and `og:*` meta.
248+
5. Requests `/docs/__shelldocs-not-found__` and, if it renders `<DocsNotFound>` with a 404, writes it as `404.html`.
249+
6. Merges the published `wwwroot/` into the output without overwriting prerendered HTML.
250+
7. 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.
247251

248252
---
249253

‎docs/ROADMAP.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ Ships to `ShellDocs.Components` + `ShellDocs.Templates` + `ShellDocs.CLI` + `She
209209
**Authoring fix (`ShellDocs.Markdown`)**
210210
- `SlotExtractor.ReplaceComponentTags` no longer `.Trim()`s the raw child content of inline component tags. The Trim was stripping the first line's indent and defeating `SlotRenderer.Dedent` — Markdig then interpreted the remaining 4-space-indented lines as an indented code block. Symptom was the same "literal `<pre>` around placeholder divs" bug that had already been fixed for `razor:preview` fences; the inline-tag code path was still hitting it.
211211

212-
### ✅ Hardening releases (`0.1.3`–`0.1.13-alpha`) — shipped
212+
### ✅ Hardening releases (`0.1.3`–`0.1.14-alpha`) — shipped
213213
Driven by building the ShellUI docs on ShellDocs. Highlights (details in the CHANGELOG):
214214

215215
- `feat/build-static-prerender` (0.1.5) — `shelldocs build` prerenders every URL into a static site.
@@ -220,6 +220,7 @@ Driven by building the ShellUI docs on ShellDocs. Highlights (details in the CHA
220220
- `fix/sidebar-index-pages` (0.1.11) — index pages as sidebar section links, `scroll` preview layout and phone-width previews, scrolling tables.
221221
- `fix/fence-scanner-inline-attrs` (0.1.12) — CommonMark fences (any length, tildes), inline and named-slot tags read by the Razor tag scanner so `>` in a quoted value is safe, `<TypeTable>` rows that share a name.
222222
- `feat/preview-code-variants` (0.1.13) — code tabs on previews with sync and a missing-tab note, page switch (`AddSyncSwitch`), captions, `<IframePreview>`, XAML highlighting; one-line component bodies stay inline.
223+
- `fix/not-found-and-sidebar-collapse` (0.1.14) — `<DocsNotFound>` answers 404, `build` fails on not-found pages and writes `404.html`, desktop sidebar collapse in `shelldocs.js`.
223224
- `fix/preview-fidelity`, `feat/generics-redirects-page-title` (0.1.10) — `not-prose` previews, Razor child content, stretch layout, generic components in markdown, redirects (middleware + static pages), `RenderPageTitle`, theme sync and a static-host `ThemeToggle`.
224225

225226
### `feat/animation-polish`

‎examples/ShellDocs.Preview/Components/Pages/DocsPage.razor‎

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
@using ShellIcons.Icons
21
@page "/docs/{*Path:nonfile}"
32
@layout DocsLayout
43
@inject NavigationGraph Graph
@@ -12,19 +11,9 @@
1211
}
1312
else
1413
{
15-
<div class="doc-not-found">
16-
<h1>Page not found</h1>
17-
<p>The page <code>@Path</code> doesn't exist yet.</p>
18-
<p><a href="/docs/introduction"><ArrowLeft Size="14" style="vertical-align:middle;margin-right:0.35rem" />Back to introduction</a></p>
19-
</div>
14+
<DocsNotFound />
2015
}
2116

22-
<style>
23-
.doc-not-found { padding: 3rem 0; }
24-
.doc-not-found h1 { font-size: 1.5rem; font-weight: 600; }
25-
.doc-not-found code { background: var(--muted); padding: 0.125rem 0.375rem; border-radius: 4px; font-family: var(--font-mono); font-size: 0.875em; border: 1px solid var(--border); }
26-
</style>
27-
2817
@code {
2918
[Parameter] public string? Path { get; set; }
3019

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,9 @@ shelldocs build
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

5454
- `--base-href /my-repo/` rewrites `<base href>` for sites served from a subpath.
55-
- `--spa-fallback` copies `index.html` to `404.html`.
55+
- `--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://docs.example.com` emits `sitemap.xml`, `robots.txt` and `og:` meta tags.
5757

58-
In the static output, navigation, sidebar sections, the mobile menu, the version and package selectors, tabs, preview tabs and menus, the table of contents, the theme toggle and code copy all work through `shelldocs.js`. Search, the desktop sidebar-collapse button and stateful demo components still need a running Blazor Server app (interactive server rendering).
58+
In the static output, navigation, sidebar sections and collapse, the mobile menu, the version and package selectors, tabs, preview tabs and menus, the table of contents, the theme toggle and code copy all work through `shelldocs.js`. Search and stateful demo components still need a running Blazor Server app (interactive server rendering).
5959

6060
URLs that aren't pages (folders, version roots, unversioned URLs that moved into a version) become redirect pages in the static output, the same redirects the running app answers.

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,19 @@ void OnStreamLine(string line)
9494
var response = http.GetAsync(url).GetAwaiter().GetResult();
9595
if (!response.IsSuccessStatusCode)
9696
{
97-
AnsiConsole.MarkupLine($" [yellow]warn:[/] [cyan]{url}[/] returned [yellow]{(int)response.StatusCode}[/]");
97+
var why = response.StatusCode == HttpStatusCode.NotFound ? "404 (page not found)" : ((int)response.StatusCode).ToString();
98+
AnsiConsole.MarkupLine($" [yellow]warn:[/] [cyan]{url}[/] returned [yellow]{why}[/]");
9899
failed++;
99100
continue;
100101
}
101102
var html = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
103+
// A docs page that rendered <DocsNotFound> where the status couldn't be set.
104+
if (IsNotFoundPage(html))
105+
{
106+
AnsiConsole.MarkupLine($" [yellow]warn:[/] [cyan]{url}[/] rendered [yellow]Page not found[/]");
107+
failed++;
108+
continue;
109+
}
102110
var outPath = UrlToFilePath(url, outputDir);
103111
Directory.CreateDirectory(Path.GetDirectoryName(outPath)!);
104112
File.WriteAllText(outPath, html);
@@ -115,6 +123,7 @@ void OnStreamLine(string line)
115123

116124
var redirects = WriteRedirects(http, outputDir);
117125
if (redirects > 0) AnsiConsole.MarkupLine($"[dim]redirects:[/] wrote [cyan]{redirects}[/] redirect page(s)");
126+
if (WriteNotFoundPage(http, outputDir)) AnsiConsole.MarkupLine("[dim]not found:[/] wrote [cyan]404.html[/]");
118127
return new Result(failed == 0, rendered, failed);
119128
}
120129
finally
@@ -123,6 +132,31 @@ void OnStreamLine(string line)
123132
}
124133
}
125134

135+
internal const string NotFoundMarker = "data-shelldocs-not-found";
136+
// A URL no site has, so the docs route renders <DocsNotFound>.
137+
internal const string NotFoundProbe = "/docs/__shelldocs-not-found__";
138+
139+
internal static bool IsNotFoundPage(string html) => html.Contains(NotFoundMarker, StringComparison.Ordinal);
140+
141+
// Static hosts (GitHub Pages, Netlify, Cloudflare Pages) serve 404.html for unknown
142+
// URLs. Written from the site's own not-found view, when its docs route has one.
143+
private static bool WriteNotFoundPage(HttpClient http, string outputDir)
144+
{
145+
try
146+
{
147+
var response = http.GetAsync(NotFoundProbe).GetAwaiter().GetResult();
148+
var html = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
149+
if (response.StatusCode != HttpStatusCode.NotFound || !IsNotFoundPage(html)) return false;
150+
File.WriteAllText(Path.Combine(outputDir, "404.html"), html);
151+
return true;
152+
}
153+
catch (Exception ex)
154+
{
155+
AnsiConsole.MarkupLine($" [yellow]warn:[/] couldn't render the not-found page: {ex.Message}");
156+
return false;
157+
}
158+
}
159+
126160
private record RedirectEntry(string From, string To);
127161

128162
// The app serves its redirect map (DocsRedirectMiddleware); each source URL without

‎src/ShellDocs.Components/Chrome/DocsSidebarHeader.razor‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
@using ShellIcons.Icons
22
@inject ShellDocsOptions Options
33
@inject IJSRuntime JS
4-
@inject SidebarCollapseState Collapse
54

65
<div class="docs-sidebar-header">
76
<div class="docs-sidebar-brand-row">
87
<a class="docs-sidebar-brand" href="/">
98
<BrandLogo />
109
<span class="docs-sidebar-brand-name">@(!string.IsNullOrEmpty(Options.SiteName) ? Options.SiteName : "Docs")</span>
1110
</a>
12-
<button type="button" class="docs-sidebar-collapse" @onclick="Collapse.Toggle" aria-label="Collapse sidebar" title="Collapse sidebar">
11+
<button type="button" class="docs-sidebar-collapse" data-sidebar-collapse-toggle aria-label="Collapse sidebar" title="Collapse sidebar">
1312
<PanelLeft StrokeWidth="1.75" />
1413
</button>
1514
</div>

‎src/ShellDocs.Components/Chrome/SidebarCollapseState.cs‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
namespace ShellDocs.Components.Chrome;
22

3-
// Desktop collapse for the Sidebar layout — persistent, unlike the
4-
// temporary mobile drawer in MobileNavState.
3+
// No longer used: the sidebar collapse is driven by shelldocs.js. Kept so existing code compiles.
54
public class SidebarCollapseState
65
{
76
public bool IsCollapsed { get; private set; }

0 commit comments

Comments
 (0)