Skip to content

[Experiment] Add glob redirects #1278

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
42 changes: 42 additions & 0 deletions docs/contribute/redirects.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,45 @@ redirects:
'old-anchor': 'active-anchor'
'removed-anchor':
```

## Glob pattern redirects

The redirect system also supports glob patterns for redirecting multiple pages at once. This is useful when moving entire sections or directories.

### Trailing wildcard redirects

Use the `**` pattern at the end of a path to match all content under a specific path prefix and redirect it to a new location while preserving the path structure.

```yaml
redirects:
'reference/apm/agents/android/**': 'reference/opentelemetry/edot-sdks/android/**'
'old-section/**': 'new-section/**'
```

This redirects:
- `reference/apm/agents/android/setup` → `reference/opentelemetry/edot-sdks/android/setup`
- `reference/apm/agents/android/api/classes` → `reference/opentelemetry/edot-sdks/android/api/classes`

### Simple glob pattern redirects

You can use basic glob patterns with `*` (single segment wildcard) and `?` (single character wildcard).

```yaml
redirects:
'guides/*/intro': 'tutorials/*/getting-started'
'api/v1/user?.json': 'api/v2/users'
```

For simple cases where the target doesn't contain wildcards, the target is used directly:

```yaml
redirects:
'docs/v*/setup': 'documentation/setup-guide'
```

### Best practices for glob patterns

1. Use exact path redirects when possible.
2. Use trailing `**` wildcards for redirecting entire directory trees.
3. Keep glob patterns simple, with wildcards in predictable positions.
4. Test your redirects thoroughly, especially when using complex patterns.
69 changes: 60 additions & 9 deletions src/Elastic.Documentation.Configuration/Builder/RedirectFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// See the LICENSE file in the project root for more information

using System.IO.Abstractions;
using System.Text.RegularExpressions;
using DotNet.Globbing;
using Elastic.Documentation.Links;
using YamlDotNet.RepresentationModel;

Expand All @@ -11,6 +13,59 @@ namespace Elastic.Documentation.Configuration.Builder;
public record RedirectFile
{
public Dictionary<string, LinkRedirect>? Redirects { get; set; }

public List<(string Pattern, string TargetPrefix)> GlobRedirects { get; set; } = [];

private readonly Dictionary<string, Glob> _compiledPatterns = new(StringComparer.Ordinal);

private Glob GetCompiledGlob(string pattern)
{
if (_compiledPatterns.TryGetValue(pattern, out var glob))
return glob;

var options = new GlobOptions();
options.Evaluation.CaseInsensitive = true;
glob = Glob.Parse(pattern, options);
_compiledPatterns[pattern] = glob;
return glob;
}

public string? ResolveRedirect(string path)
{
if (Redirects != null && Redirects.TryGetValue(path, out var linkRedirect) && linkRedirect.To != null)
return linkRedirect.To;

foreach (var (pattern, targetPrefix) in GlobRedirects)
{
if (pattern.EndsWith("**", StringComparison.Ordinal) && targetPrefix.Contains("**"))
{
var prefix = pattern.AsSpan(0, pattern.Length - 2);
if (path.StartsWith(prefix.ToString(), StringComparison.OrdinalIgnoreCase))
{
var suffix = path.AsSpan(prefix.Length);
var rewritten = targetPrefix.Replace("**", suffix.ToString());
return rewritten;
}
}
else if (pattern.Contains('*') || pattern.Contains('?'))
{
var glob = GetCompiledGlob(pattern);
if (glob.IsMatch(path))
{
if (!targetPrefix.Contains('*') && !targetPrefix.Contains('?'))
return targetPrefix;
return path;
}
}
else if (string.Equals(pattern, path, StringComparison.OrdinalIgnoreCase))
{
return targetPrefix;
}
}

return null;
}

private IFileInfo Source { get; init; }
private IDocumentationContext Context { get; init; }

Expand Down Expand Up @@ -47,6 +102,7 @@ public RedirectFile(IFileInfo source, IDocumentationContext context)

private static Dictionary<string, LinkRedirect>? ReadRedirects(YamlStreamReader reader, KeyValuePair<YamlNode, YamlNode> entry)
{
var globPatterns = new List<(string Pattern, string TargetPrefix)>();
if (!reader.ReadObjectDictionary(entry, out var mapping))
return null;

Expand All @@ -60,7 +116,8 @@ public RedirectFile(IFileInfo source, IDocumentationContext context)
if (entryValue.Value is YamlScalarNode)
{
var to = reader.ReadString(entryValue);
dictionary.Add(key,
dictionary.Add(
key,
!string.IsNullOrEmpty(to)
? to.StartsWith('!')
? new LinkRedirect { To = to.TrimStart('!'), Anchors = LinkRedirect.CatchAllAnchors }
Expand All @@ -77,7 +134,6 @@ public RedirectFile(IFileInfo source, IDocumentationContext context)
if (linkRedirect is not null)
dictionary.Add(key, linkRedirect);
}

return dictionary;
}

Expand Down Expand Up @@ -112,8 +168,7 @@ public RedirectFile(IFileInfo source, IDocumentationContext context)
if (redirect.To is null && redirect.Many is null or { Length: 0 })
return redirect with { To = file };

return string.IsNullOrEmpty(redirect.To) && redirect.Many is null or { Length: 0 }
? null : redirect;
return string.IsNullOrEmpty(redirect.To) && redirect.Many is null or { Length: 0 } ? null : redirect;
}

private static LinkSingleRedirect[]? ReadManyRedirects(YamlStreamReader reader, string file, YamlNode node)
Expand Down Expand Up @@ -153,10 +208,6 @@ public RedirectFile(IFileInfo source, IDocumentationContext context)
if (redirects.Count == 0)
return null;

return
[
..redirects
.Where(r => r.To is not null && r.Anchors is not null && r.Anchors.Count >= 0)
];
return [.. redirects.Where(r => r.To is not null && r.Anchors is not null && r.Anchors.Count >= 0)];
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
Expand All @@ -11,7 +11,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="DotNet.Glob" />
<PackageReference Include="DotNet.Glob"/>
<PackageReference Include="Samboy063.Tomlet" />
<PackageReference Include="Vecc.YamlDotNet.Analyzers.StaticGenerator"/>
<PackageReference Include="YamlDotNet"/>
Expand Down
Loading