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..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
@@ -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) => {
@@ -52,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}>
+ )
+
+ 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}
)
- ) : (
- // Default: Elastic-branded logo (light-mode styling)
+ ) : elasticOrg ? (
+ ) : (
+
+ {logoHref ? (
+
+ {title}
+
+ ) : (
+ {title}
+ )}
+
)
const headerCss =
@@ -197,6 +224,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; }
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();
+ }
+}