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
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ private async IAsyncEnumerable<DocumentationDocument> ExportFromUrl(
_ = response.EnsureSuccessStatusCode();

await using var stream = await response.Content.ReadAsStreamAsync(ctx);
var settings = new OpenApiReaderSettings { LeaveStreamOpen = false };
var settings = new OpenApiReaderSettings { LeaveStreamOpen = false, RuleSet = ValidationRuleSet.GetEmptyRuleSet() };
Comment thread
reakaleek marked this conversation as resolved.
var openApiDocument = await OpenApiDocument.LoadAsync(stream, settings: settings, cancellationToken: ctx);

return openApiDocument.Document;
Expand Down
8 changes: 0 additions & 8 deletions src/Elastic.ApiExplorer/Infrastructure/ApiViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ public abstract class ApiViewModel(ApiRenderContext context)

public ApiLayoutViewModel CreateGlobalLayoutModel()
{
var rootPath = BuildContext.SiteRootPath ?? GetDefaultRootPath(BuildContext.UrlPathPrefix);
var docTitle = Document.Info?.Title ?? "API Documentation";
var pageTitle = LayoutPageTitle;
var documentTitle = pageTitle is not null
Expand All @@ -73,7 +72,6 @@ public ApiLayoutViewModel CreateGlobalLayoutModel()
Next = null,
NavigationHtml = NavigationHtml,
UrlPathPrefix = BuildContext.UrlPathPrefix,
Htmx = new DefaultHtmxAttributeProvider(rootPath),
AllowIndexing = BuildContext.AllowIndexing,
CanonicalBaseUrl = BuildContext.CanonicalBaseUrl,
GoogleTagManager = new GoogleTagManagerConfiguration(),
Expand All @@ -92,10 +90,4 @@ public ApiLayoutViewModel CreateGlobalLayoutModel()
GitHubRef = BuildContext.Git.GitHubRef
};
}

private static string GetDefaultRootPath(string? urlPathPrefix)
{
var prefix = urlPathPrefix?.Trim('/') ?? "";
return string.IsNullOrEmpty(prefix) ? "/" : $"/{prefix}/";
}
}
3 changes: 2 additions & 1 deletion src/Elastic.ApiExplorer/Model/OpenApiReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public static class OpenApiReader

