diff --git a/docs/syntax/applies-switch.md b/docs/syntax/applies-switch.md
index 52ecf4a29..4692a4eb7 100644
--- a/docs/syntax/applies-switch.md
+++ b/docs/syntax/applies-switch.md
@@ -77,6 +77,45 @@ Content for Serverless
:::::
::::::
+## Automatic ordering
+
+Applies-switch tabs are **automatically ordered** according to documentation standards, regardless of how they appear in the source file. This ensures consistency across documentation.
+
+The ordering rules are:
+
+1. **Serverless** appears first
+2. **Stack** versions appear second, ordered from **latest to oldest**
+3. **Deployment types** appear third, in this order:
+ - ECH/ESS (Elastic Cloud Hosted)
+ - ECE (Elastic Cloud Enterprise)
+ - ECK (Elastic Cloud on Kubernetes)
+ - Self-managed
+4. Items with **unavailable** lifecycle always appear **last**
+
+**Example:**
+
+Even if you write the tabs in a different order:
+
+```markdown
+::::{applies-switch}
+:::{applies-item} stack: ga 8.11
+Old stack version
+:::
+:::{applies-item} serverless: ga
+Serverless content
+:::
+:::{applies-item} stack: preview 9.1
+New stack version
+:::
+::::
+```
+
+They will automatically render in the correct order: Serverless → Stack 9.1 → Stack 8.11
+
+:::{tip}
+You don't need to manually order tabs anymore. Focus on content, and the system will handle the ordering for consistency.
+:::
+
## Automatic grouping
All applies switches on a page automatically sync together. When you select an applies_to definition in one switch, all other switches will switch to the same applies_to definition.
diff --git a/docs/syntax/applies.md b/docs/syntax/applies.md
index 90e5397ab..e18e1bd7c 100644
--- a/docs/syntax/applies.md
+++ b/docs/syntax/applies.md
@@ -369,18 +369,24 @@ This section provides detailed rules for how badges are rendered based on lifecy
### Rendering order
-`applies_to` badges are displayed in a consistent order regardless of how they appear in your source files:
-
-1. **Stack** - Elastic Stack
-2. **Serverless** - Elastic Cloud Serverless offerings
-3. **Deployment** - Deployment options (ECH, ECK, ECE, Self-managed)
+`applies_to` badges are **automatically displayed in a consistent order** regardless of how they appear in your source files:
+
+1. **Serverless** - Elastic Cloud Serverless offerings (appears first)
+2. **Stack** - Elastic Stack (ordered from latest to oldest version)
+3. **Deployment** - Deployment options in this order:
+ - ECH/ESS (Elastic Cloud Hosted)
+ - ECE (Elastic Cloud Enterprise)
+ - ECK (Elastic Cloud on Kubernetes)
+ - Self-managed
4. **ProductApplicability** - Specialized tools and agents (ECCTL, Curator, EDOT, APM Agents)
5. **Product (generic)** - Generic product applicability
+Items with **unavailable** lifecycle always appear **last**, regardless of their category.
+
Within the ProductApplicability category, EDOT and APM Agent items are sorted alphabetically for better scanning.
:::{note}
-Inline applies annotations are rendered in the order they appear in the source file.
+This automatic ordering applies to badges displayed at page and section levels, as well as in [applies-switch](applies-switch.md) tabs. Inline applies annotations are rendered in the order they appear in the source file.
:::
### Badge rendering rules
diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToOrderComparer.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToOrderComparer.cs
new file mode 100644
index 000000000..ff91933e6
--- /dev/null
+++ b/src/Elastic.Documentation/AppliesTo/ApplicableToOrderComparer.cs
@@ -0,0 +1,174 @@
+// 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.Linq;
+
+namespace Elastic.Documentation.AppliesTo;
+
+///
+/// Comparer for ordering ApplicableTo objects according to documentation standards.
+/// Orders by:
+/// 1. Serverless first, then Stack (latest to oldest)
+/// 2. For deployments: ech/ess, ece, eck, self-managed
+/// 3. Unavailable lifecycle comes last
+///
+public class ApplicableToOrderComparer : IComparer
+{
+ public int Compare(ApplicableTo? x, ApplicableTo? y)
+ {
+ if (ReferenceEquals(x, y))
+ return 0;
+ if (x is null)
+ return 1;
+ if (y is null)
+ return -1;
+
+ // Check if either has unavailable lifecycle - unavailable goes last
+ var xIsUnavailable = IsUnavailable(x);
+ var yIsUnavailable = IsUnavailable(y);
+
+ if (xIsUnavailable && !yIsUnavailable)
+ return 1;
+ if (!xIsUnavailable && yIsUnavailable)
+ return -1;
+
+ // Both unavailable or both available, continue with normal ordering
+
+ // Determine the primary category for each
+ var xCategory = GetPrimaryCategory(x);
+ var yCategory = GetPrimaryCategory(y);
+
+ // Compare by category first
+ var categoryComparison = xCategory.CompareTo(yCategory);
+ if (categoryComparison != 0)
+ return categoryComparison;
+
+ // Within the same category, apply specific ordering rules
+ return xCategory switch
+ {
+ ApplicabilityCategory.Serverless => 0,
+ ApplicabilityCategory.Stack => CompareStack(x, y),
+ ApplicabilityCategory.Deployment => CompareDeployment(x, y),
+ _ => 0
+ };
+ }
+
+ private static bool IsUnavailable(ApplicableTo applicableTo)
+ {
+ // Check if any applicability has unavailable lifecycle
+ if (applicableTo.Stack is not null &&
+ applicableTo.Stack.Any(a => a.Lifecycle == ProductLifecycle.Unavailable))
+ return true;
+
+ if (applicableTo.Serverless is not null)
+ {
+ var serverless = applicableTo.Serverless;
+ if ((serverless.Elasticsearch?.Any(a => a.Lifecycle == ProductLifecycle.Unavailable) ?? false) ||
+ (serverless.Observability?.Any(a => a.Lifecycle == ProductLifecycle.Unavailable) ?? false) ||
+ (serverless.Security?.Any(a => a.Lifecycle == ProductLifecycle.Unavailable) ?? false))
+ return true;
+ }
+
+ if (applicableTo.Deployment is not null)
+ {
+ var deployment = applicableTo.Deployment;
+ if ((deployment.Ess?.Any(a => a.Lifecycle == ProductLifecycle.Unavailable) ?? false) ||
+ (deployment.Ece?.Any(a => a.Lifecycle == ProductLifecycle.Unavailable) ?? false) ||
+ (deployment.Eck?.Any(a => a.Lifecycle == ProductLifecycle.Unavailable) ?? false) ||
+ (deployment.Self?.Any(a => a.Lifecycle == ProductLifecycle.Unavailable) ?? false))
+ return true;
+ }
+
+ return false;
+ }
+
+ private static ApplicabilityCategory GetPrimaryCategory(ApplicableTo applicableTo)
+ {
+ // Serverless takes priority
+ if (applicableTo.Serverless is not null)
+ return ApplicabilityCategory.Serverless;
+
+ // Then Stack
+ if (applicableTo.Stack is not null)
+ return ApplicabilityCategory.Stack;
+
+ // Then Deployment
+ if (applicableTo.Deployment is not null)
+ return ApplicabilityCategory.Deployment;
+
+ // Default
+ return ApplicabilityCategory.Other;
+ }
+
+ private static int CompareStack(ApplicableTo x, ApplicableTo y)
+ {
+ // Stack: order from latest to oldest version
+ var xVersion = GetLatestVersion(x.Stack);
+ var yVersion = GetLatestVersion(y.Stack);
+
+ if (xVersion is null && yVersion is null)
+ return 0;
+ if (xVersion is null)
+ return 1;
+ if (yVersion is null)
+ return -1;
+
+ // Higher version comes first (descending order)
+ return yVersion.CompareTo(xVersion);
+ }
+
+ private static int CompareDeployment(ApplicableTo x, ApplicableTo y)
+ {
+ // Deployment order: ech/ess, ece, eck, self-managed
+ var xDeploymentType = GetPrimaryDeploymentType(x.Deployment!);
+ var yDeploymentType = GetPrimaryDeploymentType(y.Deployment!);
+
+ return xDeploymentType.CompareTo(yDeploymentType);
+ }
+
+ private static DeploymentType GetPrimaryDeploymentType(DeploymentApplicability deployment)
+ {
+ // Return the first deployment type found in priority order
+ if (deployment.Ess is not null)
+ return DeploymentType.Ech; // ESS = ECH (Elastic Cloud Hosted)
+ if (deployment.Ece is not null)
+ return DeploymentType.Ece;
+ if (deployment.Eck is not null)
+ return DeploymentType.Eck;
+ if (deployment.Self is not null)
+ return DeploymentType.SelfManaged;
+
+ return DeploymentType.Unknown;
+ }
+
+ private static SemVersion? GetLatestVersion(AppliesCollection? collection)
+ {
+ if (collection is null || collection.Count == 0)
+ return null;
+
+ // Find the highest version in the collection using LINQ
+ return collection
+ .Select(applicability => applicability.Version?.Min)
+ .Where(version => version is not null)
+ .DefaultIfEmpty(null)
+ .Max();
+ }
+
+ private enum ApplicabilityCategory
+ {
+ Serverless = 0, // Serverless first
+ Stack = 1, // Stack second
+ Deployment = 2, // Deployment third
+ Other = 3 // Everything else last
+ }
+
+ private enum DeploymentType
+ {
+ Ech = 0, // ECH/ESS first
+ Ece = 1, // ECE second
+ Eck = 2, // ECK third
+ SelfManaged = 3, // Self-managed last
+ Unknown = 4
+ }
+}
diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs
index ffe651543..a8600d704 100644
--- a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs
+++ b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs
@@ -17,10 +17,10 @@ public class ApplicableToViewModel
private static readonly Dictionary, ApplicabilityMappings.ApplicabilityDefinition> DeploymentMappings = new()
{
- [d => d.Ess] = ApplicabilityMappings.Ech,
- [d => d.Eck] = ApplicabilityMappings.Eck,
- [d => d.Ece] = ApplicabilityMappings.Ece,
- [d => d.Self] = ApplicabilityMappings.Self
+ [d => d.Ess] = ApplicabilityMappings.Ech, // ESS/ECH first
+ [d => d.Ece] = ApplicabilityMappings.Ece, // ECE second
+ [d => d.Eck] = ApplicabilityMappings.Eck, // ECK third
+ [d => d.Self] = ApplicabilityMappings.Self // Self-managed last
};
private static readonly Dictionary, ApplicabilityMappings.ApplicabilityDefinition> ServerlessMappings = new()
@@ -81,7 +81,12 @@ public IReadOnlyCollection GetApplicabilityItems()
if (AppliesTo.Product is not null)
rawItems.AddRange(CollectFromCollection(AppliesTo.Product, ApplicabilityMappings.Product));
- return RenderGroupedItems(rawItems).ToArray();
+ var items = RenderGroupedItems(rawItems).ToList();
+
+ // Sort badges: unavailable lifecycle comes last
+ return items
+ .OrderBy(item => item.PrimaryApplicability.Lifecycle == ProductLifecycle.Unavailable ? 1 : 0)
+ .ToArray();
}
///
diff --git a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs
index 82599b415..3a630fe23 100644
--- a/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs
+++ b/src/Elastic.Markdown/Myst/Directives/AppliesSwitch/AppliesSwitchBlock.cs
@@ -18,7 +18,11 @@ public class AppliesSwitchBlock(DirectiveBlockParser parser, ParserContext conte
public int Index { get; set; }
public string GetGroupKey() => Prop("group") ?? "applies-switches";
- public override void FinalizeAndValidate(ParserContext context) => Index = FindIndex();
+ public override void FinalizeAndValidate(ParserContext context)
+ {
+ Index = FindIndex();
+ SortAppliesItems();
+ }
private int _index = -1;
@@ -30,6 +34,52 @@ public int FindIndex()
_index = GetUniqueLineIndex();
return _index;
}
+
+ private void SortAppliesItems()
+ {
+ // Get all applies-item children
+ var items = this.OfType().ToList();
+ if (items.Count <= 1)
+ return; // No need to sort if 0 or 1 items
+
+ // Parse ApplicableTo for each item for sorting
+ var itemsWithAppliesTo = items.Select(item =>
+ {
+ try
+ {
+ var applicableTo = YamlSerialization.Deserialize(
+ item.AppliesToDefinition,
+ Build.ProductsConfiguration);
+ return (Item: item, AppliesTo: (ApplicableTo?)applicableTo);
+ }
+ catch
+ {
+ // If parsing fails, keep original order for this item
+ return (Item: item, AppliesTo: null);
+ }
+ }).ToList();
+
+ // Create comparer
+ var comparer = new ApplicableToOrderComparer();
+
+ // Sort items based on their ApplicableTo, putting unparseable items at the end
+ var sorted = itemsWithAppliesTo
+ .OrderBy(x => x.AppliesTo is null ? 1 : 0) // Unparseable items last
+ .ThenBy(x => x.AppliesTo, comparer)
+ .ToList();
+
+ // Remove all items from the block
+ foreach (var item in items)
+ _ = Remove(item);
+
+ // Re-add items in sorted order
+ foreach (var (item, _) in sorted)
+ Add(item);
+
+ // Update indices after sorting
+ foreach (var item in items)
+ item.UpdateIndex();
+ }
}
public class AppliesItemBlock(DirectiveBlockParser parser, ParserContext context)
@@ -51,7 +101,6 @@ public override void FinalizeAndValidate(ParserContext context)
this.EmitError("{applies-item} requires an argument with applies_to definition.");
AppliesToDefinition = (Arguments ?? "{undefined}").ReplaceSubstitutions(context);
- Index = Parent!.IndexOf(this);
var appliesSwitch = Parent as AppliesSwitchBlock;
@@ -63,6 +112,9 @@ public override void FinalizeAndValidate(ParserContext context)
Selected = PropBool("selected");
}
+ // Called after sorting to update the index
+ internal void UpdateIndex() => Index = Parent!.IndexOf(this);
+
public static string GenerateSyncKey(string appliesToDefinition, ProductsConfiguration productsConfiguration)
{
var applicableTo = YamlSerialization.Deserialize(appliesToDefinition, productsConfiguration);
diff --git a/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs b/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs
index b60ea8d17..6f77f18b4 100644
--- a/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs
+++ b/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs
@@ -258,3 +258,129 @@ public void GeneratesDeterministicSyncKeysAcrossMultipleRuns()
"Different applies_to definitions must produce different sync keys");
}
}
+
+public class ApplicabilitySwitchOrderingTests(ITestOutputHelper output) : DirectiveTest(output,
+"""
+:::::{applies-switch}
+::::{applies-item} stack: ga 8.11
+Old stack version content
+::::
+
+::::{applies-item} serverless: ga
+Serverless content
+::::
+
+::::{applies-item} stack: preview 9.1
+New stack version content
+::::
+
+::::{applies-item} deployment: { ece: ga, ess: ga }
+Deployment content
+::::
+:::::
+"""
+)
+{
+ [Fact]
+ public void OrdersAppliesItemsCorrectly()
+ {
+ var items = Block!.OfType().ToArray();
+ items.Should().HaveCount(4);
+
+ // After automatic sorting:
+ // 1. Serverless first (index 0)
+ // 2. Stack 9.1 - latest stack version (index 1)
+ // 3. Stack 8.11 - older stack version (index 2)
+ // 4. Deployment (index 3)
+
+ items[0].AppliesToDefinition.Should().Contain("serverless");
+ items[1].AppliesToDefinition.Should().Contain("stack: preview 9.1");
+ items[2].AppliesToDefinition.Should().Contain("stack: ga 8.11");
+ items[3].AppliesToDefinition.Should().Contain("deployment");
+ }
+
+ [Fact]
+ public void UpdatesIndicesAfterSorting()
+ {
+ var items = Block!.OfType().ToArray();
+ for (var i = 0; i < items.Length; i++)
+ items[i].Index.Should().Be(i, $"Item at position {i} should have Index {i}");
+ }
+}
+
+public class UnavailableLastOrderingTests(ITestOutputHelper output) : DirectiveTest(output,
+"""
+:::::{applies-switch}
+::::{applies-item} stack: ga 9.1
+Available content
+::::
+
+::::{applies-item} stack: unavailable 9.0
+Unavailable content
+::::
+
+::::{applies-item} serverless: ga
+Serverless content
+::::
+:::::
+"""
+)
+{
+ [Fact]
+ public void OrdersUnavailableLast()
+ {
+ var items = Block!.OfType().ToArray();
+ items.Should().HaveCount(3);
+
+ // After automatic sorting:
+ // 1. Serverless (index 0)
+ // 2. Stack 9.1 available (index 1)
+ // 3. Stack 9.0 unavailable (index 2) - unavailable should be last
+
+ items[0].AppliesToDefinition.Should().Contain("serverless");
+ items[1].AppliesToDefinition.Should().Contain("stack: ga 9.1");
+ items[2].AppliesToDefinition.Should().Contain("unavailable");
+ }
+}
+
+public class DeploymentOrderingTests(ITestOutputHelper output) : DirectiveTest(output,
+"""
+:::::{applies-switch}
+::::{applies-item} deployment: { self: ga }
+Self-managed content
+::::
+
+::::{applies-item} deployment: { eck: ga }
+ECK content
+::::
+
+::::{applies-item} deployment: { ess: ga }
+ESS/ECH content
+::::
+
+::::{applies-item} deployment: { ece: ga }
+ECE content
+::::
+:::::
+"""
+)
+{
+ [Fact]
+ public void OrdersDeploymentsCorrectly()
+ {
+ var items = Block!.OfType().ToArray();
+ items.Should().HaveCount(4);
+
+ // After automatic sorting:
+ // 1. ESS/ECH first (index 0)
+ // 2. ECE second (index 1)
+ // 3. ECK third (index 2)
+ // 4. Self-managed last (index 3)
+
+ items[0].AppliesToDefinition.Should().Contain("ess");
+ items[1].AppliesToDefinition.Should().Contain("ece");
+ items[2].AppliesToDefinition.Should().Contain("eck");
+ items[3].AppliesToDefinition.Should().Contain("self");
+ }
+}
+
diff --git a/tests/authoring/Applicability/ApplicableToComponent.fs b/tests/authoring/Applicability/ApplicableToComponent.fs
index 7b6b61bcf..b37bc2053 100644
--- a/tests/authoring/Applicability/ApplicableToComponent.fs
+++ b/tests/authoring/Applicability/ApplicableToComponent.fs
@@ -405,10 +405,10 @@ apm_agent_java: beta 9.1.0
-
-
+
+
@@ -475,8 +475,8 @@ product: ga 9.0.0
-
+