Skip to content
1 change: 1 addition & 0 deletions src/Elastic.ApiExplorer/ApiViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public GlobalLayoutViewModel CreateGlobalLayoutModel() =>
Previous = null,
Next = null,
NavigationHtml = NavigationHtml,
NavigationFileName = string.Empty,
UrlPathPrefix = BuildContext.UrlPathPrefix,
AllowIndexing = BuildContext.AllowIndexing,
CanonicalBaseUrl = BuildContext.CanonicalBaseUrl,
Expand Down
7 changes: 6 additions & 1 deletion src/Elastic.ApiExplorer/OpenApiGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,12 @@ private async Task<IFileInfo> Render<T>(INavigationItem current, T page, ApiRend
renderContext = renderContext with
{
CurrentNavigation = current,
NavigationHtml = navigationHtml
NavigationHtml = navigationHtml switch
{
EmptyNavigationRenderResult => string.Empty,
OkNavigationRenderResult result => result.Html,
_ => throw new Exception("Unexpected navigation render result")
}
};
await using var stream = _writeFileSystem.FileStream.New(outputFile.FullName, FileMode.OpenOrCreate);
await page.RenderAsync(stream, renderContext, ctx);
Expand Down
14 changes: 11 additions & 3 deletions src/Elastic.Documentation.Site/Assets/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,23 @@ import { $, $$ } from 'select-dom'
import { UAParser } from 'ua-parser-js'

const { getOS } = new UAParser()
const isLazyLoadNavigationEnabled =
$('meta[property="docs:feature:lazy-load-navigation"]')?.content === 'true'

document.addEventListener('htmx:load', function (event) {
console.log('htmx:load')
console.log(event.detail)
initTocNav()
initHighlight()
initCopyButton()
initTabs()
initNav()

// We do this so that the navigation is not initialized twice
if (isLazyLoadNavigationEnabled) {
if (event.detail.elt.id === 'nav-tree') {
initNav()
}
} else {
initNav()
}
initSmoothScroll()
openDetailsWithAnchor()
initDismissibleBanner()
Expand Down
1 change: 1 addition & 0 deletions src/Elastic.Documentation.Site/Layout/_Head.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@
{
<meta property="og:url" content="@Model.CanonicalUrl" />
}
<meta property="docs:feature:lazy-load-navigation" content="@Model.Features.LazyLoadNavigation" />
4 changes: 2 additions & 2 deletions src/Elastic.Documentation.Site/Layout/_PagesNav.cshtml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
@inherits RazorSlice<Elastic.Documentation.Site.GlobalLayoutViewModel>
<aside class="sidebar bg-white fixed md:sticky shadow-2xl md:shadow-none left-[100%] group-has-[#pages-nav-hamburger:checked]/body:left-0 bottom-0 md:left-auto pl-6 md:pl-2 top-[calc(var(--offset-top)+1px)] w-[80%] md:w-auto shrink-0 border-r-1 border-r-grey-20 z-40 md:z-auto">

@if (Model.Features.LazyLoadNavigation)
@if (Model.Features.LazyLoadNavigation && !string.IsNullOrEmpty(Model.NavigationFileName))
{
<div hx-get="@(Model.CurrentNavigationItem.Url + (Model.CurrentNavigationItem.Url.EndsWith('/') ? "index.nav.html" : "/index.nav.html"))" hx-trigger="load" hx-params="nav" hx-push-url="false" hx-swap="innerHTML" hx-target="#pages-nav"></div>
<div class="hidden" hx-get="@(Model.Link(Model.NavigationFileName))" hx-trigger="load" hx-params="nav" hx-push-url="false" hx-swap="innerHTML" hx-target="#pages-nav"></div>
}
<nav
id="pages-nav"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,30 @@ public interface INavigationHtmlWriter
{
const int AllLevels = -1;

Task<string> RenderNavigation(IRootNavigationItem<INavigationModel, INavigationItem> currentRootNavigation, Uri navigationSource, int maxLevel, Cancel ctx = default);
Task<INavigationRenderResult> RenderNavigation(IRootNavigationItem<INavigationModel, INavigationItem> currentRootNavigation, Uri navigationSource,
int maxLevel, Cancel ctx = default);

async Task<string> Render(NavigationViewModel model, Cancel ctx)
{
var slice = _TocTree.Create(model);
return await slice.RenderAsync(cancellationToken: ctx);
}
}

public interface INavigationRenderResult
{
string Html { get; init; }
string Id { get; init; }
}

public record OkNavigationRenderResult : INavigationRenderResult
{
public required string Html { get; init; }
public required string Id { get; init; }
}

public record EmptyNavigationRenderResult : INavigationRenderResult
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a static property on OkNavigationRenderResult ?

e.g OkNavigationRenderResult.Empty then we can just have a single record NavigationRenderResult without an interface.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
public string Html { get; init; } = string.Empty;
public string Id { get; init; } = string.Empty;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System.Collections.Concurrent;
using Elastic.Documentation.Configuration;
using Elastic.Documentation.Extensions;

namespace Elastic.Documentation.Site.Navigation;

Expand All @@ -12,19 +13,30 @@ public class IsolatedBuildNavigationHtmlWriter(BuildContext context, IRootNaviga
{
private readonly ConcurrentDictionary<(string, int), string> _renderedNavigationCache = [];

public async Task<string> RenderNavigation(IRootNavigationItem<INavigationModel, INavigationItem> currentRootNavigation, Uri navigationSource, int maxLevel, Cancel ctx = default)
public async Task<INavigationRenderResult> RenderNavigation(IRootNavigationItem<INavigationModel, INavigationItem> currentRootNavigation,
Uri navigationSource, int maxLevel, Cancel ctx = default)
{
var navigation = context.Configuration.Features.PrimaryNavEnabled || currentRootNavigation.IsUsingNavigationDropdown
? currentRootNavigation
: siteRoot;

var id = ShortId.Create($"{(navigation.Id, maxLevel).GetHashCode()}");

if (_renderedNavigationCache.TryGetValue((navigation.Id, maxLevel), out var value))
return value;
return new OkNavigationRenderResult
{
Html = value,
Id = id
};

var model = CreateNavigationModel(navigation, maxLevel);
value = await ((INavigationHtmlWriter)this).Render(model, ctx);
_renderedNavigationCache[(navigation.Id, maxLevel)] = value;
return value;
return new OkNavigationRenderResult
{
Html = value,
Id = id
};
}

private NavigationViewModel CreateNavigationModel(IRootNavigationItem<INavigationModel, INavigationItem> navigation, int maxLevel) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@using Elastic.Documentation.Site.Navigation
@inherits RazorSlice<Elastic.Documentation.Site.Navigation.NavigationViewModel>

<div class="pb-20 font-body">
<div class="pb-20 font-body" id="nav-tree">
@{
var currentTopLevelItem = Model.TopLevelItems.FirstOrDefault(i => i.Id == Model.Tree.Id) ?? Model.Tree;
}
Expand Down
1 change: 1 addition & 0 deletions src/Elastic.Documentation.Site/_ViewModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public record GlobalLayoutViewModel
public required INavigationItem? Next { get; init; }

public required string NavigationHtml { get; init; }
public required string NavigationFileName { get; init; }
public required string? UrlPathPrefix { get; init; }
public required Uri? CanonicalBaseUrl { get; init; }
public string? CanonicalUrl => CanonicalBaseUrl is not null ?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public class DocumentationFileExporter(IFileSystem readFileSystem, IFileSystem w
public override async ValueTask ProcessFile(ProcessingFileContext context, Cancel ctx)
{
if (context.File is MarkdownFile markdown)
context.MarkdownDocument = await context.HtmlWriter.WriteAsync(context.OutputFile, markdown, context.ConversionCollector, ctx);
context.MarkdownDocument = await context.HtmlWriter.WriteAsync(context.BuildContext.DocumentationOutputDirectory, context.OutputFile, markdown, context.ConversionCollector, ctx);
else
{
if (context.OutputFile.Directory is { Exists: false })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ public override async ValueTask ProcessFile(ProcessingFileContext context, Cance
switch (context.File)
{
case DetectionRuleFile df:
context.MarkdownDocument = await htmlWriter.WriteAsync(DetectionRuleFile.OutputPath(outputFile, context.BuildContext), df, conversionCollector, ctx);
context.MarkdownDocument = await htmlWriter.WriteAsync(context.BuildContext.DocumentationOutputDirectory, DetectionRuleFile.OutputPath(outputFile, context.BuildContext), df, conversionCollector, ctx);
break;
case MarkdownFile markdown:
context.MarkdownDocument = await htmlWriter.WriteAsync(outputFile, markdown, conversionCollector, ctx);
context.MarkdownDocument = await htmlWriter.WriteAsync(context.BuildContext.DocumentationOutputDirectory, outputFile, markdown, conversionCollector, ctx);
break;
default:
if (outputFile.Directory is { Exists: false })
Expand Down
41 changes: 31 additions & 10 deletions src/Elastic.Markdown/HtmlWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.IO.Abstractions;
using Elastic.Documentation;
using Elastic.Documentation.Configuration.Builder;
using Elastic.Documentation.Extensions;
using Elastic.Documentation.Legacy;
using Elastic.Documentation.Site.FileProviders;
using Elastic.Documentation.Site.Navigation;
Expand Down Expand Up @@ -55,10 +56,14 @@ private async Task<RenderResult> RenderLayout(MarkdownFile markdown, MarkdownDoc
var html = MarkdownFile.CreateHtml(document);
await DocumentationSet.Tree.Resolve(ctx);

var fullNavigationHtml = await NavigationHtmlWriter.RenderNavigation(markdown.NavigationRoot, markdown.NavigationSource, INavigationHtmlWriter.AllLevels, ctx);
var miniNavigationHtml = DocumentationSet.Context.Configuration.Features.LazyLoadNavigation
var fullNavigationRenderResult = await NavigationHtmlWriter.RenderNavigation(markdown.NavigationRoot, markdown.NavigationSource, INavigationHtmlWriter.AllLevels, ctx);
var miniNavigationRenderResult = DocumentationSet.Context.Configuration.Features.LazyLoadNavigation
? await NavigationHtmlWriter.RenderNavigation(markdown.NavigationRoot, markdown.NavigationSource, 1, ctx)
: "lazy navigation feature disabled";
: new EmptyNavigationRenderResult();

var navigationHtmlRenderResult = DocumentationSet.Context.Configuration.Features.LazyLoadNavigation
? miniNavigationRenderResult
: fullNavigationRenderResult;

var current = PositionalNavigation.GetCurrent(markdown);
var previous = PositionalNavigation.GetPrevious(markdown);
Expand Down Expand Up @@ -103,6 +108,15 @@ private async Task<RenderResult> RenderLayout(MarkdownFile markdown, MarkdownDoc
if (PositionalNavigation.MarkdownNavigationLookup.TryGetValue("docs-content://versions.md", out var item))
allVersionsUrl = item.Url;


var navigationFileName = $"{fullNavigationRenderResult.Id}.nav.html";

_ = DocumentationSet.NavigationRenderResults.TryAdd(
fullNavigationRenderResult.Id,
fullNavigationRenderResult
);


var slice = Page.Index.Create(new IndexViewModel
{
SiteName = siteName,
Expand All @@ -118,7 +132,8 @@ private async Task<RenderResult> RenderLayout(MarkdownFile markdown, MarkdownDoc
PreviousDocument = previous,
NextDocument = next,
Parents = parents,
NavigationHtml = DocumentationSet.Configuration.Features.LazyLoadNavigation ? miniNavigationHtml : fullNavigationHtml,
NavigationHtml = navigationHtmlRenderResult.Html,
NavigationFileName = navigationFileName,
UrlPathPrefix = markdown.UrlPathPrefix,
AppliesTo = markdown.YamlFrontMatter?.AppliesTo,
GithubEditUrl = editUrl,
Expand All @@ -135,15 +150,17 @@ private async Task<RenderResult> RenderLayout(MarkdownFile markdown, MarkdownDoc
Products = allProducts,
VersionsConfig = DocumentationSet.Context.VersionsConfig
});

return new RenderResult
{
Html = await slice.RenderAsync(cancellationToken: ctx),
FullNavigationPartialHtml = fullNavigationHtml
FullNavigationPartialHtml = fullNavigationRenderResult.Html,
NavigationFileName = navigationFileName
};

}

public async Task<MarkdownDocument> WriteAsync(IFileInfo outputFile, MarkdownFile markdown, IConversionCollector? collector, Cancel ctx = default)
public async Task<MarkdownDocument> WriteAsync(IDirectoryInfo outBaseDir, IFileInfo outputFile, MarkdownFile markdown, IConversionCollector? collector, Cancel ctx = default)
{
if (outputFile.Directory is { Exists: false })
outputFile.Directory.Create();
Expand Down Expand Up @@ -171,10 +188,12 @@ public async Task<MarkdownDocument> WriteAsync(IFileInfo outputFile, MarkdownFil
collector?.Collect(markdown, document, rendered.Html);
await writeFileSystem.File.WriteAllTextAsync(path, rendered.Html, ctx);

if (DocumentationSet.Configuration.Features.LazyLoadNavigation)
{
await writeFileSystem.File.WriteAllTextAsync(path.Replace(".html", ".nav.html"), rendered.FullNavigationPartialHtml, ctx);
}
if (!DocumentationSet.Configuration.Features.LazyLoadNavigation)
return document;

var navFilePath = Path.Combine(outBaseDir.FullName, rendered.NavigationFileName);
if (!writeFileSystem.File.Exists(navFilePath))
await writeFileSystem.File.WriteAllTextAsync(navFilePath, rendered.FullNavigationPartialHtml, ctx);
return document;
}

Expand All @@ -184,4 +203,6 @@ public record RenderResult
{
public required string Html { get; init; }
public required string FullNavigationPartialHtml { get; init; }
public required string NavigationFileName { get; init; }

}
3 changes: 3 additions & 0 deletions src/Elastic.Markdown/IO/DocumentationSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.IO.Abstractions;
using System.Runtime.InteropServices;
Expand Down Expand Up @@ -129,6 +130,8 @@ public class DocumentationSet : INavigationLookups, IPositionalNavigation

public IReadOnlyCollection<IDocsBuilderExtension> EnabledExtensions { get; }

public ConcurrentDictionary<string, INavigationRenderResult> NavigationRenderResults { get; } = [];

public DocumentationSet(
BuildContext context,
ILoggerFactory logger,
Expand Down
1 change: 1 addition & 0 deletions src/Elastic.Markdown/Page/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
Next = Model.NextDocument,
Parents = Model.Parents,
NavigationHtml = Model.NavigationHtml,
NavigationFileName = Model.NavigationFileName,
UrlPathPrefix = Model.UrlPathPrefix,
GithubEditUrl = Model.GithubEditUrl,
AllowIndexing = Model.AllowIndexing,
Expand Down
2 changes: 2 additions & 0 deletions src/Elastic.Markdown/Page/IndexViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public class IndexViewModel
public required INavigationItem[] Parents { get; init; }

public required string NavigationHtml { get; init; }

public required string NavigationFileName { get; init; }
public required string CurrentVersion { get; init; }

public required string? AllVersionsUrl { get; init; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Elastic.Documentation.Extensions;
using Elastic.Documentation.Site.Navigation;
using Elastic.Markdown.IO.Navigation;

Expand Down Expand Up @@ -44,24 +45,29 @@ private bool TryGetNavigationRoot(
return true;
}

public async Task<string> RenderNavigation(IRootNavigationItem<INavigationModel, INavigationItem> currentRootNavigation, Uri navigationSource, int maxLevel = -1, Cancel ctx = default)
public async Task<INavigationRenderResult> RenderNavigation(IRootNavigationItem<INavigationModel, INavigationItem> currentRootNavigation,
Uri navigationSource, int maxLevel, Cancel ctx = default)
{
if (Phantoms.Contains(navigationSource))
return string.Empty;

if (!TryGetNavigationRoot(navigationSource, out var navigationRoot, out var navigationRootSource))
return string.Empty;
if (Phantoms.Contains(navigationSource)
|| !TryGetNavigationRoot(navigationSource, out var navigationRoot, out var navigationRootSource)
|| Phantoms.Contains(navigationRootSource)
)
return new EmptyNavigationRenderResult();

if (Phantoms.Contains(navigationRootSource))
return string.Empty;
var navigationId = ShortId.Create($"{(navigationRootSource, maxLevel).GetHashCode()}");

if (_renderedNavigationCache.TryGetValue((navigationRootSource, maxLevel), out var value))
return value;
return new OkNavigationRenderResult
{
Html = value,
Id = navigationId
};

if (navigationRootSource == new Uri("docs-content:///"))
{
_renderedNavigationCache[(navigationRootSource, maxLevel)] = string.Empty;
return string.Empty;
return new EmptyNavigationRenderResult();
}

Console.WriteLine($"Rendering navigation for {navigationRootSource}");
Expand All @@ -70,7 +76,11 @@ public async Task<string> RenderNavigation(IRootNavigationItem<INavigationModel,
value = await ((INavigationHtmlWriter)this).Render(model, ctx);
_renderedNavigationCache[(navigationRootSource, maxLevel)] = value;

return value;
return new OkNavigationRenderResult
{
Html = value,
Id = navigationId
};
}

private NavigationViewModel CreateNavigationModel(DocumentationGroup group, int maxLevel)
Expand Down
Loading
Loading