var settings = new OpenApiReaderSettings
{
LeaveStreamOpen = false
LeaveStreamOpen = false,
RuleSet = ValidationRuleSet.GetEmptyRuleSet()
};
Comment thread
reakaleek marked this conversation as resolved.
await using var fs = openApiSpecification.OpenRead();
var openApiDocument = await OpenApiDocument.LoadAsync(fs, settings: settings);
Expand Down
14 changes: 2 additions & 12 deletions src/Elastic.Codex/CodexViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,8 @@ public string Static(string path)
/// <summary>
/// Creates the global layout model for the page.
/// </summary>
public GlobalLayoutViewModel CreateGlobalLayoutModel(IReadOnlyList<CodexBreadcrumb>? codexBreadcrumbs = null)
{
var rootPath = BuildContext.SiteRootPath ?? GetDefaultRootPath(BuildContext.UrlPathPrefix);
return new()
public GlobalLayoutViewModel CreateGlobalLayoutModel(IReadOnlyList<CodexBreadcrumb>? codexBreadcrumbs = null) =>
new()
{
DocsBuilderVersion = ShortId.Create(BuildContext.Version),
DocSetName = CodexNavigation.NavigationTitle,
Expand All @@ -83,7 +81,6 @@ public GlobalLayoutViewModel CreateGlobalLayoutModel(IReadOnlyList<CodexBreadcru
Next = null,
NavigationHtml = NavigationHtml,
UrlPathPrefix = BuildContext.UrlPathPrefix,
Htmx = new DefaultHtmxAttributeProvider(rootPath),
AllowIndexing = BuildContext.AllowIndexing,
CanonicalBaseUrl = BuildContext.CanonicalBaseUrl,
GoogleTagManager = new GoogleTagManagerConfiguration(),
Expand All @@ -93,13 +90,6 @@ public GlobalLayoutViewModel CreateGlobalLayoutModel(IReadOnlyList<CodexBreadcru
BuildType = BuildContext.BuildType,
CodexBreadcrumbs = codexBreadcrumbs
};
}

private static string GetDefaultRootPath(string? urlPathPrefix)
{
var prefix = urlPathPrefix?.Trim('/') ?? "";
return string.IsNullOrEmpty(prefix) ? "/" : $"/{prefix}/";
}

private static string GetCodexStaticPathPrefix(string? urlPathPrefix)
{
Expand Down
3 changes: 1 addition & 2 deletions src/Elastic.Codex/Landing/LandingView.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
style="background-image: linear-gradient(to right, #e7e5e4 1px, transparent 1px), linear-gradient(to bottom, #e7e5e4 1px, transparent 1px); background-size: 20px 20px; background-position: 0 0, 0 0; mask-image: repeating-linear-gradient(to right, black 0px, black 3px, transparent 3px, transparent 8px), repeating-linear-gradient(to bottom, black 0px, black 3px, transparent 3px, transparent 8px), radial-gradient(ellipse 70% 60% at 50% 0%, #000 60%, transparent 100%); -webkit-mask-image: repeating-linear-gradient(to right, black 0px, black 3px, transparent 3px, transparent 8px), repeating-linear-gradient(to bottom, black 0px, black 3px, transparent 3px, transparent 8px), radial-gradient(ellipse 70% 60% at 50% 0%, #000 60%, transparent 100%); mask-composite: intersect; -webkit-mask-composite: source-in;">
</div>

<div class="codex-hero flex flex-col px-4 pt-10 pb-20 items-center text-center max-w-4xl mx-auto" style="position: relative; z-index: 1;" hx-select-oob="#main-container">
<div class="codex-hero flex flex-col px-4 pt-10 pb-20 items-center text-center max-w-4xl mx-auto" style="position: relative; z-index: 1;">

<div class="flex items-center justify-between gap-2 mb-10 w-full">
<img src="@LayoutModel.Static("logo-elastic-horizontal-color.svg")" alt="Elastic" class="h-10"/>
Expand Down Expand Up @@ -141,7 +141,6 @@
var innerCols = repoCount >= 5 || repoCount == 3 ? "grid-cols-1 md:grid-cols-2 lg:grid-cols-3" : repoCount >= 2 ? "grid-cols-1 md:grid-cols-2" : "grid-cols-1";
<div class="@colSpan border border-dashed border-grey-40 rounded-xl p-3">
<a href="@group.Url"
hx-select-oob="#main-container"
preload="mousedown"
class="flex items-center gap-3 group bg-white docset-card rounded-lg border border-grey-20 p-4 mb-3 hover:border-blue-elastic-90 hover:bg-blue-elastic-10 transition-colors">
<div class="docset-icon text-blue-elastic-90 bg-blue-elastic-10 group-hover:bg-blue-elastic-20 rounded-lg p-3 shrink-0">
Expand Down
1 change: 0 additions & 1 deletion src/Elastic.Codex/Landing/_CodexCard.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
}
}
<a href="@(Model.Url)"
hx-select-oob="#main-container"
preload="mousedown"
class="flex flex-col group bg-white docset-card rounded-lg border p-4 border-grey-20 hover:border-blue-elastic-90 hover:bg-blue-elastic-10 transition-colors"
>
Expand Down
4 changes: 2 additions & 2 deletions src/Elastic.Codex/Layout/_CodexHeader.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
<header class="sticky top-0 z-10 h-(--header-height) bg-ink-dark group-has-[.codex-root-landing]/body:h-0 group-has-[.codex-root-landing]/body:min-h-0 group-has-[.codex-root-landing]/body:overflow-hidden group-has-[.codex-root-landing]/body:opacity-0 group-has-[.codex-root-landing]/body:pointer-events-none">
<div class="h-full w-full max-w-full mx-auto px-4">
<div class="h-full flex md:grid md:grid-cols-[var(--max-sidebar-width)_1fr] gap-4 items-center">
<a href="@(Model.SiteLink("/"))" hx-select-oob="#main-container" class="flex items-center gap-2 font-sans font-semibold text-white no-underline hover:text-white shrink-0" aria-label="@(Model.HeaderTitle ?? Model.DocSetName)">
<a href="@(Model.SiteLink("/"))" class="flex items-center gap-2 font-sans font-semibold text-white no-underline hover:text-white shrink-0" aria-label="@(Model.HeaderTitle ?? Model.DocSetName)">
@(new HtmlString(EuiSvgIcons.GetIcon("logo_elastic", "size-6") ?? ""))
<span class="hidden md:inline">@(Model.HeaderTitle ?? Model.DocSetName)</span>
</a>
<div class="min-w-0 flex-1 md:flex-none lg:grid lg:grid-cols-[minmax(0,var(--max-text-width))_minmax(0,var(--max-sidebar-width))] justify-center md:mx-4 gap-4 items-center">
<div class="min-w-0 flex items-center justify-end md:justify-start">
<codex-search-bar></codex-search-bar>
<codex-search-bar id="codex-search-bar" hx-preserve></codex-search-bar>
</div>
<div class="hidden lg:block"></div>
</div>
Expand Down
3 changes: 0 additions & 3 deletions src/Elastic.Codex/Layout/_CodexSubHeader.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
@if (!string.IsNullOrEmpty(crumb.Url))
{
<a href="@crumb.Url"
hx-select-oob="#main-container"
preload="mousedown"
class="inline-flex items-center gap-1 font-medium hover:text-black hover:underline"
@(crumb.Title == "Home" ? " aria-label=\"Home\"" : "")>@(crumb.Title == "Home" ? new HtmlString(EuiSvgIcons.GetIcon("home", "size-4 fill-current") ?? "") : (object)crumb.Title)</a>
Expand All @@ -38,7 +37,6 @@
@if (!string.IsNullOrEmpty(home.Url))
{
<a href="@home.Url"
hx-select-oob="#main-container"
preload="mousedown"
class="inline-flex items-center font-medium hover:text-black hover:underline"
aria-label="Back">@(new HtmlString(EuiSvgIcons.GetIcon("grid", "size-4 fill-current") ?? ""))</a>
Expand All @@ -47,7 +45,6 @@
@if (!string.IsNullOrEmpty(header.Url))
{
<a href="@header.Url"
hx-select-oob="#main-container"
preload="mousedown"
class="font-medium hover:text-black hover:underline">@header.Title</a>
}
Expand Down
9 changes: 0 additions & 9 deletions src/Elastic.Codex/Page/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,6 @@
@inherits RazorSlice<Elastic.Markdown.Page.IndexViewModel>
@implements IUsesLayout<Elastic.Codex._MarkdownLayout, MarkdownLayoutViewModel>
@functions {
static string GetRootPath(string? siteRootPath, string? urlPathPrefix)
{
if (!string.IsNullOrEmpty(siteRootPath))
return siteRootPath;
var prefix = urlPathPrefix?.Trim('/') ?? "";
return string.IsNullOrEmpty(prefix) ? "/" : $"/{prefix}/";
}

static bool IsDocsetRootUrl(string? navUrl, string? docsetUrl, string? urlPathPrefix)
{
if (string.IsNullOrEmpty(docsetUrl))
Expand Down Expand Up @@ -60,7 +52,6 @@
StaticFileContentHashProvider = Model.StaticFileContentHashProvider,
ReportIssueUrl = Model.ReportIssueUrl,
Breadcrumbs = Model.Breadcrumbs,
Htmx = new DefaultHtmxAttributeProvider(GetRootPath(Model.SiteRootPath, Model.UrlPathPrefix)),
CurrentVersion = Model.CurrentDocument.YamlFrontMatter?.Layout == MarkdownPageLayout.LandingPage ? Model.VersionsConfig.VersioningSystems[0].Current : Model.CurrentVersion,
AllVersionsUrl = Model.AllVersionsUrl,
VersioningSystem = Model.VersioningSystem,
Expand Down
51 changes: 30 additions & 21 deletions src/Elastic.Documentation.Site/Assets/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import {
ATTR_URL_FULL,
} from './telemetry/semconv'
import { initTocNav } from './toc-nav'
import {
getPathFromUrl,
isExternalDocsUrl,
} from './web-components/shared/htmx/utils'
import 'htmx-ext-head-support'
import 'htmx-ext-preload'
import * as katex from 'katex'
Expand Down Expand Up @@ -254,38 +258,43 @@ document.addEventListener(
)

document.addEventListener('htmx:beforeRequest', function (event: HtmxEvent) {
if (
event.detail.requestConfig.verb === 'get' &&
event.detail.requestConfig.triggeringEvent
) {
if (event.detail.requestConfig.verb !== 'get') return
// Speculative prefetches from the preload extension must pass through
// untouched — without this, preloading a non-docs link would trigger the
// full-page-load fallback below on mere mousedown/hover.
if (event.detail.requestConfig.headers['HX-Preloaded'] === 'true') return
// Only boosted link navigation needs scoping; explicit hx-get widgets
// manage their own requests.
if (!event.detail.boosted) return
const path: string = event.detail.requestConfig.path
if (event.detail.requestConfig.triggeringEvent) {
const { ctrlKey, metaKey, shiftKey }: PointerEvent =
event.detail.requestConfig.triggeringEvent
const { name: os } = getOS()
const modifierKey: boolean = os === 'macOS' ? metaKey : ctrlKey
if (shiftKey || modifierKey) {
event.preventDefault()
window.open(
event.detail.requestConfig.path,
'_blank',
'noopener,noreferrer'
)
window.open(path, '_blank', 'noopener,noreferrer')
return
}
}
// hx-boost intercepts every same-origin link, but only internal docs URLs should
// navigate through htmx (for assembler that means /docs/*; isolated and codex own
// their whole origin). Anything else — marketing pages on the same domain, the
// separate /docs/api app — gets a normal full page load.
const docsPath = getPathFromUrl(new URL(path, location.href).pathname)
if (!docsPath || isExternalDocsUrl(docsPath)) {
event.preventDefault()
window.location.assign(path)
}
})

document.body.addEventListener(
'htmx:oobBeforeSwap',
function (event: HtmxEvent) {
// Scroll to the top of the page when the content is swapped
if (
event.target?.id === 'main-container' ||
event.target?.id === 'markdown-content' ||
event.target?.id === 'content-container'
) {
window.scrollTo(0, 0)
}
// Boosted navigations swap the whole <body>; scroll to top like a normal page load
document.body.addEventListener('htmx:afterSwap', function (event: HtmxEvent) {
if (event.target === document.body) {
window.scrollTo(0, 0)
}
)
})

document.body.addEventListener(
'htmx:responseError',
Expand Down
4 changes: 3 additions & 1 deletion src/Elastic.Documentation.Site/Assets/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,9 @@ body {
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
display: block;
z-index: 9999;
/* Elastic's global nav (elastic-nav.js) also sits at z-index 9999 and comes
later in the DOM, so a tie here would let it paint over the indicator. */
z-index: 10000;
}

.progress {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { assemblerStrategy } from './assembler'

// PR previews serve assembler under a prefix like '/elastic/docs-builder/docs/3634'
// instead of '/docs' — the strategy must key off config.rootPath, not a hardcoded '/docs'.
jest.mock('../../../../config', () => ({
config: { rootPath: '/elastic/docs-builder/docs/3634' },
}))

describe('assemblerStrategy with a non-default rootPath', () => {
it('treats rootPath-prefixed paths as docs paths', () => {
expect(
assemblerStrategy.getPathFromUrl(
'/elastic/docs-builder/docs/3634/get-started'
)
).toBe('/elastic/docs-builder/docs/3634/get-started')
})

it('rejects paths outside rootPath', () => {
expect(assemblerStrategy.getPathFromUrl('/pricing')).toBe(null)
})

it('still excludes the /api sub-app under the prefixed root', () => {
expect(
assemblerStrategy.isExternalDocsUrl(
'/elastic/docs-builder/docs/3634/api/elasticsearch'
)
).toBe(true)
})

it('accepts absolute self-links on non-elastic.co hosts (previews)', () => {
// jsdom origin is http://localhost
expect(
assemblerStrategy.getPathFromUrl(
'http://localhost/elastic/docs-builder/docs/3634/get-started'
)
).toBe('/elastic/docs-builder/docs/3634/get-started')
expect(
assemblerStrategy.getPathFromUrl(
'https://some-other-host.dev/elastic/docs-builder/docs/3634/get-started'
)
).toBe(null)
})
})
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
import { config } from '../../../../config'
import type { HtmxUrlStrategy } from './types'

// rootPath is '/docs' on prod but varies on PR previews (e.g.
// '/elastic/docs-builder/docs/3634'), so it can't be hardcoded.
const root = config.rootPath.replace(/\/$/, '')
const apiRoot = `${root}/api`
const isDocsPath = (path: string) =>
path === root || path.startsWith(`${root}/`)

export const assemblerStrategy: HtmxUrlStrategy = {
isExternalDocsUrl: (url) =>
url.startsWith('/docs/api/') || url === '/docs/api',
url === apiRoot || url.startsWith(`${apiRoot}/`),

getPathFromUrl: (url) => {
try {
if (url.startsWith('/')) return url
if (url.startsWith('/')) return isDocsPath(url) ? url : null
const parsed = new URL(url)
if (
parsed.hostname.endsWith('elastic.co') &&
parsed.pathname.startsWith('/docs')
) {
// elastic.co covers canonical links on prod; the origin check
// covers absolute self-links on previews and white-label hosts.
const isThisSite =
parsed.hostname.endsWith('elastic.co') ||
parsed.origin === window.location.origin
if (isThisSite && isDocsPath(parsed.pathname)) {
return parsed.pathname
}
return null
} catch {
return null
}
},

getFirstSegment: (path) => path.replace('/docs/', '/').split('/')[1] ?? '',

isSimpleSwapPath: (path) => {
const normalizedPath = path.endsWith('/') ? path.slice(0, -1) : path
if (normalizedPath === '/docs') return true
return path.startsWith('/docs/api/') || path === '/docs/api'
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,4 @@ export const codexStrategy: HtmxUrlStrategy = {
return null
}
},

getFirstSegment: (path) => {
const rMatch = path.match(/^\/r\/([^/]+)/)
if (rMatch) return rMatch[1]
const gMatch = path.match(/^\/g\/([^/]+)/)
if (gMatch) return gMatch[1]
return path.split('/').filter(Boolean)[0] ?? ''
},

isSimpleSwapPath: (path) => {
const normalizedPath = path.endsWith('/') ? path.slice(0, -1) : path
if (normalizedPath === '' || normalizedPath === '/') return true
return path.startsWith('/g/')
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,4 @@ export const isolatedStrategy: HtmxUrlStrategy = {
return null
}
},

getFirstSegment: (path) => path.replace('/docs/', '/').split('/')[1] ?? '',

isSimpleSwapPath: () => false,
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,4 @@
export interface HtmxUrlStrategy {
isExternalDocsUrl(url: string): boolean
getPathFromUrl(url: string): string | null
getFirstSegment(path: string): string
isSimpleSwapPath(path: string): boolean
}
Loading
Loading