Configure redirects
When a published page is renamed or deleted, the old URL needs to forward visitors and search engines to the new location. Setting redirectUrl: in the page's front matter causes Pennington to emit a meta-refresh stub at the old path — the body is not rendered or indexed.
This covers front-matter-based redirects only. HTTP 301 responses and batch redirects via a sidecar file are handled at the hosting layer and fall outside this guide's scope.
Assumptions
- An existing Pennington doc site using
AddDocSite(see Scaffold a documentation site with DocSite if not). - Both the old URL (the page being retired) and the new URL (the canonical destination) are known.
- The front-matter type implements
IRedirectable—DocSiteFrontMatterandBlogSiteFrontMatterboth do.
To copy a working setup, see examples/DocSiteKitchenSinkExample — Content/main/redirect-source.md is the fixture this how-to is fenced from.
Steps
- 1
Add
redirectUrl:to the old page's front matterOpen the markdown file at the old URL and set
redirectUrl:to the new absolute path. Keeptitle:so diagnostics remain readable; the body is not rendered.--- title: Old page URL description: Redirects away to the new location. redirectUrl: /main/front-matter/ order: 200 uid: kitchen-sink.main.redirect-source --- # This page has moved A visit to this URL is intercepted by the Pennington redirect middleware and returns HTTP 301 with a meta-refresh body pointing at `redirectUrl`. The static build captures the same 301 and writes the meta-refresh file to disk at this page's output path — one code path for dev and publish. - 2
Confirm the front-matter record implements
IRedirectableThe engine looks for the
RedirectUrlproperty viaIRedirectablepattern-matching;DocSiteFrontMatteralready declares it. For a custom front-matter record, add the interface so the pipeline surfaces the page as a redirect./// <summary> /// Front matter bound by <see cref="DocSiteServiceExtensions.AddDocSite"/>. Extends the /// <see cref="FrontMatter.DocFrontMatter"/> shape with <see cref="RedirectUrl"/> via /// <see cref="IRedirectable"/>. Implements <see cref="IFrontMatter"/>, <see cref="ITaggable"/>, /// <see cref="ISectionable"/>, <see cref="IOrderable"/>, and <see cref="IRedirectable"/>. /// </summary> public record DocSiteFrontMatter : IFrontMatter, ITaggable, ISectionable, IOrderable, IRedirectable { /// <summary>Page title rendered in the browser tab and page heading.</summary> public string Title { get; init; } = ""; /// <summary>Short description used for the meta description and social cards.</summary> public string? Description { get; init; } /// <summary>When true, the page is skipped during production builds.</summary> public bool IsDraft { get; init; } /// <summary>Tags applied to this page for filtering and the tag index.</summary> public string[] Tags { get; init; } = []; /// <summary>Sort order within the containing section. Lower values appear first.</summary> public int Order { get; init; } = int.MaxValue; /// <summary>When set, the page emits a client-side redirect to this URL instead of normal content.</summary> public string? RedirectUrl { get; init; } /// <summary>Section heading this page belongs under in navigation.</summary> public string? SectionLabel { get; init; } /// <summary>Stable identifier used for cross-references (<c>[text](xref:uid)</c>).</summary> public string? Uid { get; init; } /// <summary>When false, the page is excluded from the search index.</summary> public bool Search { get; init; } = true; /// <summary>When false, the page is excluded from the generated llms.txt output.</summary> public bool Llms { get; init; } = true; }/// <summary> /// Content that can redirect to another URL. /// </summary> public interface IRedirectable { /// <summary>Target URL when this page should redirect; <c>null</c> or empty means no redirect.</summary> string? RedirectUrl { get; } } - 3
Understand what the pipeline emits
During discovery,
MarkdownContentServicedetectsRedirectUrland yields aRedirectSourceinstead of aMarkdownFileSource, so the page skips parse/render and the redirect middleware handles it uniformly at dev serve andbuildtime./// <inheritdoc/> public async IAsyncEnumerable<DiscoveredItem> DiscoverAsync() { foreach (var (route, sourceFile) in DiscoverRoutesWithFallbacks()) { TFrontMatter? frontMatter = default; try { var content = await _fileSystem.File.ReadAllTextAsync(sourceFile.Value); var parsed = _parser.Parse<TFrontMatter>(content); if (parsed.Metadata is { IsDraft: true }) continue; frontMatter = parsed.Metadata; } catch { // If front matter can't be parsed, include the file as a markdown source. } // Pages with RedirectUrl front matter are surfaced as RedirectSource so the // unified redirect middleware handles them at dev time and the static build // crawler captures the 301 response (written as a meta-refresh HTML file). if (frontMatter is IRedirectable { RedirectUrl: { Length: > 0 } redirectTarget }) { yield return new DiscoveredItem(route, new RedirectSource(new UrlPath(redirectTarget))); continue; } yield return new DiscoveredItem(route, new MarkdownFileSource(sourceFile)); } } - 4
Run the site and follow the old URL
Start the site with
dotnet run. The old URL responds with a meta-refresh stub that forwards toredirectUrl; the static build writes the same stub to disk at the old page's output path.dotnet run --project src/YourDocSite
Verify
- Visit the old URL in a browser: the page redirects immediately to the target set in
redirectUrl. - View the old URL's source: the markup contains
<meta http-equiv="refresh" content="0;url=...">and a<link rel="canonical" href="...">tag pointing at the redirect target. - Check
/sitemap.xmland/llms.txt: the old URL does not appear (redirects are filtered bySitemapBuilderandLlmsTxtService).
Related
- Reference: Front matter key reference — the row for
redirectUrl(type, default, which records support it). - Reference:
IFrontMatterand capability defaults — howIRedirectablefits alongside the other capability interfaces. - Background: The front-matter capability system — why
IRedirectablestayed a separate capability instead of collapsing intoIFrontMatter.