From 7d272dba0c08ee256f7916daef3727d6b68ae10a Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 24 Jul 2026 09:40:20 +0200 Subject: [PATCH 1/3] Fix CopyBrandingResources failure during serve in-memory build source.CopyTo() operates within the read filesystem, which fails when the write filesystem is a separate in-memory MockFileSystem (serve path) because _static only exists on the write side. Use the same cross-FS copy pattern as DocumentationFileExporterBase.CopyFileFsAware. Co-Authored-By: Claude Sonnet 4.6 (1M context) Co-authored-by: Cursor --- .../DocumentationGenerator.cs | 13 +++- .../BrandingCopyTests.cs | 60 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 tests/Elastic.Markdown.Tests/BrandingCopyTests.cs diff --git a/src/Elastic.Markdown/DocumentationGenerator.cs b/src/Elastic.Markdown/DocumentationGenerator.cs index c0179760f2..cdca65b33a 100644 --- a/src/Elastic.Markdown/DocumentationGenerator.cs +++ b/src/Elastic.Markdown/DocumentationGenerator.cs @@ -232,11 +232,22 @@ private void CopyBrandingResources() } var destination = _writeFileSystem.FileInfo.New(Path.Join(outputStaticDir, source.Name)); - _ = source.CopyTo(destination.FullName, overwrite: true); + CopyFileAcrossFileSystems(source, destination); _logger.LogInformation("Copied branding asset {Source} -> {Destination}", source.FullName, destination.FullName); } } + private void CopyFileAcrossFileSystems(IFileInfo source, IFileInfo destination) + { + if (Context.ReadFileSystem == _writeFileSystem) + Context.ReadFileSystem.File.Copy(source.FullName, destination.FullName, overwrite: true); + else + { + var bytes = Context.ReadFileSystem.File.ReadAllBytes(source.FullName); + _writeFileSystem.File.WriteAllBytes(destination.FullName, bytes); + } + } + private void HintUnusedSubstitutionKeys() { var definedKeys = new HashSet(Context.Configuration.Substitutions.Keys.ToArray()); diff --git a/tests/Elastic.Markdown.Tests/BrandingCopyTests.cs b/tests/Elastic.Markdown.Tests/BrandingCopyTests.cs new file mode 100644 index 0000000000..22ff9a961d --- /dev/null +++ b/tests/Elastic.Markdown.Tests/BrandingCopyTests.cs @@ -0,0 +1,60 @@ +// 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 System.IO.Abstractions.TestingHelpers; +using AwesomeAssertions; +using Elastic.Documentation; +using Elastic.Documentation.Configuration; +using Elastic.Documentation.Diagnostics; +using Elastic.Markdown.IO; + +namespace Elastic.Markdown.Tests; + +public class BrandingCopyTests(ITestOutputHelper output) +{ + [Fact] + public async Task CopyBrandingResources_SeparateFileSystems_DoesNotThrow() + { + var logger = new TestLoggerFactory(output); + + var readFs = new MockFileSystem(new Dictionary + { + { "docs/docset.yml", + //language=yaml + new MockFileData(""" +project: test +toc: +- file: index.md +branding: + icon: assets/logo.svg +""") }, + { "docs/index.md", new MockFileData("# Hello") }, + { "docs/assets/logo.svg", new MockFileData("") } + }, new MockFileSystemOptions + { + CurrentDirectory = Paths.WorkingDirectoryRoot.FullName + }); + + var writeFs = new MockFileSystem(new MockFileSystemOptions + { + CurrentDirectory = Paths.WorkingDirectoryRoot.FullName + }); + + await using var collector = new DiagnosticsCollector([]).StartAsync(TestContext.Current.CancellationToken); + var configurationContext = TestHelpers.CreateConfigurationContext(readFs); + var readScoped = FileSystemFactory.ScopeCurrentWorkingDirectory(readFs); + var writeScoped = FileSystemFactory.ScopeCurrentWorkingDirectoryForWrite(writeFs); + var context = new BuildContext(collector, readScoped, writeScoped, configurationContext, ExportOptions.Default); + + var linkResolver = new TestCrossLinkResolver(); + var set = new DocumentationSet(context, logger, linkResolver); + var generator = new DocumentationGenerator(set, logger); + + await generator.GenerateAll(TestContext.Current.CancellationToken); + await collector.StopAsync(TestContext.Current.CancellationToken); + + var outputStaticDir = Path.Join(set.OutputDirectory.FullName, "_static"); + writeFs.File.Exists(Path.Join(outputStaticDir, "logo.svg")).Should().BeTrue(); + } +} From 87844594a00e9de393804814478752ae8eb9af22 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 24 Jul 2026 09:59:39 +0200 Subject: [PATCH 2/3] Show Elastic logo in isolated header only when git org is elastic Non-elastic orgs using docs-builder should not see the Elastic logo by default. The isolated header now checks the git remote org and renders a plain title link for non-elastic repos. Co-Authored-By: Claude Sonnet 4.6 (1M context) Co-authored-by: Cursor --- .../Assets/web-components/Header/Header.tsx | 29 +++++++++++++++++-- .../Layout/_IsolatedHeader.cshtml | 1 + src/Elastic.Documentation.Site/_ViewModels.cs | 4 +++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/web-components/Header/Header.tsx b/src/Elastic.Documentation.Site/Assets/web-components/Header/Header.tsx index e98f83d31f..4224c7336d 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/Header/Header.tsx +++ b/src/Elastic.Documentation.Site/Assets/web-components/Header/Header.tsx @@ -29,6 +29,8 @@ interface Props { * component does not have to infer branding state from other optional props. */ branded?: boolean + /** When true the git remote belongs to the `elastic` GitHub organization. Controls whether the Elastic logo is shown as default. */ + elasticOrg?: boolean /** Custom header background CSS colour. Only used when branded=true; defaults to #000000. */ headerBg?: string /** Custom icon image URL. When set (and branded=true), renders an instead of the title text. */ @@ -45,6 +47,7 @@ export const Header = ({ githubRef, airGapped = false, branded = false, + elasticOrg = false, headerBg, iconSrc, }: Props) => { @@ -92,8 +95,8 @@ export const Header = ({ {title} ) - ) : ( - // Default: Elastic-branded logo (light-mode styling) + ) : elasticOrg ? ( + // Default for elastic org: Elastic-branded logo (light-mode styling) + ) : ( + // Non-elastic org, no branding: title text only, no logo + + + {title} + + ) const headerCss = @@ -197,6 +221,7 @@ customElements.define( githubRef: 'string', airGapped: 'boolean', branded: 'boolean', + elasticOrg: 'boolean', headerBg: 'string', iconSrc: 'string', }, diff --git a/src/Elastic.Documentation.Site/Layout/_IsolatedHeader.cshtml b/src/Elastic.Documentation.Site/Layout/_IsolatedHeader.cshtml index c87249119b..3c7ea83183 100644 --- a/src/Elastic.Documentation.Site/Layout/_IsolatedHeader.cshtml +++ b/src/Elastic.Documentation.Site/Layout/_IsolatedHeader.cshtml @@ -8,6 +8,7 @@ git-commit="@(Model.GitCommitShort)" github-ref="@(Model.GitHubRef)" branded="@(Model.Branding != null ? "true" : "false")" + elastic-org="@(Model.IsElasticOrg ? "true" : "false")" header-bg="@(Model.Branding?.HeaderBg)" icon-src="@(Model.BrandingIconStaticPath)"> diff --git a/src/Elastic.Documentation.Site/_ViewModels.cs b/src/Elastic.Documentation.Site/_ViewModels.cs index 04593b02ec..2705824838 100644 --- a/src/Elastic.Documentation.Site/_ViewModels.cs +++ b/src/Elastic.Documentation.Site/_ViewModels.cs @@ -76,6 +76,10 @@ public record GlobalLayoutViewModel public bool RenderHamburgerIcon { get; init; } = true; + /// Whether the git remote belongs to the elastic GitHub organization. + public bool IsElasticOrg => + GitRepository?.StartsWith("elastic/", StringComparison.OrdinalIgnoreCase) == true; + /// White-label branding overrides. When non-null, all Elastic-specific chrome is suppressed. public BrandingConfiguration? Branding { get; init; } From 81d742a8acfd6a35bde2583e5a924a28f9929a6b Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Fri, 24 Jul 2026 10:03:42 +0200 Subject: [PATCH 3/3] Render logo as plain span when logoHref is not set Make logoHref optional and fall back to a non-clickable in all header variants when no href is provided, instead of rendering an with an empty or undefined href. Co-Authored-By: Claude Sonnet 4.6 (1M context) Co-authored-by: Cursor --- .../Assets/web-components/Header/Header.tsx | 115 +++++++++--------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/web-components/Header/Header.tsx b/src/Elastic.Documentation.Site/Assets/web-components/Header/Header.tsx index 4224c7336d..b4a65df6b0 100644 --- a/src/Elastic.Documentation.Site/Assets/web-components/Header/Header.tsx +++ b/src/Elastic.Documentation.Site/Assets/web-components/Header/Header.tsx @@ -14,7 +14,7 @@ import { useRef } from 'react' interface Props { title: string - logoHref: string + logoHref?: string githubRepository?: string githubLink?: string gitBranch: string @@ -55,51 +55,65 @@ export const Header = ({ const containerRef = useRef(null) useHtmxContainer(containerRef) - const logoSection = branded ? ( - iconSrc ? ( - // Branded with icon — plain , no HTMX (no containerRef) - + {title} - {title} - {title} + /> + {title} + + ) : ( + <>{title} + ) + + const brandedStyles = iconSrc + ? css` + display: inline-flex; + align-items: center; + gap: ${euiTheme.size.s}; + color: var(--color-white); + text-decoration: none; + padding: ${euiTheme.size.s}; + ` + : css` + display: inline-flex; + align-items: center; + color: var(--color-white); + text-decoration: none; + padding: ${euiTheme.size.s}; + font-weight: ${euiTheme.font.weight.bold}; + ` + + const plainStyles = css` + display: inline-flex; + align-items: center; + padding: ${euiTheme.size.s}; + font-weight: ${euiTheme.font.weight.bold}; + color: ${euiTheme.colors.textInk}; + text-decoration: none; + border-radius: ${euiTheme.border.radius.small}; + &:hover { + background: rgba(0, 0, 0, 0.06); + } + ` + + const logoSection = branded ? ( + logoHref ? ( + + {brandedIconContent} ) : ( - // Branded without icon — title text only, no HTMX - - {title} - + {brandedIconContent} ) ) : elasticOrg ? ( - // Default for elastic org: Elastic-branded logo (light-mode styling) ) : ( - // Non-elastic org, no branding: title text only, no logo - - {title} - + {logoHref ? ( + + {title} + + ) : ( + {title} + )} )