Skip to content

Relative links in markdown resolve incorrectly due to directory-style output mismatch #1

@phil-scott-78

Description

@phil-scott-78

Summary

Relative links in markdown content (e.g., ../sibling-page/) are resolved to incorrect absolute paths during server-side rendering. The link rewriter treats the page URL as a file path, but static output uses directory-style URLs (page/index.html), causing ../ to go up one level too many.

Reproduction

Given this file structure:

Content/reference/cli/commands/aspire.md
Content/reference/cli/commands/aspire-add.md

With aspire.md containing:

[aspire add](../aspire-add/)

Expected: The rendered HTML link resolves to /reference/cli/commands/aspire-add/ (sibling page in the same directory), matching how browsers resolve the relative URL from the directory-style output at /reference/cli/commands/aspire/index.html.

Actual: The rendered HTML contains href="/reference/cli/aspire-add/" — one directory too high, pointing to a non-existent page.

Root cause

In MarkdownContentService.cs (lines 118-119), the page URL is stripped to its parent directory before being passed to the link rewriter:

var lastSlash = page.Url.LastIndexOf('/');
var pageUrl = lastSlash == -1 ? page.Url : page.Url[..lastSlash];

For page URL /reference/cli/commands/aspire, this produces pageUrl = "/reference/cli/commands".

Then in LinkRewriter.GetAbsolutePathWithBaseUrl, ../aspire-add/ is resolved relative to /reference/cli/commands:

  • Base segments: ["reference", "cli", "commands"]
  • One ../ removes "commands"["reference", "cli"]
  • Result: /reference/cli/aspire-add/

But since the static output is commands/aspire/index.html, the browser sees the page at /reference/cli/commands/aspire/. From the browser's perspective:

  • Base segments: ["reference", "cli", "commands", "aspire"]
  • One ../ removes "aspire"["reference", "cli", "commands"]
  • Result: /reference/cli/commands/aspire-add/

Suggested fix

Pass the full page URL to the link rewriter instead of stripping the last segment:

// Before:
var lastSlash = page.Url.LastIndexOf('/');
var pageUrl = lastSlash == -1 ? page.Url : page.Url[..lastSlash];
var html = _markdownParserService.RenderMarkdownToHtml(page.MarkdownContent, pageUrl);

// After:
var html = _markdownParserService.RenderMarkdownToHtml(page.MarkdownContent, page.Url);

This makes the link rewriter treat the page as a directory (matching the page/index.html output convention), so ../ correctly resolves to the sibling level.

Note: LinkRewriter.GetAbsolutePathWithBaseUrl would also need to be updated — it currently assumes the relativeToUrl is already a directory. With the full page URL, the ../ counting logic would need to account for the page name being the last segment.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions