Skip to content

Commit c690629

Browse files
Codex URL structure and category navigation (#2624)
* Codex URL structure and category navigation * Potential fix for pull request finding 'Dereferenced variable may be null' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
1 parent dc9ed92 commit c690629

15 files changed

Lines changed: 1182 additions & 199 deletions

File tree

config/codex.example.yml

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
# Site prefix for all codex URLs
1212
# All documentation sets will be served under this prefix
1313
# e.g., /internal-docs, /dev-docs, /docs
14-
site_prefix: /internal-docs
14+
# Set to "/" or omit for no prefix (site hosted at root, e.g., codex.elastic.dev)
15+
site_prefix: /
1516

1617
# Title displayed on the codex index page
1718
title: "Elastic's Internal Dev Docs"
@@ -40,13 +41,27 @@ documentation_sets:
4041
display_name: "Content Architecture"
4142
icon: architecture
4243

43-
# Benchmarking documentation in observability category
44+
# Observability group (multiple members)
45+
- name: docs-eng-team
46+
branch: feature/test
47+
category: docs
48+
repo_name: docs-eng-team
49+
display_name: "Docs Engineering"
50+
icon: documentation
51+
4452
- name: docs-eng-team
4553
branch: main
4654
category: observability
47-
repo_name: benchmarking
48-
display_name: "Benchmarking Elasticsearch"
49-
icon: benchmark
55+
repo_name: uptime-docs
56+
display_name: "Uptime Monitoring"
57+
icon: observability
58+
59+
- name: docs-eng-team
60+
branch: main
61+
category: observability
62+
repo_name: apm-agent-docs
63+
display_name: "APM Agent"
64+
icon: apm
5065

5166
# Migration Guide in tooling category
5267
- name: docs-eng-team
@@ -96,9 +111,10 @@ documentation_sets:
96111
# Defaults to "docs".
97112
#
98113
# category (optional):
99-
# Group documentation sets under a category.
100-
# Creates URL structure: /{site_prefix}/{category}/{repo_name}/
101-
# If not specified, URL is: /{site_prefix}/{repo_name}/
114+
# Group documentation sets under a category (group).
115+
# Repos use stable URLs: /r/{repo_name}/ (or /{site_prefix}/r/{repo_name}/ with prefix)
116+
# Group landing: /g/{category}/ (or /{site_prefix}/g/{category}/ with prefix)
117+
# Group membership only affects which sidebar navigation is shown, not the URL.
102118
#
103119
# repo_name (optional):
104120
# Override the name used for checkout directories and URL paths.

src/Elastic.Codex/Building/CodexBuildService.cs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public async Task<CodexBuildResult> BuildAll(
6969
foreach (var buildContext in buildContexts)
7070
await BuildDocumentationSet(context, buildContext, ctx);
7171

72-
// Phase 4: Generate codex index and category pages using CodexGenerator
72+
// Phase 4: Generate codex landing and category pages
7373
if (buildContexts.Count > 0)
7474
await GenerateCodexPages(context, buildContexts[0].BuildContext, codexNavigation, ctx);
7575

@@ -86,18 +86,19 @@ public async Task<CodexBuildResult> BuildAll(
8686

8787
try
8888
{
89-
// Calculate output path based on category (using ResolvedRepoName for directory/URL paths)
90-
// Include site prefix in output path so file structure matches URL structure
89+
// All repos use stable /r/repoName paths (group-independent)
9190
var repoName = checkout.Reference.ResolvedRepoName;
92-
var sitePrefix = context.Configuration.SitePrefix.Trim('/');
93-
var outputPath = string.IsNullOrEmpty(checkout.Reference.Category)
94-
? fileSystem.Path.Combine(context.OutputDirectory.FullName, sitePrefix, repoName)
95-
: fileSystem.Path.Combine(context.OutputDirectory.FullName, sitePrefix, checkout.Reference.Category, repoName);
91+
var sitePrefix = context.Configuration.SitePrefix?.Trim('/') ?? "";
9692

97-
// Calculate URL path prefix
98-
var pathPrefix = string.IsNullOrEmpty(checkout.Reference.Category)
99-
? $"{context.Configuration.SitePrefix}/{repoName}"
100-
: $"{context.Configuration.SitePrefix}/{checkout.Reference.Category}/{repoName}";
93+
// Build output path: {outputDir}/{sitePrefix}/r/{repoName} or {outputDir}/r/{repoName} if no prefix
94+
var outputPath = string.IsNullOrEmpty(sitePrefix)
95+
? fileSystem.Path.Combine(context.OutputDirectory.FullName, "r", repoName)
96+
: fileSystem.Path.Combine(context.OutputDirectory.FullName, sitePrefix, "r", repoName);
97+
98+
// Build URL path prefix: /r/{repoName} or /{sitePrefix}/r/{repoName}
99+
var pathPrefix = string.IsNullOrEmpty(sitePrefix)
100+
? $"/r/{repoName}"
101+
: $"/{sitePrefix}/r/{repoName}";
101102

102103
// Create git checkout information
103104
var git = new GitCheckoutInformation
@@ -151,8 +152,6 @@ private async Task BuildDocumentationSet(
151152

152153
try
153154
{
154-
// Use the documentation set's own navigation for traversal (file lookups, prev/next)
155-
// TODO: Create a CodexNavigationHtmlWriter to render codex-wide navigation
156155
_ = await isolatedBuildService.BuildDocumentationSet(
157156
buildContext.DocumentationSet,
158157
null, // Use doc set's navigation for traversal

src/Elastic.Codex/CodexGenerator.cs

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
using System.IO.Abstractions;
66
using System.Text.RegularExpressions;
7-
using Elastic.Codex.Category;
7+
using Elastic.Codex.Group;
88
using Elastic.Codex.Landing;
99
using Elastic.Codex.Navigation;
1010
using Elastic.Documentation.Configuration;
@@ -63,14 +63,9 @@ public async Task Generate(CodexNavigation codexNavigation, Cancel ctx = default
6363
// Render the codex landing page
6464
await RenderLandingPage(codexNavigation, renderContext, navigationRenderer, ctx);
6565

66-
// Render category pages
67-
foreach (var item in codexNavigation.NavigationItems)
68-
{
69-
if (item is CategoryNavigation category)
70-
{
71-
await RenderCategoryPage(category, renderContext, navigationRenderer, ctx);
72-
}
73-
}
66+
// Render group landing pages (/g/slug)
67+
foreach (var groupNav in codexNavigation.GroupNavigations)
68+
await RenderGroupLandingPage(groupNav, renderContext, navigationRenderer, ctx);
7469
}
7570

7671
private async Task ExtractEmbeddedStaticResources(Cancel ctx)
@@ -138,42 +133,40 @@ private async Task RenderLandingPage(
138133

139134
await using var stream = _writeFileSystem.FileStream.New(outputFile.FullName, FileMode.Create);
140135
await slice.RenderAsync(stream, cancellationToken: ctx);
141-
142-
143136
_logger.LogDebug("Generated codex landing page: {Path}", outputFile.FullName);
144137
}
145138

146-
private async Task RenderCategoryPage(
147-
CategoryNavigation category,
139+
private async Task RenderGroupLandingPage(
140+
GroupNavigation groupNav,
148141
CodexRenderContext renderContext,
149142
CodexNavigationHtmlWriter navigationRenderer,
150143
CancellationToken ctx)
151144
{
152145
var navigationRenderResult = await navigationRenderer.RenderNavigation(
153-
category.NavigationRoot,
154-
category.Index,
146+
groupNav,
147+
groupNav.Index,
155148
ctx);
156149

157150
renderContext = renderContext with
158151
{
159-
CurrentNavigation = category.Index,
152+
CurrentNavigation = groupNav.Index,
160153
NavigationHtml = navigationRenderResult.Html
161154
};
162155

163-
var viewModel = new CategoryViewModel(renderContext)
156+
var viewModel = new GroupLandingViewModel(renderContext)
164157
{
165-
Category = category
158+
Group = groupNav
166159
};
167160

168-
var outputFile = GetOutputFile(category.Url);
161+
var outputFile = GetOutputFile(groupNav.Url);
169162
if (!outputFile.Directory!.Exists)
170163
outputFile.Directory.Create();
171164

172165
await using var stream = _writeFileSystem.FileStream.New(outputFile.FullName, FileMode.Create);
173-
var slice = CategoryView.Create(viewModel);
166+
var slice = GroupLandingView.Create(viewModel);
174167
await slice.RenderAsync(stream, cancellationToken: ctx);
175168

176-
_logger.LogDebug("Generated category page: {Path}", outputFile.FullName);
169+
_logger.LogDebug("Generated group landing page: {Path}", outputFile.FullName);
177170
}
178171

179172
private IFileInfo GetOutputFile(string url)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
@inherits RazorSliceHttpResult<Elastic.Codex.Group.GroupLandingViewModel>
2+
@using Elastic.Codex.Group
3+
@using Elastic.Codex.Navigation
4+
@implements IUsesLayout<Elastic.Codex._Layout, GlobalLayoutViewModel>
5+
@functions {
6+
public GlobalLayoutViewModel LayoutModel => Model.CreateGlobalLayoutModel();
7+
}
8+
<section id="elastic-docs-v3" class="codex-group-page">
9+
<div class="codex-header py-8">
10+
<h1 class="text-3xl font-bold mb-4">@Model.Group.DisplayTitle</h1>
11+
<p class="text-gray-60">Documentation sets in this group</p>
12+
</div>
13+
14+
<div class="codex-grid grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-8">
15+
@foreach (var docSet in Model.DocumentationSets.OrderBy(ds => ds.Title))
16+
{
17+
<a href="@docSet.Url"
18+
hx-select-oob="#main-container"
19+
preload="mousedown"
20+
class="codex-card block p-6 rounded-lg border border-gray-20 hover:border-blue-40 hover:shadow-md transition-all">
21+
<div class="codex-card-content">
22+
<h3 class="text-xl font-medium mb-2 text-gray-80">@docSet.Title</h3>
23+
<div class="codex-card-meta text-sm text-gray-50">
24+
<span>@docSet.PageCount pages</span>
25+
</div>
26+
</div>
27+
</a>
28+
}
29+
</div>
30+
</section>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using Elastic.Codex.Navigation;
6+
7+
namespace Elastic.Codex.Group;
8+
9+
/// <summary>
10+
/// View model for a group landing page (/g/slug) that lists only documentation sets in that group.
11+
/// </summary>
12+
public class GroupLandingViewModel(CodexRenderContext context) : CodexViewModel(context)
13+
{
14+
/// <summary>
15+
/// The group navigation (landing + repos in this group).
16+
/// </summary>
17+
public required GroupNavigation Group { get; init; }
18+
19+
/// <summary>
20+
/// Documentation sets in this group (for the group landing cards).
21+
/// </summary>
22+
public IEnumerable<CodexDocumentationSetInfo> DocumentationSets => Group.DocumentationSetInfos;
23+
}

0 commit comments

Comments
 (0)