From da24970f073d9bb2fd13d8de3a088b821aaee139 Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Tue, 28 Jul 2026 22:47:06 +0200 Subject: [PATCH 1/4] chore: bump ShellDocs version to 0.1.2-alpha in InitCommand.cs --- src/ShellDocs.CLI/Commands/InitCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ShellDocs.CLI/Commands/InitCommand.cs b/src/ShellDocs.CLI/Commands/InitCommand.cs index 0a0b867..3ef9e4e 100644 --- a/src/ShellDocs.CLI/Commands/InitCommand.cs +++ b/src/ShellDocs.CLI/Commands/InitCommand.cs @@ -23,7 +23,7 @@ internal static class InitCommand // Bump with in Directory.Build.props on every release. Determines // which ShellDocs.* versions the scaffold references. If stale, consumers // scaffolding via a new CLI get old packages that lack the fresh CLI's fixes. - private const string ShellDocsVersion = "0.1.1-alpha"; + private const string ShellDocsVersion = "0.1.2-alpha"; public static int Run(string? path, string dir, bool attach, bool yes, string theme) { From 92ad3b2ac539faeefc0659a4da7ca93027ba37ae Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Tue, 28 Jul 2026 22:48:26 +0200 Subject: [PATCH 2/4] feat: add support for hidden pages in navigation graph, allowing routing without sidebar visibility --- src/ShellDocs.Core/MetaJson.cs | 6 +++ src/ShellDocs.Core/NavigationGraph.cs | 17 ++++++++- src/ShellDocs.Core/NavigationGraphBuilder.cs | 39 ++++++++++++++++++-- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/ShellDocs.Core/MetaJson.cs b/src/ShellDocs.Core/MetaJson.cs index a7e653e..bae1bbc 100644 --- a/src/ShellDocs.Core/MetaJson.cs +++ b/src/ShellDocs.Core/MetaJson.cs @@ -11,6 +11,12 @@ public class MetaJson [JsonPropertyName("pages")] public List Pages { get; set; } = new(); + // Slugs of pages or subfolders that should route (URLs resolve) but not + // appear in the sidebar tree. Useful for landing pages reached only via + // the package selector, private drafts, or archived content. + [JsonPropertyName("hidden")] + public List Hidden { get; set; } = new(); + private static readonly JsonSerializerOptions Options = new() { PropertyNameCaseInsensitive = true, diff --git a/src/ShellDocs.Core/NavigationGraph.cs b/src/ShellDocs.Core/NavigationGraph.cs index 03176d4..9fad0e6 100644 --- a/src/ShellDocs.Core/NavigationGraph.cs +++ b/src/ShellDocs.Core/NavigationGraph.cs @@ -7,12 +7,27 @@ public class NavigationGraph private readonly Dictionary _byUrl; private readonly List _flatPages; - public NavigationGraph(NavigationNode root) + public NavigationGraph(NavigationNode root, IEnumerable? hiddenPages = null) { Root = root; _byUrl = new Dictionary(StringComparer.OrdinalIgnoreCase); _flatPages = new List(); Index(root); + // Hidden pages route (URLs resolve) but never appear in the visible + // tree, so they're excluded from _flatPages (prev/next skips them). + if (hiddenPages is not null) + { + foreach (var page in hiddenPages) IndexHidden(page); + } + } + + private void IndexHidden(NavigationNode node) + { + if (node.Kind == NodeKind.Page && !string.IsNullOrEmpty(node.Url)) + { + _byUrl[Normalize(node.Url)] = node; + } + foreach (var child in node.Children) IndexHidden(child); } public NavigationNode? ResolveByUrl(string url) diff --git a/src/ShellDocs.Core/NavigationGraphBuilder.cs b/src/ShellDocs.Core/NavigationGraphBuilder.cs index 9593e47..e75da28 100644 --- a/src/ShellDocs.Core/NavigationGraphBuilder.cs +++ b/src/ShellDocs.Core/NavigationGraphBuilder.cs @@ -18,12 +18,13 @@ public static NavigationGraph Build(string contentRoot) Path = abs }; - var children = BuildFolder(abs, abs, urlPrefix: ""); + var hidden = new List(); + var children = BuildFolder(abs, abs, urlPrefix: "", hidden); LinkChildren(rootNode, children); - return new NavigationGraph(rootNode); + return new NavigationGraph(rootNode, hidden); } - private static List BuildFolder(string folder, string root, string urlPrefix) + private static List BuildFolder(string folder, string root, string urlPrefix, List hidden) { var meta = ReadMeta(folder); var mdFiles = Directory.GetFiles(folder, "*.md", SearchOption.TopDirectoryOnly); @@ -36,7 +37,7 @@ private static List BuildFolder(string folder, string root, stri var folderNameToChildren = subfolders.ToDictionary( path => System.IO.Path.GetFileName(path), - path => (folderPath: path, children: BuildFolder(path, root, CombineUrl(urlPrefix, System.IO.Path.GetFileName(path)))), + path => (folderPath: path, children: BuildFolder(path, root, CombineUrl(urlPrefix, System.IO.Path.GetFileName(path)), hidden)), StringComparer.OrdinalIgnoreCase); // No meta.json: alphabetical ordering, subfolders inline as sections. @@ -61,6 +62,32 @@ private static List BuildFolder(string folder, string root, stri var result = new List(); var consumedSlugs = new HashSet(StringComparer.OrdinalIgnoreCase); var consumedFolders = new HashSet(StringComparer.OrdinalIgnoreCase); + + // Pages/folders in meta.hidden route (URLs still resolve) but never + // appear in the sidebar tree. Seeded into consumed sets so both the + // explicit-render loop and the auto-append loop skip them; the actual + // nodes get pushed into `hidden` so the graph can still index their URLs. + foreach (var slug in meta.Hidden) + { + consumedSlugs.Add(slug); + consumedFolders.Add(slug); + if (slugToNode.TryGetValue(slug, out var hiddenPage)) + hidden.Add(hiddenPage); + if (folderNameToChildren.TryGetValue(slug, out var hiddenFolder)) + { + // Wrap the folder's children in a section node so the graph's + // Index() walk (which recurses .Children) reaches every page. + var section = new NavigationNode + { + Title = TitleFromFolderName(slug), + Kind = NodeKind.Section, + Path = hiddenFolder.folderPath + }; + LinkChildren(section, hiddenFolder.children); + hidden.Add(section); + } + } + foreach (var entry in meta.Pages) { var node = ResolveEntry(entry, slugToNode, folderNameToChildren, root, urlPrefix, consumedSlugs, consumedFolders); @@ -108,6 +135,10 @@ consumer touching meta.json. */ switch (entry) { case MetaJsonPageRef pageRef: + // Hidden entries were pre-seeded into these sets; skip so a + // slug listed in both `hidden` and `pages` stays hidden. + if (consumedSlugs.Contains(pageRef.Slug) || consumedFolders.Contains(pageRef.Slug)) + return null; if (slugToNode.TryGetValue(pageRef.Slug, out var page)) { consumedSlugs.Add(pageRef.Slug); From be987a428834e7c403912c7cb8e588f2d7dfb8ad Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Tue, 28 Jul 2026 22:48:50 +0200 Subject: [PATCH 3/4] test: add unit tests for hidden page functionality in NavigationGraphBuilder, ensuring correct sidebar exclusion and URL resolution --- .../NavigationGraphBuilderTests.cs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/ShellDocs.Tests/NavigationGraphBuilderTests.cs b/tests/ShellDocs.Tests/NavigationGraphBuilderTests.cs index 9c75b34..a813f80 100644 --- a/tests/ShellDocs.Tests/NavigationGraphBuilderTests.cs +++ b/tests/ShellDocs.Tests/NavigationGraphBuilderTests.cs @@ -139,6 +139,75 @@ public void Build_UnknownSlugInMetaJson_IsSilentlySkipped() Assert.Equal(new[] { "Real" }, titles); } + [Fact] + public void Build_HiddenSlug_IsExcludedFromSidebar_ButUrlStillResolves() + { + WriteMd("visible.md", "Visible"); + WriteMd("secret.md", "Secret"); + WriteMeta("", """{ "pages": ["visible"], "hidden": ["secret"] }"""); + + var graph = NavigationGraphBuilder.Build(_root); + var titles = graph.Root.Children.Select(c => c.Title).ToList(); + + // Sidebar shows only visible (secret excluded despite being on disk). + Assert.Equal(new[] { "Visible" }, titles); + + // But secret's URL still resolves (the whole point of `hidden` vs + // deleting the file: the dropdown/direct link still works). + var secretNode = graph.ResolveByUrl("/secret"); + Assert.NotNull(secretNode); + Assert.Equal("Secret", secretNode!.Title); + } + + [Fact] + public void Build_HiddenFolder_IsExcludedFromSidebar_ButChildUrlsResolve() + { + WriteMd("visible.md", "Visible"); + WriteMd("packages/components.md", "Components"); + WriteMd("packages/cli.md", "CLI"); + WriteMeta("", """{ "pages": ["visible"], "hidden": ["packages"] }"""); + + var graph = NavigationGraphBuilder.Build(_root); + var titles = graph.Root.Children.Select(c => c.Title).ToList(); + + // Sidebar has no "Packages" section. + Assert.Equal(new[] { "Visible" }, titles); + + // But child URLs still route via ResolveByUrl. + Assert.NotNull(graph.ResolveByUrl("/packages/components")); + Assert.NotNull(graph.ResolveByUrl("/packages/cli")); + } + + [Fact] + public void Build_HiddenTakesPrecedenceOverPages() + { + // A slug listed in BOTH hidden and pages: hidden wins. + WriteMd("alpha.md", "Alpha"); + WriteMd("beta.md", "Beta"); + WriteMeta("", """{ "pages": ["alpha", "beta"], "hidden": ["beta"] }"""); + + var graph = NavigationGraphBuilder.Build(_root); + var titles = graph.Root.Children.Select(c => c.Title).ToList(); + + Assert.Equal(new[] { "Alpha" }, titles); + Assert.NotNull(graph.ResolveByUrl("/beta")); + } + + [Fact] + public void Build_HiddenSlug_IsAlsoExcludedFromAutoAppend() + { + // No `pages` array. Without hidden support, auto-append would surface + // secret alongside visible. With hidden, secret still hidden. + WriteMd("visible.md", "Visible"); + WriteMd("secret.md", "Secret"); + WriteMeta("", """{ "hidden": ["secret"] }"""); + + var graph = NavigationGraphBuilder.Build(_root); + var titles = graph.Root.Children.Select(c => c.Title).ToList(); + + Assert.Equal(new[] { "Visible" }, titles); + } + [Fact] public void Build_MdFileNotInMetaJson_IsAppendedAfterExplicitOrdering() { From f2c120f1af4991605b268741587896c2b96b1ec1 Mon Sep 17 00:00:00 2001 From: Shephard Tseisi Date: Tue, 28 Jul 2026 22:49:42 +0200 Subject: [PATCH 4/4] chore: update CHANGELOG for 0.1.2-alpha release, documenting the addition of hidden pages functionality and version bump in Directory.Build.props --- CHANGELOG.md | 23 ++++++++++++++++++++++- Directory.Build.props | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 602e7dd..260b0b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ All notable changes to ShellDocs land here. Format follows [Keep a Changelog](ht ## [Unreleased] +## [0.1.2-alpha] — 2026-07-28 + +Dogfood-driven addition. Surfaced while building shelldocs.dev: the framework had no way to route to a page without also showing it in the sidebar. Fine for typical docs, blocker for landing pages reached via the sidebar package selector (they'd render redundantly in the sidebar tree AND be the dropdown target). + +### Added + +- **`meta.json` `hidden` array.** New optional field alongside `title` / `pages`. Slugs listed there route (URLs resolve, direct links + package-selector navigation work) but never appear in the sidebar tree. Takes precedence over `pages` — a slug listed in both stays hidden. + ```json + { + "title": "Documentation", + "pages": ["introduction", "getting-started"], + "hidden": ["components", "cli", "markdown"] + } + ``` +- **`NavigationGraph` constructor gains an optional `hiddenPages` parameter.** Hidden pages get indexed into the URL lookup but are excluded from `_flatPages` (so `GetPrevNext` skips them) and never appear as `Root.Children` (so sidebar tree and `Flatten()` skip them). Not intended for direct consumer use — `NavigationGraphBuilder.Build()` produces the collection during folder walking. + +### Test coverage + +Four new `NavigationGraphBuilderTests`: hidden slug excluded from sidebar but URL resolves, hidden folder excluded from sidebar but child URLs resolve, `hidden` takes precedence over `pages`, hidden slug excluded from auto-append. + ## [0.1.1-alpha] — 2026-07-25 First point-release after the dogfood smoke of `0.1.0-alpha`. Three consumer-blocking fixes plus release-workflow hardening. @@ -109,6 +129,7 @@ Published to NuGet: - `` is hand-authored today; XML-doc auto-generation ships in `ShellDocs.Xml` (Phase 4) - No `` opt-out — currently hides when the trail has ≤ 1 node, otherwise always renders -[Unreleased]: https://github.com/shellui-dev/shelldocs/compare/v0.1.1-alpha...HEAD +[Unreleased]: https://github.com/shellui-dev/shelldocs/compare/v0.1.2-alpha...HEAD +[0.1.2-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.2-alpha [0.1.1-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.1-alpha [0.1.0-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.0-alpha diff --git a/Directory.Build.props b/Directory.Build.props index 6e5ca5f..f4bbdce 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,7 +18,7 @@ - 0.1.1-alpha + 0.1.2-alpha ShellUI ShellUI Copyright © 2026 ShellUI