diff --git a/docs/configure/content-set/navigation.md b/docs/configure/content-set/navigation.md
index e39933e732..c8a56a1268 100644
--- a/docs/configure/content-set/navigation.md
+++ b/docs/configure/content-set/navigation.md
@@ -201,6 +201,30 @@ does not match the directory structure.
A `file` can only select siblings and more deeply nested files as its children. It can't select files outside its own subtree on disk.
+#### Deep-linked `index.md` files
+
+A `file` entry without `children` that points at an `index.md` inside a subdirectory is treated as a single-page subsection for that directory:
+
+```yaml
+toc:
+ - file: reference/1password/index.md
+ - file: reference/activemq/index.md
+```
+
+Each entry becomes its own subsection linking to the directory's `index.md`. This is convenient when every subdirectory contains only an `index.md`. It is shorthand for:
+
+```yaml
+toc:
+ - folder: reference/1password
+ children:
+ - file: index.md
+ - folder: reference/activemq
+ children:
+ - file: index.md
+```
+
+If the entry declares `children`, it keeps the [virtual grouping](#virtual-grouping) (deep-linking) behavior instead.
+
#### Hidden files
A hidden file can be declared in the TOC.
diff --git a/src/Elastic.Documentation.Configuration/Toc/TableOfContentsYamlConverters.cs b/src/Elastic.Documentation.Configuration/Toc/TableOfContentsYamlConverters.cs
index 664c18085b..ddacc423b2 100644
--- a/src/Elastic.Documentation.Configuration/Toc/TableOfContentsYamlConverters.cs
+++ b/src/Elastic.Documentation.Configuration/Toc/TableOfContentsYamlConverters.cs
@@ -152,9 +152,22 @@ public class TocItemYamlConverter : IYamlTypeConverter
// PathRelativeToContainer will be set during resolution
if (dictionary.TryGetValue("file", out var filePathOnly) && filePathOnly is string fileOnly)
{
- return fileOnly == "index.md"
- ? new IndexFileRef(fileOnly, fileOnly, false, children, placeholderContext)
- : new FileRef(fileOnly, fileOnly, false, children, placeholderContext);
+ if (fileOnly == "index.md")
+ return new IndexFileRef(fileOnly, fileOnly, false, children, placeholderContext);
+
+ // A childless deep-linked index file (e.g. "file: reference/1password/index.md") is treated as
+ // sugar for "folder: reference/1password, file: index.md". Otherwise it resolves to a bare leaf
+ // that competes with its siblings for the parent's index slot and gets silently dropped from the
+ // navigation. Entries that declare explicit children keep the existing virtual-file (deep-linking)
+ // semantics, so this remains backward compatible.
+ if (children.Count == 0 && fileOnly.EndsWith("/index.md", StringComparison.Ordinal))
+ {
+ var indexFolderPath = fileOnly[..^"/index.md".Length];
+ var indexFile = new FolderIndexFileRef("index.md", "index.md", false, [], placeholderContext);
+ return new FolderRef(indexFolderPath, indexFolderPath, [indexFile], placeholderContext);
+ }
+
+ return new FileRef(fileOnly, fileOnly, false, children, placeholderContext);
}
if (dictionary.TryGetValue("hidden", out var hiddenPath) && hiddenPath is string p)
diff --git a/tests/Navigation.Tests/Isolation/DeepLinkedIndexFileTests.cs b/tests/Navigation.Tests/Isolation/DeepLinkedIndexFileTests.cs
new file mode 100644
index 0000000000..f7837b7702
--- /dev/null
+++ b/tests/Navigation.Tests/Isolation/DeepLinkedIndexFileTests.cs
@@ -0,0 +1,185 @@
+// 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.Configuration.Toc;
+using Elastic.Documentation.Extensions;
+using Elastic.Documentation.Navigation.Isolated;
+using Elastic.Documentation.Navigation.Isolated.Leaf;
+using Elastic.Documentation.Navigation.Isolated.Node;
+
+namespace Elastic.Documentation.Navigation.Tests.Isolation;
+
+///
+/// Regression coverage for https://github.com/elastic/docs-builder/issues/764.
+/// A childless file: subdir/index.md entry is sugar for folder: subdir, file: index.md,
+/// so each entry becomes its own subfolder with a proper index page instead of competing for the
+/// parent's index slot (which silently dropped it from the navigation).
+///
+public class DeepLinkedIndexFileTests(ITestOutputHelper output) : DocumentationSetNavigationTestBase(output)
+{
+ [Fact]
+ public void ChildlessDeepLinkedIndexFileBecomesFolderRef()
+ {
+ // language=yaml
+ var yaml = """
+ project: 'test-project'
+ toc:
+ - file: reference/1password/index.md
+ """;
+
+ var fileSystem = new MockFileSystem();
+ fileSystem.AddDirectory("/docs");
+ var context = CreateContext(fileSystem);
+ var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, yaml, fileSystem.NewDirInfo("docs"));
+
+ var folder = docSet.TableOfContents.First().Should().BeOfType().Subject;
+ folder.PathRelativeToDocumentationSet.Should().Be("reference/1password");
+
+ var index = folder.Children.First().Should().BeOfType().Subject;
+ index.PathRelativeToDocumentationSet.Should().Be("reference/1password/index.md");
+ }
+
+ [Fact]
+ public void BareIndexFileIsNotConverted()
+ {
+ // language=yaml
+ var yaml = """
+ project: 'test-project'
+ toc:
+ - file: index.md
+ - file: other.md
+ """;
+
+ var fileSystem = new MockFileSystem();
+ fileSystem.AddDirectory("/docs");
+ var context = CreateContext(fileSystem);
+ var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, yaml, fileSystem.NewDirInfo("docs"));
+
+ // Bare index.md stays on the IndexFileRef path, never wrapped into a folder.
+ _ = docSet.TableOfContents.First().Should().BeOfType();
+ docSet.TableOfContents.OfType().Should().BeEmpty();
+ }
+
+ [Fact]
+ public void DeepLinkedIndexFileWithChildrenStaysVirtualFile()
+ {
+ // language=yaml
+ var yaml = """
+ project: 'test-project'
+ toc:
+ - file: reference/aws/index.md
+ children:
+ - file: cloudfront.md
+ """;
+
+ var fileSystem = new MockFileSystem();
+ fileSystem.AddDirectory("/docs");
+ var context = CreateContext(fileSystem);
+ var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, yaml, fileSystem.NewDirInfo("docs"));
+
+ // Entries that declare explicit children keep the existing virtual-file (deep-linking) semantics.
+ var fileRef = docSet.TableOfContents.First().Should().BeOfType().Subject;
+ fileRef.Should().NotBeOfType();
+ fileRef.PathRelativeToDocumentationSet.Should().Be("reference/aws/index.md");
+ fileRef.Children.Should().ContainSingle();
+ }
+
+ [Fact]
+ public async Task ChildlessDeepLinkedIndexFileRendersAsSingleLinkFolder()
+ {
+ // language=yaml
+ var yaml = """
+ project: 'test-project'
+ toc:
+ - file: index.md
+ - file: reference/1password/index.md
+ """;
+
+ var fileSystem = new MockFileSystem();
+ fileSystem.AddDirectory("/docs");
+ var context = CreateContext(fileSystem);
+ var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, yaml, fileSystem.NewDirInfo("docs"));
+ _ = context.Collector.StartAsync(TestContext.Current.CancellationToken);
+
+ var navigation = new DocumentationSetNavigation(docSet, context, GenericDocumentationFileFactory.Instance);
+
+ await context.Collector.StopAsync(TestContext.Current.CancellationToken);
+
+ var folder = navigation.NavigationItems.OfType>().Single();
+ folder.Url.Should().Be("/reference/1password");
+ // No other navigation items, so the frontend renders this as a plain link rather than an expandable folder.
+ folder.NavigationItems.Should().BeEmpty();
+ context.Collector.Errors.Should().Be(0);
+ }
+
+ [Fact]
+ public async Task FolderWithChildlessIndexFileChildrenKeepsEveryEntry()
+ {
+ // language=yaml
+ var yaml = """
+ project: 'test-project'
+ toc:
+ - folder: reference
+ file: index.md
+ children:
+ - file: 1password/index.md
+ - file: abnormal_security/index.md
+ - file: activemq/index.md
+ """;
+
+ var fileSystem = new MockFileSystem();
+ fileSystem.AddDirectory("/docs");
+ var context = CreateContext(fileSystem);
+ var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, yaml, fileSystem.NewDirInfo("docs"));
+ _ = context.Collector.StartAsync(TestContext.Current.CancellationToken);
+
+ var navigation = new DocumentationSetNavigation(docSet, context, GenericDocumentationFileFactory.Instance);
+
+ await context.Collector.StopAsync(TestContext.Current.CancellationToken);
+
+ var reference = navigation.NavigationItems.OfType>().Single();
+ reference.Url.Should().Be("/reference");
+
+ // All three entries survive as their own subfolders; none is consumed as the parent's index.
+ var childFolders = reference.NavigationItems.OfType>().ToList();
+ childFolders.Select(f => f.Url).Should().BeEquivalentTo(
+ ["/reference/1password", "/reference/abnormal_security", "/reference/activemq"]);
+ childFolders.Should().AllSatisfy(f => f.NavigationItems.Should().BeEmpty());
+ context.Collector.Errors.Should().Be(0);
+ }
+
+ [Fact]
+ public async Task ChildlessIndexFileChildrenUnderVirtualFileResolveWithoutPathDoubling()
+ {
+ // language=yaml
+ var yaml = """
+ project: 'test-project'
+ toc:
+ - file: reference/apache-intro.md
+ children:
+ - file: reference/apache/index.md
+ - file: reference/apache_spark/index.md
+ """;
+
+ var fileSystem = new MockFileSystem();
+ fileSystem.AddDirectory("/docs");
+ var context = CreateContext(fileSystem);
+ var docSet = DocumentationSetFile.LoadAndResolve(context.Collector, yaml, fileSystem.NewDirInfo("docs"));
+ _ = context.Collector.StartAsync(TestContext.Current.CancellationToken);
+
+ var navigation = new DocumentationSetNavigation(docSet, context, GenericDocumentationFileFactory.Instance);
+
+ await context.Collector.StopAsync(TestContext.Current.CancellationToken);
+
+ var intro = navigation.NavigationItems.OfType>().Single();
+ intro.Url.Should().Be("/reference/apache-intro");
+
+ var childFolders = intro.NavigationItems.OfType>().ToList();
+ // Deep-linked index paths resolve relative to the documentation set root, not the parent path.
+ childFolders.Select(f => f.Url).Should().BeEquivalentTo(["/reference/apache", "/reference/apache_spark"]);
+ context.Collector.Errors.Should().Be(0);
+ }
+}