This site provides a machine-readable index at /llms.txt.

Skip to main content Skip to navigation

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 IRedirectableDocSiteFrontMatter and BlogSiteFrontMatter both do.

To copy a working setup, see examples/DocSiteKitchenSinkExampleContent/main/redirect-source.md is the fixture this how-to is fenced from.


Steps

  1. 1

    Add redirectUrl: to the old page's front matter

    Open the markdown file at the old URL and set redirectUrl: to the new absolute path. Keep title: 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. 2

    Confirm the front-matter record implements IRedirectable

    The engine looks for the RedirectUrl property via IRedirectable pattern-matching; DocSiteFrontMatter already 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. 3

    Understand what the pipeline emits

    During discovery, MarkdownContentService detects RedirectUrl and yields a RedirectSource instead of a MarkdownFileSource, so the page skips parse/render and the redirect middleware handles it uniformly at dev serve and build time.

    /// <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. 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 to redirectUrl; 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.xml and /llms.txt: the old URL does not appear (redirects are filtered by SitemapBuilder and LlmsTxtService).