Class
Sealed
public sealed class NavigationBuildPhase : IBuildPhase
Namespace: Moka.Docs.Engine.Phases
Generates the sidebar navigation tree from the folder structure, front matter, and config overrides.
Inheritance
Inherits from: IBuildPhase
Constructors
| Name | Description |
|---|---|
NavigationBuildPhase(NavigationBuildPhase> logger) |
Generates the sidebar navigation tree from the folder structure, front matter, and config overrides. |
NavigationBuildPhase(NavigationBuildPhase> logger)
NavigationBuildPhase.NavigationBuildPhase(ILogger<NavigationBuildPhase> logger)
Generates the sidebar navigation tree from the folder structure, front matter, and config overrides.
Properties
Methods
| Name | Description |
|---|---|
ExecuteAsync(BuildContext context, CancellationToken ct) |
Type Relationships
classDiagram
style NavigationBuildPhase fill:#f9f,stroke:#333,stroke-width:2px
NavigationBuildPhase --|> IBuildPhase : inherits
View Source
/// <summary>
/// Generates the sidebar navigation tree from the folder structure, front matter, and config overrides.
/// </summary>
public sealed class NavigationBuildPhase(ILogger<NavigationBuildPhase> logger) : IBuildPhase
{
/// <inheritdoc/>
public string Name => "NavigationBuild";
/// <inheritdoc/>
public int Order => 600;
/// <inheritdoc/>
public Task ExecuteAsync(BuildContext context, CancellationToken ct = default)
{
var items = new List<NavigationNode>();
// If explicit nav config exists, use it as the skeleton
if (context.Config.Nav.Count > 0)
{
items = BuildFromConfig(context.Config.Nav, context.Pages);
}
else
// Auto-generate from page routes
{
items = BuildFromPages(context.Pages);
}
context.Navigation = new NavigationTree
{
Items = items
};
logger.LogInformation("Built navigation tree with {Count} top-level items", items.Count);
return Task.CompletedTask;
}
private static List<NavigationNode> BuildFromConfig(List<NavItem> navItems, List<DocPage> pages)
{
return navItems.Select(item =>
{
List<NavigationNode> children = item.Children.Count > 0 ? BuildFromConfig(item.Children, pages) : BuildChildrenFromPath(item.Path ?? "", pages);
// Check if the nav item's path points to an actual page
DocPage? matchingPage = pages.FirstOrDefault(p => string.Equals(p.Route, item.Path, StringComparison.OrdinalIgnoreCase));
// If no page exists at this path, resolve to first child's route
// so clicking doesn't hit a redirect page
string? resolvedRoute = item.Path;
if (matchingPage is null && !string.IsNullOrEmpty(item.Path) && children.Count > 0)
{
resolvedRoute = children[0].Route;
}
return new NavigationNode
{
Label = item.Label,
Route = resolvedRoute,
Icon = item.Icon,
Expanded = item.Expanded,
Children = children
};
}).ToList();
}
private static List<NavigationNode> BuildChildrenFromPath(string parentPath, List<DocPage> pages)
{
if (string.IsNullOrEmpty(parentPath))
{
return[];
}
return pages.Where(p => p.Route.StartsWith(parentPath + "/", StringComparison.OrdinalIgnoreCase) && p.Route[parentPath.Length..].Count(c => c == '/') == 1 && p.FrontMatter.Visibility == PageVisibility.Public).OrderBy(p => p.FrontMatter.Order).ThenBy(p => p.FrontMatter.Title, StringComparer.OrdinalIgnoreCase).Select(p => new NavigationNode { Label = p.FrontMatter.Title, Route = p.Route, Icon = p.FrontMatter.Icon, Order = p.FrontMatter.Order, Expanded = p.FrontMatter.Expanded, Children = BuildChildrenFromPath(p.Route, pages) }).ToList();
}
private static List<NavigationNode> BuildFromPages(List<DocPage> pages)
{
var publicPages = pages.Where(p => p.FrontMatter.Visibility == PageVisibility.Public).OrderBy(p => p.FrontMatter.Order).ThenBy(p => p.FrontMatter.Title, StringComparer.OrdinalIgnoreCase).ToList();
// Group by top-level path segment
var groups = new Dictionary<string, List<DocPage>>(StringComparer.OrdinalIgnoreCase);
foreach (DocPage page in publicPages)
{
string[] segments = page.Route.Trim('/').Split('/', 2);
string topLevel = segments.Length > 0 ? segments[0] : "";
if (!groups.TryGetValue(topLevel, out List<DocPage>? list))
{
list = [];
groups[topLevel] = list;
}
list.Add(page);
}
return groups.Select(g =>
{
DocPage? rootPage = g.Value.FirstOrDefault(p => p.Route.Trim('/') == g.Key || p.Route.Trim('/') == g.Key + "/index");
string label = rootPage?.FrontMatter.Title ?? FormatLabel(g.Key);
return new NavigationNode
{
Label = label,
Route = rootPage?.Route ?? "/" + g.Key,
Order = rootPage?.FrontMatter.Order ?? 0,
Children = g.Value.Where(p => p != rootPage).Select(p => new NavigationNode { Label = p.FrontMatter.Title, Route = p.Route, Icon = p.FrontMatter.Icon, Order = p.FrontMatter.Order }).OrderBy(n => n.Order).ThenBy(n => n.Label, StringComparer.OrdinalIgnoreCase).ToList()
};
}).OrderBy(n => n.Order).ThenBy(n => n.Label, StringComparer.OrdinalIgnoreCase).ToList();
}
private static string FormatLabel(string pathSegment)
{
if (string.IsNullOrEmpty(pathSegment))
{
return "Home";
}
return char.ToUpper(pathSegment[0]) + pathSegment[1..].Replace('-', ' ');
}
}