Skip to content

Add external links to navigation #1613

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/_docset.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,5 @@ toc:
- folder: baz
children:
- file: qux.md
- link: https://github.com/elastic/docs-builder
title: GitHub repository
33 changes: 33 additions & 0 deletions docs/configure/content-set/navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,39 @@ It [may be linked to locally however](../../developer-notes.md)

#### Nesting `toc`

The `toc` key can include nested `toc.yml` files.

The following example includes two sub-`toc.yml` files located in directories named `elastic-basics` and `solutions`:

```yml
toc:
- file: index.md
- toc: elastic-basics
- toc: solutions
```

#### External links

You can include links to external websites directly in your navigation tree. Use the `link` key with a valid URL, and provide a human-friendly `title`.

```yaml
toc:
- link: https://elastic.co
title: Elastic Website
- toc: getting-started
- link: https://github.com/elastic/docs-builder
title: Docs Builder Repository
```

Docs-builder renders external links with an icon, opens them in a new tab, and adds `rel="noopener noreferrer"` for security.

Best practices:

* Use external links sparingly in your primary navigation, placing them near the bottom of a section when possible.
* Provide clear titles that indicate the link leads to an external resource.
* Periodically verify that external URLs remain valid, and update them if destinations change.


The `toc` key can include nested `toc.yml` files.

