Skip to content
Merged
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
14 changes: 12 additions & 2 deletions docs/configure/content-set/api-explorer.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ Make sure you have:
- Understanding of RESTful API principles
```

:::{important}
Intro and outro Markdown files must not use a slug that would collide with the reserved API Explorer segments like `types` and `tags`.
:::

## Place your spec files

OpenAPI specification files must be in JSON format and located in the same folder as your `docset.yml` (or in a subfolder of it). The path you specify in `api` is resolved relative to the `docset.yml` location.
Expand Down Expand Up @@ -133,6 +137,7 @@ toc:
The API Explorer generates the following types of pages from your OpenAPI spec:

- **Landing page**: An overview of the API grouped by tag
- **Tag landing pages**: One page per tag that lists operations in that tag, with the tag's display name, optional OpenAPI `description` (CommonMark), and optional `externalDocs` link
- **Operation pages**: One page per API operation, with the HTTP method, path, parameters, request body, response schemas, and examples
- **Schema type pages**: Dedicated pages for complex shared types such as `QueryContainer` and `AggregationContainer`

Expand Down Expand Up @@ -214,9 +219,13 @@ Use the `x-displayName` extension (from [Redocly](https://redocly.com/docs-legac

**Behavior:**

- When `x-displayName` is present, it's used for navigation titles and section headings in the API Explorer
- When `x-displayName` is present, it's used for navigation titles, tag landing page titles, and section headings on the main API overview
- When `x-displayName` is absent, the canonical tag `name` is used as a fallback
- Navigation URLs and internal references always use the canonical tag `name` for stability
- Tag landing page URLs and tag URL segments are derived from the canonical tag `name`

:::{note}
If two different canonical tag names normalize to the same tag landing page URL, the build fails with an error that names both tags and the colliding segment so the spec can be fixed.
:::

### Tag groups [x-taggroups]

Expand All @@ -243,5 +252,6 @@ Use the document-level `x-tagGroups` extension (from [Redocly](https://redocly.c
**Behavior:**

- When `x-tagGroups` is present and valid, the API Explorer uses it as an additional level of grouping in the sidebar.
- In the navigation tree, a group's section title links to the **main API overview** for that product (it is not a separate page and does not point at the first tag in the group; tag landings stay under `.../tags/...` only for tags).
- When `x-tagGroups` is absent, tags are listed directly under the API root in a single flat layer.
- Any operation tag that is not listed under any group is still included: it appears under a fallback section named `unknown`, and the build logs a warning so you can fix the spec.
2 changes: 1 addition & 1 deletion docs/kibana-openapi.json

Large diffs are not rendered by default.

18 changes: 16 additions & 2 deletions src/Elastic.ApiExplorer/Landing/LandingNavigationItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ INodeNavigationItem<INavigationModel, INavigationItem> parent

{
/// <inheritdoc />
public string Url => NavigationItems.First().Url;
public virtual string Url => NavigationItems.First().Url;

/// <inheritdoc />
public abstract string NavigationTitle { get; }
Expand Down Expand Up @@ -100,6 +100,9 @@ INodeNavigationItem<INavigationModel, INavigationItem> parent
public class ClassificationNavigationItem(ApiClassification classification, LandingNavigationItem rootNavigation, LandingNavigationItem parent)
: ApiGroupingNavigationItem<ApiClassification, INavigationItem>(classification, rootNavigation, parent), IRootNavigationItem<ApiClassification, INavigationItem>
{
/// <summary>Section titles from <c>x-tagGroups</c> are not their own page; the sidebar link targets the main API overview for the product, not a tag (or the first child) page.</summary>
public override string Url => rootNavigation.Index.Url;

/// <inheritdoc />
public override string NavigationTitle { get; } = classification.Name;

Expand All @@ -113,9 +116,20 @@ void IAssignableChildrenNavigation.SetNavigationItems(IReadOnlyCollection<INavig
throw new NotSupportedException($"{nameof(IAssignableChildrenNavigation.SetNavigationItems)} is not supported on ${nameof(ClassificationNavigationItem)}");
}

public class TagNavigationItem(ApiTag tag, IRootNavigationItem<IApiGroupingModel, INavigationItem> rootNavigation, INodeNavigationItem<INavigationModel, INavigationItem> parent)
public class TagNavigationItem(
ApiTag tag,
string? urlPathPrefix,
string apiUrlSuffix,
IRootNavigationItem<IApiGroupingModel, INavigationItem> rootNavigation,
INodeNavigationItem<INavigationModel, INavigationItem> parent
)
: ApiGroupingNavigationItem<ApiTag, IEndpointOrOperationNavigationItem>(tag, rootNavigation, parent)
{
private readonly string _url = $"{urlPathPrefix?.TrimEnd('/')}/api/{apiUrlSuffix}/tags/{tag.TagUrlSegment}/";

/// <inheritdoc />
public override string Url => _url;

/// <inheritdoc />
public override string NavigationTitle { get; } = tag.DisplayName;

Expand Down
2 changes: 1 addition & 1 deletion src/Elastic.ApiExplorer/Landing/LandingView.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
else if (navigationItem is TagNavigationItem tag)
{
<tr>
<td colspan="2"><h3>@(tag.NavigationTitle)</h3><td>
<td colspan="2"><h3><a href="@tag.Url" class="api-tag-landing-link">@tag.NavigationTitle</a></h3></td>
</tr>
@RenderProduct(tag)
}
Expand Down
97 changes: 97 additions & 0 deletions src/Elastic.ApiExplorer/Landing/TagLandingView.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
@inherits RazorSliceHttpResult<Elastic.ApiExplorer.Landing.TagLandingViewModel>
@using Elastic.ApiExplorer
@using Elastic.ApiExplorer.Landing
@using Elastic.ApiExplorer.Operations
@using Elastic.Documentation.Navigation
@using Elastic.Documentation.Site.Navigation
@implements IUsesLayout<Elastic.ApiExplorer._Layout, ApiLayoutViewModel>
@functions {
public ApiLayoutViewModel LayoutModel => Model.CreateGlobalLayoutModel();

private IHtmlContent RenderOp(IReadOnlyCollection<OperationNavigationItem> endpointOperations)
{
<ul class="api-url-listing">
@foreach (var overload in endpointOperations)
{
var method = overload.Model.OperationType.ToString().ToLowerInvariant();
<li class="api-url-list-item">
<a href="@overload.Url" class="current api-url-list-item-landing" hx-disable="true">
<span class="api-method api-method-@method">@method.ToUpperInvariant()</span>
<span class="api-url">@overload.Model.Route</span>
</a>
</li>
}
</ul>

return HtmlString.Empty;
}

private IHtmlContent RenderTagChild(INavigationItem item)
{
if (item is INodeNavigationItem<INavigationModel, INavigationItem> node)
{
foreach (var navigationItem in node.NavigationItems)
{
if (navigationItem is EndpointNavigationItem endpoint)
{
var endpointOperations =
endpoint is { NavigationItems.Count: > 0 } && endpoint.NavigationItems.All(n => n.Hidden)
? endpoint.NavigationItems
: [];
if (endpointOperations.Count > 0)
{
<tr>
<td class="api-name">@(endpoint.NavigationTitle)</td>
<td>@RenderOp(endpointOperations)</td>
</tr>
}
else
{
@RenderTagChild(endpoint)
}
}
else if (navigationItem is OperationNavigationItem operation)
{
<tr>
<td class="api-name">@(operation.NavigationTitle)</td>
<td>@RenderOp([operation])</td>
</tr>
}
else
{
throw new System.Exception($"Unexpected type on tag landing: {navigationItem.GetType().FullName}");
}
}
}

return HtmlString.Empty;
}
}
<section id="elastic-api-v3">
<h1>@Model.Tag.DisplayName</h1>
@if (!string.Equals(Model.Tag.Name, Model.Tag.DisplayName, StringComparison.Ordinal))
{
<p class="api-tag-canonical"><code>@Model.Tag.Name</code></p>
}
@if (!string.IsNullOrEmpty(Model.Tag.Description))
{
<div class="api-tag-description">@Model.RenderMarkdown(Model.Tag.Description)</div>
}
@{
var ed = Model.Tag.ExternalDocs;
}
@if (ed is not null)
{
var externalDocsUrl = ed.Url;
var isElasticDocs = externalDocsUrl.Contains("www.elastic.co/docs", System.StringComparison.Ordinal) || externalDocsUrl.Contains("elastic.co/guide", System.StringComparison.Ordinal);
var linkText = string.IsNullOrWhiteSpace(ed.Description) ? "Documentation" : ed.Description;
<p class="api-tag-external-docs">
<a href="@externalDocsUrl" class="docs-reference-btn" @(isElasticDocs ? "" : "target=\"_blank\" rel=\"noopener noreferrer\"")>@linkText</a>
</p>
}
<div class="api-overview">
<table>
@RenderTagChild(Model.CurrentNavigationItem)
</table>
</div>
</section>
15 changes: 15 additions & 0 deletions src/Elastic.ApiExplorer/Landing/TagLandingViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// 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.ApiExplorer;

namespace Elastic.ApiExplorer.Landing;

public class TagLandingViewModel(ApiRenderContext context) : ApiViewModel(context)
{
public required ApiTag Tag { get; init; }

/// <inheritdoc />
protected override string? LayoutPageTitle => Tag.DisplayName;
}
Loading
Loading