The following example includes two sub-`toc.yml` files located in directories named `elastic-basics` and `solutions`:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ private IReadOnlyCollection<ITocItem> ReadChildren(YamlStreamReader reader, KeyV
{
string? file = null;
string? folder = null;
string? link = null;
string? linkTitle = null;
string[]? detectionRules = null;
TableOfContentsConfiguration? toc = null;
var detectionRulesFound = false;
Expand All @@ -148,6 +150,12 @@ private IReadOnlyCollection<ITocItem> ReadChildren(YamlStreamReader reader, KeyV
hiddenFile = key == "hidden";
file = ReadFile(reader, entry, parentPath);
break;
case "link":
link = reader.ReadString(entry);
break;
case "title":
linkTitle = reader.ReadString(entry);
break;
case "folder":
folder = ReadFolder(reader, entry, parentPath);
parentPath += $"{Path.DirectorySeparatorChar}{folder}";
Expand Down Expand Up @@ -199,6 +207,15 @@ private IReadOnlyCollection<ITocItem> ReadChildren(YamlStreamReader reader, KeyV
return [new FileReference(this, path, hiddenFile, children ?? [])];
}

if (link is not null)
{
// external links cannot have children
if (children is not null)
reader.EmitWarning("'link' entries may not contain 'children'", tocEntry);

return [new LinkReference(this, link, linkTitle ?? link)];
}

if (folder is not null)
{
if (children is null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ public record TocReference(Uri Source, ITableOfContentsScope TableOfContentsScop
/// A phantom table of contents is a table of contents that is not rendered in the UI but is used to generate the TOC.
/// This should be used sparingly and needs explicit configuration in navigation.yml.
/// It's typically used for container TOC that holds various other TOC's where its children are rehomed throughout the navigation.
/// <para>Examples of phantom toc's:</para>
/// <list type="">
/// <item> - toc: elasticsearch://reference</item>
/// <item> - toc: docs-content://</item>
/// </list>
/// <para>Because navigation.yml does exhaustive checks to ensure all toc.yml files are referenced, marking these containers as phantoms
/// ensures that these skip validation checks
/// </para>
/// </summary>
public bool IsPhantom { get; init; }
}

/// <summary>
/// Represents an external link in the table of contents.
/// </summary>
/// <param name="TableOfContentsScope">Scope this link belongs to.</param>
/// <param name="Url">Absolute URL of the external resource.</param>
/// <param name="Title">Display title for the link.</param>
public record LinkReference(ITableOfContentsScope TableOfContentsScope, string Url, string Title) : ITocItem;

Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,14 @@
}

a {
@apply font-body text-blue-elastic hover:text-blue-elastic-100 underline;

&[target='_blank']::after {
@apply ml-0.5;
content: url("data:image/svg+xml; utf8, <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='grey' height='16'><path d='M6.22 8.72a.75.75 0 0 0 1.06 1.06l5.22-5.22v1.69a.75.75 0 0 0 1.5 0v-3.5a.75.75 0 0 0-.75-.75h-3.5a.75.75 0 0 0 0 1.5h1.69L6.22 8.72Z' /><path d='M3.5 6.75c0-.69.56-1.25 1.25-1.25H7A.75.75 0 0 0 7 4H4.75A2.75 2.75 0 0 0 2 6.75v4.5A2.75 2.75 0 0 0 4.75 14h4.5A2.75 2.75 0 0 0 12 11.25V9a.75.75 0 0 0-1.5 0v2.25c0 .69-.56 1.25-1.25 1.25h-4.5c-.69 0-1.25-.56-1.25-1.25v-4.5Z' /></svg>");
}
}
}

#pages-nav a[target='_blank']:after {
content: url("data:image/svg+xml; utf8, <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='grey' height='16'><path d='M6.22 8.72a.75.75 0 0 0 1.06 1.06l5.22-5.22v1.69a.75.75 0 0 0 1.5 0v-3.5a.75.75 0 0 0-.75-.75h-3.5a.75.75 0 0 0 0 1.5h1.69L6.22 8.72Z' /><path d='M3.5 6.75c0-.69.56-1.25 1.25-1.25H7A.75.75 0 0 0 7 4H4.75A2.75 2.75 0 0 0 2 6.75v4.5A2.75 2.75 0 0 0 4.75 14h4.5A2.75 2.75 0 0 0 12 11.25V9a.75.75 0 0 0-1.5 0v2.25c0 .69-.56 1.25-1.25 1.25h-4.5c-.69 0-1.25-.56-1.25-1.25v-4.5Z' /></svg>");
margin-left: 0.5em;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
Expand All @@ -15,7 +15,7 @@

<ItemGroup>
<ProjectReference Include="..\Elastic.Documentation\Elastic.Documentation.csproj" />
<ProjectReference Include="..\Elastic.Documentation.Configuration\Elastic.Documentation.Configuration.csproj" />
<ProjectReference Include="..\Elastic.Documentation.Configuration\Elastic.Documentation.Configuration.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// 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 Elastic.Documentation.Site.Navigation;

namespace Elastic.Documentation.Site.Navigation;

/// <summary>
/// Navigation item representing an external URL in the TOC.
/// </summary>
public record ExternalLinkNavigationItem(string ExternalUrl, string Title, INodeNavigationItem<INavigationModel, INavigationItem> Parent, IRootNavigationItem<INavigationModel, INavigationItem> NavigationRoot)
: INavigationItem
{
public INodeNavigationItem<INavigationModel, INavigationItem>? Parent { get; set; } = Parent;

public IRootNavigationItem<INavigationModel, INavigationItem> NavigationRoot { get; } = NavigationRoot;

public string Url => ExternalUrl;

public string NavigationTitle => Title;

public int NavigationIndex { get; set; }

public bool Hidden => false;
}
16 changes: 15 additions & 1 deletion src/Elastic.Documentation.Site/Navigation/_TocTreeNav.cshtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@using Elastic.Documentation.Site.Navigation

@inherits RazorSlice<NavigationTreeItem>
@{
var isTopLevel = Model.Level == 0;
Expand Down Expand Up @@ -60,7 +61,7 @@
// Only render children if we're within the allowed level depth
// MaxLevel of -1 means render all levels
bool shouldRenderChildren = Model.MaxLevel == -1 || Model.Level < (Model.MaxLevel);
<ul class="w-full hidden peer-has-checked:block ml-4">
<ul class="block w-full markdown-content hidden peer-has-checked:block ml-4">
@if (shouldRenderChildren)
{
@await RenderPartialAsync(_TocTreeNav.Create(new NavigationTreeItem
Expand Down Expand Up @@ -89,4 +90,17 @@
</a>
</li>
}
else if (item is ExternalLinkNavigationItem ext)
{
<li class="flex group/li pr-8 @(isTopLevel ? "font-semibold mt-6" : "mt-4")">
<a
href="@ext.Url"
target="_blank"
rel="noopener noreferrer"
class="sidebar-link grow group-[.current]/li:text-blue-elastic! flex items-center"
>
@ext.NavigationTitle
</a>
</li>
}
}
5 changes: 5 additions & 0 deletions src/Elastic.Markdown/IO/DocumentationSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using Elastic.Markdown.Links.CrossLinks;
using Elastic.Markdown.Myst;
using Microsoft.Extensions.Logging;
using ExternalLinkNavigationItem = Elastic.Documentation.Site.Navigation.ExternalLinkNavigationItem;

namespace Elastic.Markdown.IO;

Expand Down Expand Up @@ -227,6 +228,10 @@ private void UpdateNavigationIndex(IReadOnlyCollection<INavigationItem> navigati
documentationGroup.NavigationIndex = groupIndex;
UpdateNavigationIndex(documentationGroup.NavigationItems, ref navigationIndex);
break;
case ExternalLinkNavigationItem ext:
var linkIndex = Interlocked.Increment(ref navigationIndex);
ext.NavigationIndex = linkIndex;
break;
default:
Context.EmitError(Context.ConfigurationPath, $"Unhandled navigation item type: {item.GetType()}");
break;
Expand Down
6 changes: 6 additions & 0 deletions src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Elastic.Documentation.Configuration.TableOfContents;
using Elastic.Documentation.Extensions;
using Elastic.Documentation.Site.Navigation;
using ExternalLinkNavigationItem = Elastic.Documentation.Site.Navigation.ExternalLinkNavigationItem;

namespace Elastic.Markdown.IO.Navigation;

Expand Down Expand Up @@ -178,6 +179,11 @@ void AddToNavigationItems(INavigationItem item, ref int fileIndex)
if (indexFile != md)
AddToNavigationItems(new FileNavigationItem(md, this, file.Hidden), ref fileIndex);
}
else if (tocItem is LinkReference extLink)
{
var nav = new ExternalLinkNavigationItem(extLink.Url, extLink.Title, this, rootNavigationItem);
AddToNavigationItems(nav, ref fileIndex);
}
else if (tocItem is FolderReference folder)
{
var children = folder.Children;
Expand Down
3 changes: 3 additions & 0 deletions src/tooling/docs-assembler/Building/SitemapBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ private static IReadOnlyCollection<INavigationItem> GetNavigationItems(IReadOnly
result.AddRange(GetNavigationItems(group.NavigationItems));
result.Add(group);
break;
case ExternalLinkNavigationItem link:
// skip external links in sitemap
continue;
default:
throw new Exception($"Unhandled navigation item type: {item.GetType()}");
}
Expand Down
9 changes: 9 additions & 0 deletions src/tooling/docs-assembler/Navigation/GlobalNavigation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ private void UpdateParent(
_ = allNavigationItems.Add(documentationGroup);
UpdateParent(allNavigationItems, documentationGroup.NavigationItems, documentationGroup);
break;
case ExternalLinkNavigationItem extLink:
if (parent is not null)
extLink.Parent = parent;
_ = allNavigationItems.Add(extLink);
break;
default:
_navigationFile.EmitError($"Unhandled navigation item type: {item.GetType()}");
break;
Expand All @@ -105,6 +110,10 @@ private void UpdateNavigationIndex(IReadOnlyCollection<INavigationItem> navigati
documentationGroup.NavigationIndex = groupIndex;
UpdateNavigationIndex(documentationGroup.NavigationItems, ref navigationIndex);
break;
case ExternalLinkNavigationItem extLink:
var linkIndex = Interlocked.Increment(ref navigationIndex);
extLink.NavigationIndex = linkIndex;
break;
default:
_navigationFile.EmitError($"Unhandled navigation item type: {item.GetType()}");
break;
Expand Down
Loading