From 022552d5b6adc792d3cd16df7de6e52cb7b41a72 Mon Sep 17 00:00:00 2001
From: KN4CK3R <admin@oldschoolhack.me>
Date: Tue, 16 Jan 2024 03:13:29 +0100
Subject: [PATCH] Rework markup link rendering (#26745) (#28803)

Backport #26745
Fixes #26548

This PR refactors the rendering of markup links. The old code uses
`strings.Replace` to change some urls while the new code uses more
context to decide which link should be generated.

The added tests should ensure the same output for the old and new
behaviour (besides the bug).

We may need to refactor the rendering a bit more to make it clear how
the different helper methods render the input string. There are lots of
options (resolve links / images / mentions / git hashes / emojis / ...)
but you don't really know what helper uses which options. For example,
we currently support images in the user description which should not be
allowed I think:

<details>
  <summary>Profile</summary>

https://try.gitea.io/KN4CK3R


![grafik](https://github.com/go-gitea/gitea/assets/1666336/109ae422-496d-4200-b52e-b3a528f553e5)

</details>
---
 models/issues/comment_code.go            |   8 +-
 models/repo/repo.go                      |   3 +-
 modules/markup/external/external.go      |  13 +-
 modules/markup/html.go                   |  57 +--
 modules/markup/html_internal_test.go     |  30 +-
 modules/markup/html_test.go              | 117 ++++---
 modules/markup/markdown/goldmark.go      |  30 +-
 modules/markup/markdown/markdown.go      |   6 -
 modules/markup/markdown/markdown_test.go | 422 +++++++++++++++++++++--
 modules/markup/orgmode/orgmode.go        |  60 ++--
 modules/markup/orgmode/orgmode_test.go   |  23 +-
 modules/markup/renderer.go               |  42 ++-
 modules/templates/helper.go              |   1 -
 modules/templates/util_render.go         |  52 +--
 modules/templates/util_render_test.go    | 141 +++++++-
 routers/common/markup.go                 |  12 +-
 routers/web/feed/convert.go              |  19 +-
 routers/web/feed/profile.go              |   6 +-
 routers/web/org/home.go                  |   6 +-
 routers/web/repo/commit.go               |  18 +-
 routers/web/repo/issue.go                |  51 +--
 routers/web/repo/milestone.go            |  20 +-
 routers/web/repo/projects.go             |  20 +-
 routers/web/repo/release.go              |  20 +-
 routers/web/repo/render.go               |  15 +-
 routers/web/repo/view.go                 |  58 ++--
 routers/web/repo/wiki.go                 |  10 +-
 routers/web/shared/user/header.go        |  12 +-
 routers/web/user/home.go                 |   8 +-
 routers/web/user/profile.go              |  18 +-
 services/mailer/mail.go                  |   8 +-
 services/mailer/mail_release.go          |   8 +-
 templates/repo/branch/list.tmpl          |   4 +-
 templates/repo/commit_page.tmpl          |   8 +-
 templates/repo/commits_list.tmpl         |   4 +-
 templates/repo/commits_list_small.tmpl   |   4 +-
 templates/repo/diff/compare.tmpl         |   2 +-
 templates/repo/graph/commits.tmpl        |   2 +-
 templates/repo/issue/view_title.tmpl     |   2 +-
 templates/repo/view_list.tmpl            |   6 +-
 templates/user/dashboard/feeds.tmpl      |   2 +-
 tests/fuzz/fuzz_test.go                  |   6 +-
 42 files changed, 966 insertions(+), 388 deletions(-)

diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go
index 49927fbbe2..27f6694134 100644
--- a/models/issues/comment_code.go
+++ b/models/issues/comment_code.go
@@ -109,9 +109,11 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
 
 		var err error
 		if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-			Ctx:       ctx,
-			URLPrefix: issue.Repo.Link(),
-			Metas:     issue.Repo.ComposeMetas(),
+			Ctx: ctx,
+			Links: markup.Links{
+				Base: issue.Repo.Link(),
+			},
+			Metas: issue.Repo.ComposeMetas(),
 		}, comment.Content); err != nil {
 			return nil, err
 		}
diff --git a/models/repo/repo.go b/models/repo/repo.go
index 3fd701f874..596a4268c8 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -578,8 +578,7 @@ func (repo *Repository) CanEnableEditor() bool {
 // DescriptionHTML does special handles to description and return HTML string.
 func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
 	desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{
-		Ctx:       ctx,
-		URLPrefix: repo.HTMLURL(),
+		Ctx: ctx,
 		// Don't use Metas to speedup requests
 	}, repo.Description)
 	if err != nil {
diff --git a/modules/markup/external/external.go b/modules/markup/external/external.go
index ffbb6da4da..122517ed11 100644
--- a/modules/markup/external/external.go
+++ b/modules/markup/external/external.go
@@ -79,9 +79,10 @@ func envMark(envName string) string {
 // Render renders the data of the document to HTML via the external tool.
 func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
 	var (
-		urlRawPrefix = strings.Replace(ctx.URLPrefix, "/src/", "/raw/", 1)
-		command      = strings.NewReplacer(envMark("GITEA_PREFIX_SRC"), ctx.URLPrefix,
-			envMark("GITEA_PREFIX_RAW"), urlRawPrefix).Replace(p.Command)
+		command = strings.NewReplacer(
+			envMark("GITEA_PREFIX_SRC"), ctx.Links.SrcLink(),
+			envMark("GITEA_PREFIX_RAW"), ctx.Links.RawLink(),
+		).Replace(p.Command)
 		commands = strings.Fields(command)
 		args     = commands[1:]
 	)
@@ -121,14 +122,14 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
 		ctx.Ctx = graceful.GetManager().ShutdownContext()
 	}
 
-	processCtx, _, finished := process.GetManager().AddContext(ctx.Ctx, fmt.Sprintf("Render [%s] for %s", commands[0], ctx.URLPrefix))
+	processCtx, _, finished := process.GetManager().AddContext(ctx.Ctx, fmt.Sprintf("Render [%s] for %s", commands[0], ctx.Links.SrcLink()))
 	defer finished()
 
 	cmd := exec.CommandContext(processCtx, commands[0], args...)
 	cmd.Env = append(
 		os.Environ(),
-		"GITEA_PREFIX_SRC="+ctx.URLPrefix,
-		"GITEA_PREFIX_RAW="+urlRawPrefix,
+		"GITEA_PREFIX_SRC="+ctx.Links.SrcLink(),
+		"GITEA_PREFIX_RAW="+ctx.Links.RawLink(),
 	)
 	if !p.IsInputFile {
 		cmd.Stdin = input
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 03168b6946..4d3fc32fbe 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -80,15 +80,10 @@ const keywordClass = "issue-keyword"
 
 // IsLink reports whether link fits valid format.
 func IsLink(link []byte) bool {
-	return isLink(link)
-}
-
-// isLink reports whether link fits valid format.
-func isLink(link []byte) bool {
 	return validLinksPattern.Match(link)
 }
 
-func isLinkStr(link string) bool {
+func IsLinkStr(link string) bool {
 	return validLinksPattern.MatchString(link)
 }
 
@@ -344,7 +339,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
 		node = node.FirstChild
 	}
 
-	visitNode(ctx, procs, procs, node)
+	visitNode(ctx, procs, node)
 
 	newNodes := make([]*html.Node, 0, 5)
 
@@ -375,7 +370,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
 	return nil
 }
 
-func visitNode(ctx *RenderContext, procs, textProcs []processor, node *html.Node) {
+func visitNode(ctx *RenderContext, procs []processor, node *html.Node) {
 	// Add user-content- to IDs and "#" links if they don't already have them
 	for idx, attr := range node.Attr {
 		val := strings.TrimPrefix(attr.Val, "#")
@@ -390,35 +385,29 @@ func visitNode(ctx *RenderContext, procs, textProcs []processor, node *html.Node
 		}
 
 		if attr.Key == "class" && attr.Val == "emoji" {
-			textProcs = nil
+			procs = nil
 		}
 	}
 
 	// We ignore code and pre.
 	switch node.Type {
 	case html.TextNode:
-		textNode(ctx, textProcs, node)
+		textNode(ctx, procs, node)
 	case html.ElementNode:
 		if node.Data == "img" {
 			for i, attr := range node.Attr {
 				if attr.Key != "src" {
 					continue
 				}
-				if len(attr.Val) > 0 && !isLinkStr(attr.Val) && !strings.HasPrefix(attr.Val, "data:image/") {
-					prefix := ctx.URLPrefix
-					if ctx.IsWiki {
-						prefix = util.URLJoin(prefix, "wiki", "raw")
-					}
-					prefix = strings.Replace(prefix, "/src/", "/media/", 1)
-
-					attr.Val = util.URLJoin(prefix, attr.Val)
+				if len(attr.Val) > 0 && !IsLinkStr(attr.Val) && !strings.HasPrefix(attr.Val, "data:image/") {
+					attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val)
 				}
 				attr.Val = camoHandleLink(attr.Val)
 				node.Attr[i] = attr
 			}
 		} else if node.Data == "a" {
 			// Restrict text in links to emojis
-			textProcs = emojiProcessors
+			procs = emojiProcessors
 		} else if node.Data == "code" || node.Data == "pre" {
 			return
 		} else if node.Data == "i" {
@@ -444,7 +433,7 @@ func visitNode(ctx *RenderContext, procs, textProcs []processor, node *html.Node
 			}
 		}
 		for n := node.FirstChild; n != nil; n = n.NextSibling {
-			visitNode(ctx, procs, textProcs, n)
+			visitNode(ctx, procs, n)
 		}
 	}
 	// ignore everything else
@@ -641,10 +630,6 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
 }
 
 func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
-	shortLinkProcessorFull(ctx, node, false)
-}
-
-func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) {
 	next := node.NextSibling
 	for node != nil && node != next {
 		m := shortLinkPattern.FindStringSubmatchIndex(node.Data)
@@ -665,7 +650,7 @@ func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) {
 			if equalPos := strings.IndexByte(v, '='); equalPos == -1 {
 				// There is no equal in this argument; this is a mandatory arg
 				if props["name"] == "" {
-					if isLinkStr(v) {
+					if IsLinkStr(v) {
 						// If we clearly see it is a link, we save it so
 
 						// But first we need to ensure, that if both mandatory args provided
@@ -740,7 +725,7 @@ func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) {
 			DataAtom:   atom.A,
 		}
 		childNode.Parent = linkNode
-		absoluteLink := isLinkStr(link)
+		absoluteLink := IsLinkStr(link)
 		if !absoluteLink {
 			if image {
 				link = strings.ReplaceAll(link, " ", "+")
@@ -751,16 +736,9 @@ func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) {
 				link = url.PathEscape(link)
 			}
 		}
-		urlPrefix := ctx.URLPrefix
 		if image {
 			if !absoluteLink {
-				if IsSameDomain(urlPrefix) {
-					urlPrefix = strings.Replace(urlPrefix, "/src/", "/raw/", 1)
-				}
-				if ctx.IsWiki {
-					link = util.URLJoin("wiki", "raw", link)
-				}
-				link = util.URLJoin(urlPrefix, link)
+				link = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), link)
 			}
 			title := props["title"]
 			if title == "" {
@@ -789,18 +767,15 @@ func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) {
 		} else {
 			if !absoluteLink {
 				if ctx.IsWiki {
-					link = util.URLJoin("wiki", link)
+					link = util.URLJoin(ctx.Links.WikiLink(), link)
+				} else {
+					link = util.URLJoin(ctx.Links.SrcLink(), link)
 				}
-				link = util.URLJoin(urlPrefix, link)
 			}
 			childNode.Type = html.TextNode
 			childNode.Data = name
 		}
-		if noLink {
-			linkNode = childNode
-		} else {
-			linkNode.Attr = []html.Attribute{{Key: "href", Val: link}}
-		}
+		linkNode.Attr = []html.Attribute{{Key: "href", Val: link}}
 		replaceContent(node, m[0], m[1], linkNode)
 		node = node.NextSibling.NextSibling
 	}
diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go
index 7b7f6df701..5ba9561915 100644
--- a/modules/markup/html_internal_test.go
+++ b/modules/markup/html_internal_test.go
@@ -287,8 +287,8 @@ func TestRender_IssueIndexPattern_Document(t *testing.T) {
 }
 
 func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) {
-	if ctx.URLPrefix == "" {
-		ctx.URLPrefix = TestAppURL
+	if ctx.Links.Base == "" {
+		ctx.Links.Base = TestRepoURL
 	}
 
 	var buf strings.Builder
@@ -303,19 +303,23 @@ func TestRender_AutoLink(t *testing.T) {
 	test := func(input, expected string) {
 		var buffer strings.Builder
 		err := PostProcess(&RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: TestRepoURL,
-			Metas:     localMetas,
+			Ctx: git.DefaultContext,
+			Links: Links{
+				Base: TestRepoURL,
+			},
+			Metas: localMetas,
 		}, strings.NewReader(input), &buffer)
 		assert.Equal(t, err, nil)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
 
 		buffer.Reset()
 		err = PostProcess(&RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: TestRepoURL,
-			Metas:     localMetas,
-			IsWiki:    true,
+			Ctx: git.DefaultContext,
+			Links: Links{
+				Base: TestRepoURL,
+			},
+			Metas:  localMetas,
+			IsWiki: true,
 		}, strings.NewReader(input), &buffer)
 		assert.Equal(t, err, nil)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
@@ -342,9 +346,11 @@ func TestRender_FullIssueURLs(t *testing.T) {
 	test := func(input, expected string) {
 		var result strings.Builder
 		err := postProcess(&RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: TestRepoURL,
-			Metas:     localMetas,
+			Ctx: git.DefaultContext,
+			Links: Links{
+				Base: TestRepoURL,
+			},
+			Metas: localMetas,
 		}, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result)
 		assert.NoError(t, err)
 		assert.Equal(t, expected, result.String())
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index 8b9ed44db6..a5eecb93bb 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -42,8 +42,10 @@ func TestRender_Commits(t *testing.T) {
 		buffer, err := RenderString(&RenderContext{
 			Ctx:          git.DefaultContext,
 			RelativePath: ".md",
-			URLPrefix:    TestRepoURL,
-			Metas:        localMetas,
+			Links: Links{
+				Base: TestRepoURL,
+			},
+			Metas: localMetas,
 		}, input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@@ -93,8 +95,10 @@ func TestRender_CrossReferences(t *testing.T) {
 		buffer, err := RenderString(&RenderContext{
 			Ctx:          git.DefaultContext,
 			RelativePath: "a.md",
-			URLPrefix:    setting.AppSubURL,
-			Metas:        localMetas,
+			Links: Links{
+				Base: setting.AppSubURL,
+			},
+			Metas: localMetas,
 		}, input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@@ -138,7 +142,9 @@ func TestRender_links(t *testing.T) {
 		buffer, err := RenderString(&RenderContext{
 			Ctx:          git.DefaultContext,
 			RelativePath: "a.md",
-			URLPrefix:    TestRepoURL,
+			Links: Links{
+				Base: TestRepoURL,
+			},
 		}, input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@@ -238,7 +244,9 @@ func TestRender_email(t *testing.T) {
 		res, err := RenderString(&RenderContext{
 			Ctx:          git.DefaultContext,
 			RelativePath: "a.md",
-			URLPrefix:    TestRepoURL,
+			Links: Links{
+				Base: TestRepoURL,
+			},
 		}, input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res))
@@ -309,7 +317,9 @@ func TestRender_emoji(t *testing.T) {
 		buffer, err := RenderString(&RenderContext{
 			Ctx:          git.DefaultContext,
 			RelativePath: "a.md",
-			URLPrefix:    TestRepoURL,
+			Links: Links{
+				Base: TestRepoURL,
+			},
 		}, input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@@ -371,29 +381,34 @@ func TestRender_ShortLinks(t *testing.T) {
 
 	test := func(input, expected, expectedWiki string) {
 		buffer, err := markdown.RenderString(&RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: tree,
+			Ctx: git.DefaultContext,
+			Links: Links{
+				Base:       TestRepoURL,
+				BranchPath: "master",
+			},
 		}, input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
 		buffer, err = markdown.RenderString(&RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: TestRepoURL,
-			Metas:     localMetas,
-			IsWiki:    true,
+			Ctx: git.DefaultContext,
+			Links: Links{
+				Base: TestRepoURL,
+			},
+			Metas:  localMetas,
+			IsWiki: true,
 		}, input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
 	}
 
-	rawtree := util.URLJoin(TestRepoURL, "raw", "master")
+	mediatree := util.URLJoin(TestRepoURL, "media", "master")
 	url := util.URLJoin(tree, "Link")
 	otherURL := util.URLJoin(tree, "Other-Link")
 	encodedURL := util.URLJoin(tree, "Link%3F")
-	imgurl := util.URLJoin(rawtree, "Link.jpg")
-	otherImgurl := util.URLJoin(rawtree, "Link+Other.jpg")
-	encodedImgurl := util.URLJoin(rawtree, "Link+%23.jpg")
-	notencodedImgurl := util.URLJoin(rawtree, "some", "path", "Link+#.jpg")
+	imgurl := util.URLJoin(mediatree, "Link.jpg")
+	otherImgurl := util.URLJoin(mediatree, "Link+Other.jpg")
+	encodedImgurl := util.URLJoin(mediatree, "Link+%23.jpg")
+	notencodedImgurl := util.URLJoin(mediatree, "some", "path", "Link+#.jpg")
 	urlWiki := util.URLJoin(TestRepoURL, "wiki", "Link")
 	otherURLWiki := util.URLJoin(TestRepoURL, "wiki", "Other-Link")
 	encodedURLWiki := util.URLJoin(TestRepoURL, "wiki", "Link%3F")
@@ -475,21 +490,25 @@ func TestRender_ShortLinks(t *testing.T) {
 
 func TestRender_RelativeImages(t *testing.T) {
 	setting.AppURL = TestAppURL
-	tree := util.URLJoin(TestRepoURL, "src", "master")
 
 	test := func(input, expected, expectedWiki string) {
 		buffer, err := markdown.RenderString(&RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: tree,
-			Metas:     localMetas,
+			Ctx: git.DefaultContext,
+			Links: Links{
+				Base:       TestRepoURL,
+				BranchPath: "master",
+			},
+			Metas: localMetas,
 		}, input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
 		buffer, err = markdown.RenderString(&RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: TestRepoURL,
-			Metas:     localMetas,
-			IsWiki:    true,
+			Ctx: git.DefaultContext,
+			Links: Links{
+				Base: TestRepoURL,
+			},
+			Metas:  localMetas,
+			IsWiki: true,
 		}, input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
@@ -521,9 +540,11 @@ func Test_ParseClusterFuzz(t *testing.T) {
 
 	var res strings.Builder
 	err := PostProcess(&RenderContext{
-		Ctx:       git.DefaultContext,
-		URLPrefix: "https://example.com",
-		Metas:     localMetas,
+		Ctx: git.DefaultContext,
+		Links: Links{
+			Base: "https://example.com",
+		},
+		Metas: localMetas,
 	}, strings.NewReader(data), &res)
 	assert.NoError(t, err)
 	assert.NotContains(t, res.String(), "<html")
@@ -532,9 +553,11 @@ func Test_ParseClusterFuzz(t *testing.T) {
 
 	res.Reset()
 	err = PostProcess(&RenderContext{
-		Ctx:       git.DefaultContext,
-		URLPrefix: "https://example.com",
-		Metas:     localMetas,
+		Ctx: git.DefaultContext,
+		Links: Links{
+			Base: "https://example.com",
+		},
+		Metas: localMetas,
 	}, strings.NewReader(data), &res)
 
 	assert.NoError(t, err)
@@ -543,6 +566,7 @@ func Test_ParseClusterFuzz(t *testing.T) {
 
 func TestPostProcess_RenderDocument(t *testing.T) {
 	setting.AppURL = TestAppURL
+	setting.StaticURLPrefix = TestAppURL // can't run standalone
 
 	localMetas := map[string]string{
 		"user": "go-gitea",
@@ -553,9 +577,11 @@ func TestPostProcess_RenderDocument(t *testing.T) {
 	test := func(input, expected string) {
 		var res strings.Builder
 		err := PostProcess(&RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: "https://example.com",
-			Metas:     localMetas,
+			Ctx: git.DefaultContext,
+			Links: Links{
+				Base: "https://example.com",
+			},
+			Metas: localMetas,
 		}, strings.NewReader(input), &res)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res.String()))
@@ -589,9 +615,8 @@ func TestIssue16020(t *testing.T) {
 
 	var res strings.Builder
 	err := PostProcess(&RenderContext{
-		Ctx:       git.DefaultContext,
-		URLPrefix: "https://example.com",
-		Metas:     localMetas,
+		Ctx:   git.DefaultContext,
+		Metas: localMetas,
 	}, strings.NewReader(data), &res)
 	assert.NoError(t, err)
 	assert.Equal(t, data, res.String())
@@ -606,9 +631,8 @@ func BenchmarkEmojiPostprocess(b *testing.B) {
 	for i := 0; i < b.N; i++ {
 		var res strings.Builder
 		err := PostProcess(&RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: "https://example.com",
-			Metas:     localMetas,
+			Ctx:   git.DefaultContext,
+			Metas: localMetas,
 		}, strings.NewReader(data), &res)
 		assert.NoError(b, err)
 	}
@@ -617,8 +641,10 @@ func BenchmarkEmojiPostprocess(b *testing.B) {
 func TestFuzz(t *testing.T) {
 	s := "t/l/issues/8#/../../a"
 	renderContext := RenderContext{
-		Ctx:       git.DefaultContext,
-		URLPrefix: "https://example.com/go-gitea/gitea",
+		Ctx: git.DefaultContext,
+		Links: Links{
+			Base: "https://example.com/go-gitea/gitea",
+		},
 		Metas: map[string]string{
 			"user": "go-gitea",
 			"repo": "gitea",
@@ -635,9 +661,8 @@ func TestIssue18471(t *testing.T) {
 
 	var res strings.Builder
 	err := PostProcess(&RenderContext{
-		Ctx:       git.DefaultContext,
-		URLPrefix: "https://example.com",
-		Metas:     localMetas,
+		Ctx:   git.DefaultContext,
+		Metas: localMetas,
 	}, strings.NewReader(data), &res)
 
 	assert.NoError(t, err)
diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go
index ff4e6b1bd0..b92b90561b 100644
--- a/modules/markup/markdown/goldmark.go
+++ b/modules/markup/markdown/goldmark.go
@@ -87,18 +87,8 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 			// Check if the destination is a real link
 			link := v.Destination
 			if len(link) > 0 && !markup.IsLink(link) {
-				prefix := pc.Get(urlPrefixKey).(string)
-				if pc.Get(isWikiKey).(bool) {
-					prefix = giteautil.URLJoin(prefix, "wiki", "raw")
-				}
-				prefix = strings.Replace(prefix, "/src/", "/media/", 1)
-
-				lnk := strings.TrimLeft(string(link), "/")
-
-				lnk = giteautil.URLJoin(prefix, lnk)
-				link = []byte(lnk)
+				v.Destination = []byte(giteautil.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), string(link)))
 			}
-			v.Destination = link
 
 			parent := n.Parent()
 			// Create a link around image only if parent is not already a link
@@ -107,7 +97,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 
 				// Create a link wrapper
 				wrap := ast.NewLink()
-				wrap.Destination = link
+				wrap.Destination = v.Destination
 				wrap.Title = v.Title
 				wrap.SetAttributeString("target", []byte("_blank"))
 
@@ -143,11 +133,15 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 				link[0] != '#' && !bytes.HasPrefix(link, byteMailto) {
 				// special case: this is not a link, a hash link or a mailto:, so it's a
 				// relative URL
-				lnk := string(link)
-				if pc.Get(isWikiKey).(bool) {
-					lnk = giteautil.URLJoin("wiki", lnk)
+
+				var base string
+				if ctx.IsWiki {
+					base = ctx.Links.WikiLink()
+				} else {
+					base = ctx.Links.Base
 				}
-				link = []byte(giteautil.URLJoin(pc.Get(urlPrefixKey).(string), lnk))
+
+				link = []byte(giteautil.URLJoin(base, string(link)))
 			}
 			if len(link) > 0 && link[0] == '#' {
 				link = []byte("#user-content-" + string(link)[1:])
@@ -188,9 +182,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 			applyElementDir(v)
 		case *ast.Text:
 			if v.SoftLineBreak() && !v.HardLineBreak() {
-				renderMetas := pc.Get(renderMetasKey).(map[string]string)
-				mode := renderMetas["mode"]
-				if mode != "document" {
+				if ctx.Metas["mode"] != "document" {
 					v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
 				} else {
 					v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments)
diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
index 43885889d1..771162b9a3 100644
--- a/modules/markup/markdown/markdown.go
+++ b/modules/markup/markdown/markdown.go
@@ -34,9 +34,6 @@ var (
 )
 
 var (
-	urlPrefixKey     = parser.NewContextKey()
-	isWikiKey        = parser.NewContextKey()
-	renderMetasKey   = parser.NewContextKey()
 	renderContextKey = parser.NewContextKey()
 	renderConfigKey  = parser.NewContextKey()
 )
@@ -66,9 +63,6 @@ func (l *limitWriter) Write(data []byte) (int, error) {
 // newParserContext creates a parser.Context with the render context set
 func newParserContext(ctx *markup.RenderContext) parser.Context {
 	pc := parser.NewContext(parser.WithIDs(newPrefixedIDs()))
-	pc.Set(urlPrefixKey, ctx.URLPrefix)
-	pc.Set(isWikiKey, ctx.IsWiki)
-	pc.Set(renderMetasKey, ctx.Metas)
 	pc.Set(renderContextKey, ctx)
 	return pc
 }
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index f2322b2554..6f2bbae6e0 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -52,16 +52,20 @@ func TestRender_StandardLinks(t *testing.T) {
 
 	test := func(input, expected, expectedWiki string) {
 		buffer, err := RenderString(&markup.RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: setting.AppSubURL,
+			Ctx: git.DefaultContext,
+			Links: markup.Links{
+				Base: setting.AppSubURL,
+			},
 		}, input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
 
 		buffer, err = RenderString(&markup.RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: setting.AppSubURL,
-			IsWiki:    true,
+			Ctx: git.DefaultContext,
+			Links: markup.Links{
+				Base: setting.AppSubURL,
+			},
+			IsWiki: true,
 		}, input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer))
@@ -83,8 +87,10 @@ func TestRender_Images(t *testing.T) {
 
 	test := func(input, expected string) {
 		buffer, err := RenderString(&markup.RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: setting.AppSubURL,
+			Ctx: git.DefaultContext,
+			Links: markup.Links{
+				Base: setting.AppSubURL,
+			},
 		}, input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@@ -107,7 +113,6 @@ func TestRender_Images(t *testing.T) {
 		"[!["+title+"]("+url+")]("+href+")",
 		`<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`)
 
-	url = "/../../.images/src/02/train.jpg"
 	test(
 		"!["+title+"]("+url+")",
 		`<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`)
@@ -286,14 +291,16 @@ func TestTotal_RenderWiki(t *testing.T) {
 	setting.AppURL = AppURL
 	setting.AppSubURL = AppSubURL
 
-	answers := testAnswers(util.URLJoin(AppSubURL, "wiki/"), util.URLJoin(AppSubURL, "wiki", "raw/"))
+	answers := testAnswers(util.URLJoin(AppSubURL, "wiki"), util.URLJoin(AppSubURL, "wiki", "raw"))
 
 	for i := 0; i < len(sameCases); i++ {
 		line, err := RenderString(&markup.RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: AppSubURL,
-			Metas:     localMetas,
-			IsWiki:    true,
+			Ctx: git.DefaultContext,
+			Links: markup.Links{
+				Base: setting.AppSubURL,
+			},
+			Metas:  localMetas,
+			IsWiki: true,
 		}, sameCases[i])
 		assert.NoError(t, err)
 		assert.Equal(t, answers[i], line)
@@ -314,9 +321,11 @@ func TestTotal_RenderWiki(t *testing.T) {
 
 	for i := 0; i < len(testCases); i += 2 {
 		line, err := RenderString(&markup.RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: AppSubURL,
-			IsWiki:    true,
+			Ctx: git.DefaultContext,
+			Links: markup.Links{
+				Base: setting.AppSubURL,
+			},
+			IsWiki: true,
 		}, testCases[i])
 		assert.NoError(t, err)
 		assert.Equal(t, testCases[i+1], line)
@@ -327,13 +336,16 @@ func TestTotal_RenderString(t *testing.T) {
 	setting.AppURL = AppURL
 	setting.AppSubURL = AppSubURL
 
-	answers := testAnswers(util.URLJoin(AppSubURL, "src", "master/"), util.URLJoin(AppSubURL, "raw", "master/"))
+	answers := testAnswers(util.URLJoin(AppSubURL, "src", "master"), util.URLJoin(AppSubURL, "media", "master"))
 
 	for i := 0; i < len(sameCases); i++ {
 		line, err := RenderString(&markup.RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: util.URLJoin(AppSubURL, "src", "master/"),
-			Metas:     localMetas,
+			Ctx: git.DefaultContext,
+			Links: markup.Links{
+				Base:       AppSubURL,
+				BranchPath: "master",
+			},
+			Metas: localMetas,
 		}, sameCases[i])
 		assert.NoError(t, err)
 		assert.Equal(t, answers[i], line)
@@ -343,8 +355,10 @@ func TestTotal_RenderString(t *testing.T) {
 
 	for i := 0; i < len(testCases); i += 2 {
 		line, err := RenderString(&markup.RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: AppSubURL,
+			Ctx: git.DefaultContext,
+			Links: markup.Links{
+				Base: AppSubURL,
+			},
 		}, testCases[i])
 		assert.NoError(t, err)
 		assert.Equal(t, testCases[i+1], line)
@@ -556,3 +570,367 @@ foo: bar
 		assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
 	}
 }
+
+func TestRenderLinks(t *testing.T) {
+	input := `  space @mention-user  
+/just/a/path.bin
+https://example.com/file.bin
+[local link](file.bin)
+[remote link](https://example.com)
+[[local link|file.bin]]
+[[remote link|https://example.com]]
+![local image](image.jpg)
+![remote image](https://example.com/image.jpg)
+[[local image|image.jpg]]
+[[remote link|https://example.com/image.jpg]]
+https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
+https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
+:+1:
+mail@domain.com
+@mention-user test
+#123
+  space  
+`
+	cases := []struct {
+		Links    markup.Links
+		IsWiki   bool
+		Expected string
+	}{
+		{ // 0
+			Links:  markup.Links{},
+			IsWiki: false,
+			Expected: `<p>space @mention-user<br/>
+/just/a/path.bin<br/>
+<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
+<a href="/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/src/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
+<a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
+<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
+<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
+<span class="emoji" aria-label="thumbs up">👍</span><br/>
+<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
+@mention-user test<br/>
+#123<br/>
+space</p>
+`,
+		},
+		{ // 1
+			Links:  markup.Links{},
+			IsWiki: true,
+			Expected: `<p>space @mention-user<br/>
+/just/a/path.bin<br/>
+<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
+<a href="/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/image.jpg" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
+<a href="/wiki/raw/image.jpg" rel="nofollow"><img src="/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
+<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
+<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
+<span class="emoji" aria-label="thumbs up">👍</span><br/>
+<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
+@mention-user test<br/>
+#123<br/>
+space</p>
+`,
+		},
+		{ // 2
+			Links: markup.Links{
+				Base: "https://gitea.io/",
+			},
+			IsWiki: false,
+			Expected: `<p>space @mention-user<br/>
+/just/a/path.bin<br/>
+<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
+<a href="https://gitea.io/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="https://gitea.io/src/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="https://gitea.io/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/image.jpg" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
+<a href="https://gitea.io/image.jpg" rel="nofollow"><img src="https://gitea.io/image.jpg" title="local image" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
+<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
+<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
+<span class="emoji" aria-label="thumbs up">👍</span><br/>
+<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
+@mention-user test<br/>
+#123<br/>
+space</p>
+`,
+		},
+		{ // 3
+			Links: markup.Links{
+				Base: "https://gitea.io/",
+			},
+			IsWiki: true,
+			Expected: `<p>space @mention-user<br/>
+/just/a/path.bin<br/>
+<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
+<a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="https://gitea.io/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/image.jpg" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
+<a href="https://gitea.io/wiki/raw/image.jpg" rel="nofollow"><img src="https://gitea.io/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
+<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
+<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
+<span class="emoji" aria-label="thumbs up">👍</span><br/>
+<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
+@mention-user test<br/>
+#123<br/>
+space</p>
+`,
+		},
+		{ // 4
+			Links: markup.Links{
+				Base: "/relative/path",
+			},
+			IsWiki: false,
+			Expected: `<p>space @mention-user<br/>
+/just/a/path.bin<br/>
+<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
+<a href="/relative/path/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/relative/path/src/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/relative/path/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/image.jpg" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
+<a href="/relative/path/image.jpg" rel="nofollow"><img src="/relative/path/image.jpg" title="local image" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
+<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
+<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
+<span class="emoji" aria-label="thumbs up">👍</span><br/>
+<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
+@mention-user test<br/>
+#123<br/>
+space</p>
+`,
+		},
+		{ // 5
+			Links: markup.Links{
+				Base: "/relative/path",
+			},
+			IsWiki: true,
+			Expected: `<p>space @mention-user<br/>
+/just/a/path.bin<br/>
+<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
+<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
+<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
+<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
+<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
+<span class="emoji" aria-label="thumbs up">👍</span><br/>
+<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
+@mention-user test<br/>
+#123<br/>
+space</p>
+`,
+		},
+		{ // 6
+			Links: markup.Links{
+				Base:       "/user/repo",
+				BranchPath: "branch/main",
+			},
+			IsWiki: false,
+			Expected: `<p>space @mention-user<br/>
+/just/a/path.bin<br/>
+<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
+<a href="/user/repo/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/user/repo/src/branch/main/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/user/repo/media/branch/main/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/image.jpg" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
+<a href="/user/repo/media/branch/main/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/image.jpg" title="local image" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
+<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
+<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
+<span class="emoji" aria-label="thumbs up">👍</span><br/>
+<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
+@mention-user test<br/>
+#123<br/>
+space</p>
+`,
+		},
+		{ // 7
+			Links: markup.Links{
+				Base:       "/relative/path",
+				BranchPath: "branch/main",
+			},
+			IsWiki: true,
+			Expected: `<p>space @mention-user<br/>
+/just/a/path.bin<br/>
+<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
+<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
+<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
+<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
+<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
+<span class="emoji" aria-label="thumbs up">👍</span><br/>
+<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
+@mention-user test<br/>
+#123<br/>
+space</p>
+`,
+		},
+		{ // 8
+			Links: markup.Links{
+				Base:     "/user/repo",
+				TreePath: "sub/folder",
+			},
+			IsWiki: false,
+			Expected: `<p>space @mention-user<br/>
+/just/a/path.bin<br/>
+<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
+<a href="/user/repo/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/user/repo/src/sub/folder/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/user/repo/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/image.jpg" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
+<a href="/user/repo/image.jpg" rel="nofollow"><img src="/user/repo/image.jpg" title="local image" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
+<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
+<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
+<span class="emoji" aria-label="thumbs up">👍</span><br/>
+<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
+@mention-user test<br/>
+#123<br/>
+space</p>
+`,
+		},
+		{ // 9
+			Links: markup.Links{
+				Base:     "/relative/path",
+				TreePath: "sub/folder",
+			},
+			IsWiki: true,
+			Expected: `<p>space @mention-user<br/>
+/just/a/path.bin<br/>
+<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
+<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
+<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
+<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
+<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
+<span class="emoji" aria-label="thumbs up">👍</span><br/>
+<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
+@mention-user test<br/>
+#123<br/>
+space</p>
+`,
+		},
+		{ // 10
+			Links: markup.Links{
+				Base:       "/user/repo",
+				BranchPath: "branch/main",
+				TreePath:   "sub/folder",
+			},
+			IsWiki: false,
+			Expected: `<p>space @mention-user<br/>
+/just/a/path.bin<br/>
+<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
+<a href="/user/repo/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/user/repo/src/branch/main/sub/folder/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/user/repo/media/branch/main/sub/folder/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/sub/folder/image.jpg" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
+<a href="/user/repo/media/branch/main/sub/folder/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/sub/folder/image.jpg" title="local image" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
+<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
+<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
+<span class="emoji" aria-label="thumbs up">👍</span><br/>
+<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
+@mention-user test<br/>
+#123<br/>
+space</p>
+`,
+		},
+		{ // 11
+			Links: markup.Links{
+				Base:       "/relative/path",
+				BranchPath: "branch/main",
+				TreePath:   "sub/folder",
+			},
+			IsWiki: true,
+			Expected: `<p>space @mention-user<br/>
+/just/a/path.bin<br/>
+<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
+<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
+<a href="https://example.com" rel="nofollow">remote link</a><br/>
+<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
+<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt="local image"/></a><br/>
+<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a><br/>
+<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare<br/>
+<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a><br/>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit<br/>
+<span class="emoji" aria-label="thumbs up">👍</span><br/>
+<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a><br/>
+@mention-user test<br/>
+#123<br/>
+space</p>
+`,
+		},
+	}
+
+	for i, c := range cases {
+		result, err := RenderString(&markup.RenderContext{Ctx: context.Background(), Links: c.Links, IsWiki: c.IsWiki}, input)
+		assert.NoError(t, err, "Unexpected error in testcase: %v", i)
+		assert.Equal(t, c.Expected, result, "Unexpected result in testcase %v", i)
+	}
+}
diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go
index d3474c0efb..f7a051476b 100644
--- a/modules/markup/orgmode/orgmode.go
+++ b/modules/markup/orgmode/orgmode.go
@@ -4,7 +4,6 @@
 package markup
 
 import (
-	"bytes"
 	"fmt"
 	"html"
 	"io"
@@ -101,8 +100,7 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
 
 	w := &Writer{
 		HTMLWriter: htmlWriter,
-		URLPrefix:  ctx.URLPrefix,
-		IsWiki:     ctx.IsWiki,
+		Ctx:        ctx,
 	}
 
 	htmlWriter.ExtendingWriter = w
@@ -132,63 +130,53 @@ func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Wri
 // Writer implements org.Writer
 type Writer struct {
 	*org.HTMLWriter
-	URLPrefix string
-	IsWiki    bool
+	Ctx *markup.RenderContext
 }
 
-var byteMailto = []byte("mailto:")
+const mailto = "mailto:"
 
-// WriteRegularLink renders images, links or videos
-func (r *Writer) WriteRegularLink(l org.RegularLink) {
-	link := []byte(html.EscapeString(l.URL))
+func (r *Writer) resolveLink(l org.RegularLink) string {
+	link := html.EscapeString(l.URL)
 	if l.Protocol == "file" {
 		link = link[len("file:"):]
 	}
-	if len(link) > 0 && !markup.IsLink(link) &&
-		link[0] != '#' && !bytes.HasPrefix(link, byteMailto) {
-		lnk := string(link)
-		if r.IsWiki {
-			lnk = util.URLJoin("wiki", lnk)
+	if len(link) > 0 && !markup.IsLinkStr(link) &&
+		link[0] != '#' && !strings.HasPrefix(link, mailto) {
+		base := r.Ctx.Links.Base
+		switch l.Kind() {
+		case "image", "video":
+			base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsWiki)
 		}
-		link = []byte(util.URLJoin(r.URLPrefix, lnk))
+		link = util.URLJoin(base, link)
 	}
+	return link
+}
+
+// WriteRegularLink renders images, links or videos
+func (r *Writer) WriteRegularLink(l org.RegularLink) {
+	link := r.resolveLink(l)
 
 	// Inspired by https://github.com/niklasfasching/go-org/blob/6eb20dbda93cb88c3503f7508dc78cbbc639378f/org/html_writer.go#L406-L427
 	switch l.Kind() {
 	case "image":
 		if l.Description == nil {
-			imageSrc := getMediaURL(link)
-			fmt.Fprintf(r, `<img src="%s" alt="%s" title="%s" />`, imageSrc, link, link)
+			fmt.Fprintf(r, `<img src="%s" alt="%s" />`, link, link)
 		} else {
-			description := strings.TrimPrefix(org.String(l.Description...), "file:")
-			imageSrc := getMediaURL([]byte(description))
+			imageSrc := r.resolveLink(l.Description[0].(org.RegularLink))
 			fmt.Fprintf(r, `<a href="%s"><img src="%s" alt="%s" /></a>`, link, imageSrc, imageSrc)
 		}
 	case "video":
 		if l.Description == nil {
-			imageSrc := getMediaURL(link)
-			fmt.Fprintf(r, `<video src="%s" title="%s">%s</video>`, imageSrc, link, link)
+			fmt.Fprintf(r, `<video src="%s">%s</video>`, link, link)
 		} else {
-			description := strings.TrimPrefix(org.String(l.Description...), "file:")
-			videoSrc := getMediaURL([]byte(description))
-			fmt.Fprintf(r, `<a href="%s"><video src="%s" title="%s"></video></a>`, link, videoSrc, videoSrc)
+			videoSrc := r.resolveLink(l.Description[0].(org.RegularLink))
+			fmt.Fprintf(r, `<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc)
 		}
 	default:
-		description := string(link)
+		description := link
 		if l.Description != nil {
 			description = r.WriteNodesAsString(l.Description...)
 		}
 		fmt.Fprintf(r, `<a href="%s" title="%s">%s</a>`, link, description, description)
 	}
 }
-
-func getMediaURL(l []byte) string {
-	srcURL := string(l)
-
-	// Check if link is valid
-	if len(srcURL) > 0 && !markup.IsLink(l) {
-		srcURL = strings.Replace(srcURL, "/src/", "/media/", 1)
-	}
-
-	return srcURL
-}
diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go
index 8f454e9955..cb9b24f6d0 100644
--- a/modules/markup/orgmode/orgmode_test.go
+++ b/modules/markup/orgmode/orgmode_test.go
@@ -27,8 +27,10 @@ func TestRender_StandardLinks(t *testing.T) {
 
 	test := func(input, expected string) {
 		buffer, err := RenderString(&markup.RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: setting.AppSubURL,
+			Ctx: git.DefaultContext,
+			Links: markup.Links{
+				Base: setting.AppSubURL,
+			},
 		}, input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@@ -48,8 +50,10 @@ func TestRender_Media(t *testing.T) {
 
 	test := func(input, expected string) {
 		buffer, err := RenderString(&markup.RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: setting.AppSubURL,
+			Ctx: git.DefaultContext,
+			Links: markup.Links{
+				Base: setting.AppSubURL,
+			},
 		}, input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
@@ -59,19 +63,19 @@ func TestRender_Media(t *testing.T) {
 	result := util.URLJoin(AppSubURL, url)
 
 	test("[[file:"+url+"]]",
-		"<p><img src=\""+result+"\" alt=\""+result+"\" title=\""+result+"\" /></p>")
+		"<p><img src=\""+result+"\" alt=\""+result+"\" /></p>")
 
 	// With description.
 	test("[[https://example.com][https://example.com/example.svg]]",
 		`<p><a href="https://example.com"><img src="https://example.com/example.svg" alt="https://example.com/example.svg" /></a></p>`)
 	test("[[https://example.com][https://example.com/example.mp4]]",
-		`<p><a href="https://example.com"><video src="https://example.com/example.mp4" title="https://example.com/example.mp4"></video></a></p>`)
+		`<p><a href="https://example.com"><video src="https://example.com/example.mp4">https://example.com/example.mp4</video></a></p>`)
 
 	// Without description.
 	test("[[https://example.com/example.svg]]",
-		`<p><img src="https://example.com/example.svg" alt="https://example.com/example.svg" title="https://example.com/example.svg" /></p>`)
+		`<p><img src="https://example.com/example.svg" alt="https://example.com/example.svg" /></p>`)
 	test("[[https://example.com/example.mp4]]",
-		`<p><video src="https://example.com/example.mp4" title="https://example.com/example.mp4">https://example.com/example.mp4</video></p>`)
+		`<p><video src="https://example.com/example.mp4">https://example.com/example.mp4</video></p>`)
 }
 
 func TestRender_Source(t *testing.T) {
@@ -80,8 +84,7 @@ func TestRender_Source(t *testing.T) {
 
 	test := func(input, expected string) {
 		buffer, err := RenderString(&markup.RenderContext{
-			Ctx:       git.DefaultContext,
-			URLPrefix: setting.AppSubURL,
+			Ctx: git.DefaultContext,
 		}, input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
index 0331c3742a..5a7adcc553 100644
--- a/modules/markup/renderer.go
+++ b/modules/markup/renderer.go
@@ -16,6 +16,7 @@ import (
 
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
 
 	"github.com/yuin/goldmark/ast"
 )
@@ -69,7 +70,7 @@ type RenderContext struct {
 	RelativePath     string // relative path from tree root of the branch
 	Type             string
 	IsWiki           bool
-	URLPrefix        string
+	Links            Links
 	Metas            map[string]string
 	DefaultLink      string
 	GitRepo          *git.Repository
@@ -80,6 +81,45 @@ type RenderContext struct {
 	InStandalonePage bool // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page
 }
 
+type Links struct {
+	Base       string
+	BranchPath string
+	TreePath   string
+}
+
+func (l *Links) HasBranchInfo() bool {
+	return l.BranchPath != ""
+}
+
+func (l *Links) SrcLink() string {
+	return util.URLJoin(l.Base, "src", l.BranchPath, l.TreePath)
+}
+
+func (l *Links) MediaLink() string {
+	return util.URLJoin(l.Base, "media", l.BranchPath, l.TreePath)
+}
+
+func (l *Links) RawLink() string {
+	return util.URLJoin(l.Base, "raw", l.BranchPath, l.TreePath)
+}
+
+func (l *Links) WikiLink() string {
+	return util.URLJoin(l.Base, "wiki")
+}
+
+func (l *Links) WikiRawLink() string {
+	return util.URLJoin(l.Base, "wiki/raw")
+}
+
+func (l *Links) ResolveMediaLink(isWiki bool) string {
+	if isWiki {
+		return l.WikiRawLink()
+	} else if l.HasBranchInfo() {
+		return l.MediaLink()
+	}
+	return l.Base
+}
+
 // Cancel runs any cleanup functions that have been registered for this Ctx
 func (ctx *RenderContext) Cancel() {
 	if ctx == nil {
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index bb1411cbfd..7605320ed2 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -157,7 +157,6 @@ func NewFuncMap() template.FuncMap {
 		"RenderEmoji":      RenderEmoji,
 		"RenderEmojiPlain": emoji.ReplaceAliases,
 		"ReactionToEmoji":  ReactionToEmoji,
-		"RenderNote":       RenderNote,
 
 		"RenderMarkdownToHtml": RenderMarkdownToHtml,
 		"RenderLabel":          RenderLabel,
diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go
index 639d58126c..1d9635410b 100644
--- a/modules/templates/util_render.go
+++ b/modules/templates/util_render.go
@@ -24,21 +24,13 @@ import (
 )
 
 // RenderCommitMessage renders commit message with XSS-safe and special links.
-func RenderCommitMessage(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
-	return RenderCommitMessageLink(ctx, msg, urlPrefix, "", metas)
-}
-
-// RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
-// default url, handling for special links.
-func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
+func RenderCommitMessage(ctx context.Context, msg string, metas map[string]string) template.HTML {
 	cleanMsg := template.HTMLEscapeString(msg)
 	// we can safely assume that it will not return any error, since there
 	// shouldn't be any special HTML.
 	fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
-		Ctx:         ctx,
-		URLPrefix:   urlPrefix,
-		DefaultLink: urlDefault,
-		Metas:       metas,
+		Ctx:   ctx,
+		Metas: metas,
 	}, cleanMsg)
 	if err != nil {
 		log.Error("RenderCommitMessage: %v", err)
@@ -51,9 +43,9 @@ func RenderCommitMessageLink(ctx context.Context, msg, urlPrefix, urlDefault str
 	return template.HTML(msgLines[0])
 }
 
-// RenderCommitMessageLinkSubject renders commit message as a XXS-safe link to
+// RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to
 // the provided default url, handling for special links without email to links.
-func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefault string, metas map[string]string) template.HTML {
+func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlDefault string, metas map[string]string) template.HTML {
 	msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace)
 	lineEnd := strings.IndexByte(msgLine, '\n')
 	if lineEnd > 0 {
@@ -68,7 +60,6 @@ func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefa
 	// shouldn't be any special HTML.
 	renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{
 		Ctx:         ctx,
-		URLPrefix:   urlPrefix,
 		DefaultLink: urlDefault,
 		Metas:       metas,
 	}, template.HTMLEscapeString(msgLine))
@@ -80,7 +71,7 @@ func RenderCommitMessageLinkSubject(ctx context.Context, msg, urlPrefix, urlDefa
 }
 
 // RenderCommitBody extracts the body of a commit message without its title.
-func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
+func RenderCommitBody(ctx context.Context, msg string, metas map[string]string) template.HTML {
 	msgLine := strings.TrimSpace(msg)
 	lineEnd := strings.IndexByte(msgLine, '\n')
 	if lineEnd > 0 {
@@ -94,9 +85,8 @@ func RenderCommitBody(ctx context.Context, msg, urlPrefix string, metas map[stri
 	}
 
 	renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
-		Ctx:       ctx,
-		URLPrefix: urlPrefix,
-		Metas:     metas,
+		Ctx:   ctx,
+		Metas: metas,
 	}, template.HTMLEscapeString(msgLine))
 	if err != nil {
 		log.Error("RenderCommitMessage: %v", err)
@@ -115,11 +105,10 @@ func RenderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
 }
 
 // RenderIssueTitle renders issue/pull title with defined post processors
-func RenderIssueTitle(ctx context.Context, text, urlPrefix string, metas map[string]string) template.HTML {
+func RenderIssueTitle(ctx context.Context, text string, metas map[string]string) template.HTML {
 	renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{
-		Ctx:       ctx,
-		URLPrefix: urlPrefix,
-		Metas:     metas,
+		Ctx:   ctx,
+		Metas: metas,
 	}, template.HTMLEscapeString(text))
 	if err != nil {
 		log.Error("RenderIssueTitle: %v", err)
@@ -211,25 +200,10 @@ func ReactionToEmoji(reaction string) template.HTML {
 	return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction)))
 }
 
-// RenderNote renders the contents of a git-notes file as a commit message.
-func RenderNote(ctx context.Context, msg, urlPrefix string, metas map[string]string) template.HTML {
-	cleanMsg := template.HTMLEscapeString(msg)
-	fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{
-		Ctx:       ctx,
-		URLPrefix: urlPrefix,
-		Metas:     metas,
-	}, cleanMsg)
-	if err != nil {
-		log.Error("RenderNote: %v", err)
-		return ""
-	}
-	return template.HTML(fullMessage)
-}
-
 func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //nolint:revive
 	output, err := markdown.RenderString(&markup.RenderContext{
-		Ctx:       ctx,
-		URLPrefix: setting.AppSubURL,
+		Ctx:   ctx,
+		Metas: map[string]string{"mode": "document"},
 	}, input)
 	if err != nil {
 		log.Error("RenderString: %v", err)
diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go
index 29d3ed3a56..8648967d38 100644
--- a/modules/templates/util_render_test.go
+++ b/modules/templates/util_render_test.go
@@ -6,17 +6,64 @@ package templates
 import (
 	"context"
 	"html/template"
+	"os"
 	"testing"
 
+	"code.gitea.io/gitea/models/unittest"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/markup"
+
 	"github.com/stretchr/testify/assert"
 )
 
+const testInput = `  space @mention-user  
+/just/a/path.bin
+https://example.com/file.bin
+[local link](file.bin)
+[remote link](https://example.com)
+[[local link|file.bin]]
+[[remote link|https://example.com]]
+![local image](image.jpg)
+![remote image](https://example.com/image.jpg)
+[[local image|image.jpg]]
+[[remote link|https://example.com/image.jpg]]
+https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
+https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
+:+1:
+mail@domain.com
+@mention-user test
+#123
+  space  
+`
+
+var testMetas = map[string]string{
+	"user":     "user13",
+	"repo":     "repo11",
+	"repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/",
+	"mode":     "comment",
+}
+
+func TestMain(m *testing.M) {
+	unittest.InitSettings()
+	if err := git.InitSimple(context.Background()); err != nil {
+		log.Fatal("git init failed, err: %v", err)
+	}
+	markup.Init(&markup.ProcessorHelper{
+		IsUsernameMentionable: func(ctx context.Context, username string) bool {
+			return username == "mention-user"
+		},
+	})
+	os.Exit(m.Run())
+}
+
 func TestRenderCommitBody(t *testing.T) {
 	type args struct {
-		ctx       context.Context
-		msg       string
-		urlPrefix string
-		metas     map[string]string
+		ctx   context.Context
+		msg   string
+		metas map[string]string
 	}
 	tests := []struct {
 		name string
@@ -50,7 +97,91 @@ func TestRenderCommitBody(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			assert.Equalf(t, tt.want, RenderCommitBody(tt.args.ctx, tt.args.msg, tt.args.urlPrefix, tt.args.metas), "RenderCommitBody(%v, %v, %v, %v)", tt.args.ctx, tt.args.msg, tt.args.urlPrefix, tt.args.metas)
+			assert.Equalf(t, tt.want, RenderCommitBody(tt.args.ctx, tt.args.msg, tt.args.metas), "RenderCommitBody(%v, %v, %v)", tt.args.ctx, tt.args.msg, tt.args.metas)
 		})
 	}
+
+	expected := `/just/a/path.bin
+<a href="https://example.com/file.bin" class="link">https://example.com/file.bin</a>
+[local link](file.bin)
+[remote link](<a href="https://example.com" class="link">https://example.com</a>)
+[[local link|file.bin]]
+[[remote link|<a href="https://example.com" class="link">https://example.com</a>]]
+![local image](image.jpg)
+![remote image](<a href="https://example.com/image.jpg" class="link">https://example.com/image.jpg</a>)
+[[local image|image.jpg]]
+[[remote link|<a href="https://example.com/image.jpg" class="link">https://example.com/image.jpg</a>]]
+<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" class="compare"><code class="nohighlight">88fc37a3c0...12fc37a3c0 (hash)</code></a>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
+<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" class="commit"><code class="nohighlight">88fc37a3c0</code></a>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
+<span class="emoji" aria-label="thumbs up">👍</span>
+<a href="mailto:mail@domain.com" class="mailto">mail@domain.com</a>
+<a href="http://localhost:3000/mention-user" class="mention">@mention-user</a> test
+<a href="http://localhost:3000/user13/repo11/issues/123" class="ref-issue">#123</a>
+  space`
+
+	assert.EqualValues(t, expected, RenderCommitBody(context.Background(), testInput, testMetas))
+}
+
+func TestRenderCommitMessage(t *testing.T) {
+	expected := `space <a href="http://localhost:3000/mention-user" class="mention">@mention-user</a>  `
+
+	assert.EqualValues(t, expected, RenderCommitMessage(context.Background(), testInput, testMetas))
+}
+
+func TestRenderCommitMessageLinkSubject(t *testing.T) {
+	expected := `<a href="https://example.com/link" class="default-link muted">space </a><a href="http://localhost:3000/mention-user" class="mention">@mention-user</a>`
+
+	assert.EqualValues(t, expected, RenderCommitMessageLinkSubject(context.Background(), testInput, "https://example.com/link", testMetas))
+}
+
+func TestRenderIssueTitle(t *testing.T) {
+	expected := `  space @mention-user  
+/just/a/path.bin
+https://example.com/file.bin
+[local link](file.bin)
+[remote link](https://example.com)
+[[local link|file.bin]]
+[[remote link|https://example.com]]
+![local image](image.jpg)
+![remote image](https://example.com/image.jpg)
+[[local image|image.jpg]]
+[[remote link|https://example.com/image.jpg]]
+https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
+https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
+<span class="emoji" aria-label="thumbs up">👍</span>
+mail@domain.com
+@mention-user test
+<a href="http://localhost:3000/user13/repo11/issues/123" class="ref-issue">#123</a>
+  space  
+`
+	assert.EqualValues(t, expected, RenderIssueTitle(context.Background(), testInput, testMetas))
+}
+
+func TestRenderMarkdownToHtml(t *testing.T) {
+	expected := `<p>space <a href="http://localhost:3000/mention-user" rel="nofollow">@mention-user</a><br/>
+/just/a/path.bin
+<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a>
+<a href="/file.bin" rel="nofollow">local link</a>
+<a href="https://example.com" rel="nofollow">remote link</a>
+<a href="/src/file.bin" rel="nofollow">local link</a>
+<a href="https://example.com" rel="nofollow">remote link</a>
+<a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image"/></a>
+<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a>
+<a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a>
+<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a>
+<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
+<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a>
+com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
+<span class="emoji" aria-label="thumbs up">👍</span>
+<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a>
+<a href="http://localhost:3000/mention-user" rel="nofollow">@mention-user</a> test
+#123
+space</p>
+`
+	assert.EqualValues(t, expected, RenderMarkdownToHtml(context.Background(), testInput))
 }
diff --git a/routers/common/markup.go b/routers/common/markup.go
index 5f412014d7..e2e206f0c9 100644
--- a/routers/common/markup.go
+++ b/routers/common/markup.go
@@ -32,8 +32,10 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
 	case "markdown":
 		// Raw markdown
 		if err := markdown.RenderRaw(&markup.RenderContext{
-			Ctx:       ctx,
-			URLPrefix: urlPrefix,
+			Ctx: ctx,
+			Links: markup.Links{
+				Base: urlPrefix,
+			},
 		}, strings.NewReader(text), ctx.Resp); err != nil {
 			ctx.Error(http.StatusInternalServerError, err.Error())
 		}
@@ -75,8 +77,10 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
 	}
 
 	if err := markup.Render(&markup.RenderContext{
-		Ctx:          ctx,
-		URLPrefix:    urlPrefix,
+		Ctx: ctx,
+		Links: markup.Links{
+			Base: urlPrefix,
+		},
 		Metas:        meta,
 		IsWiki:       wiki,
 		Type:         markupType,
diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go
index 04078955bb..9d43887ce4 100644
--- a/routers/web/feed/convert.go
+++ b/routers/web/feed/convert.go
@@ -51,9 +51,11 @@ func toReleaseLink(ctx *context.Context, act *activities_model.Action) string {
 // If rendering fails, the original markdown text is returned
 func renderMarkdown(ctx *context.Context, act *activities_model.Action, content string) string {
 	markdownCtx := &markup.RenderContext{
-		Ctx:       ctx,
-		URLPrefix: act.GetRepoLink(ctx),
-		Type:      markdown.MarkupName,
+		Ctx: ctx,
+		Links: markup.Links{
+			Base: act.GetRepoLink(ctx),
+		},
+		Type: markdown.MarkupName,
 		Metas: map[string]string{
 			"user": act.GetRepoUserName(ctx),
 			"repo": act.GetRepoName(ctx),
@@ -199,7 +201,6 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
 			switch act.OpType {
 			case activities_model.ActionCommitRepo, activities_model.ActionMirrorSyncPush:
 				push := templates.ActionContent2Commits(act)
-				repoLink := act.GetRepoAbsoluteLink(ctx)
 
 				for _, commit := range push.Commits {
 					if len(desc) != 0 {
@@ -208,7 +209,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
 					desc += fmt.Sprintf("<a href=\"%s\">%s</a>\n%s",
 						html.EscapeString(fmt.Sprintf("%s/commit/%s", act.GetRepoAbsoluteLink(ctx), commit.Sha1)),
 						commit.Sha1,
-						templates.RenderCommitMessage(ctx, commit.Message, repoLink, nil),
+						templates.RenderCommitMessage(ctx, commit.Message, nil),
 					)
 				}
 
@@ -288,9 +289,11 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release, i
 
 		link := &feeds.Link{Href: rel.HTMLURL()}
 		content, err = markdown.RenderString(&markup.RenderContext{
-			Ctx:       ctx,
-			URLPrefix: rel.Repo.Link(),
-			Metas:     rel.Repo.ComposeMetas(),
+			Ctx: ctx,
+			Links: markup.Links{
+				Base: rel.Repo.Link(),
+			},
+			Metas: rel.Repo.ComposeMetas(),
 		}, rel.Note)
 
 		if err != nil {
diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go
index ce86727e24..04f84c0c8d 100644
--- a/routers/web/feed/profile.go
+++ b/routers/web/feed/profile.go
@@ -42,8 +42,10 @@ func showUserFeed(ctx *context.Context, formatType string) {
 	}
 
 	ctxUserDescription, err := markdown.RenderString(&markup.RenderContext{
-		Ctx:       ctx,
-		URLPrefix: ctx.ContextUser.HTMLURL(),
+		Ctx: ctx,
+		Links: markup.Links{
+			Base: ctx.ContextUser.HTMLURL(),
+		},
 		Metas: map[string]string{
 			"user": ctx.ContextUser.GetDisplayName(),
 		},
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index ec866eb6b3..05ccd48fca 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -44,10 +44,8 @@ func Home(ctx *context.Context) {
 	ctx.Data["Title"] = org.DisplayName()
 	if len(org.Description) != 0 {
 		desc, err := markdown.RenderString(&markup.RenderContext{
-			Ctx:       ctx,
-			URLPrefix: ctx.Repo.RepoLink,
-			Metas:     map[string]string{"mode": "document"},
-			GitRepo:   ctx.Repo.GitRepo,
+			Ctx:   ctx,
+			Metas: map[string]string{"mode": "document"},
 		}, org.Description)
 		if err != nil {
 			ctx.ServerError("RenderString", err)
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index a6eb7efeb0..7c8dcbbe7b 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -7,7 +7,9 @@ package repo
 import (
 	"errors"
 	"fmt"
+	"html/template"
 	"net/http"
+	"path"
 	"strings"
 
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
@@ -21,7 +23,9 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/gitgraph"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/markup"
 	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/gitdiff"
 	git_service "code.gitea.io/gitea/services/repository"
 )
@@ -370,9 +374,21 @@ func Diff(ctx *context.Context) {
 	note := &git.Note{}
 	err = git.GetNote(ctx, ctx.Repo.GitRepo, commitID, note)
 	if err == nil {
-		ctx.Data["Note"] = string(charset.ToUTF8WithFallback(note.Message))
 		ctx.Data["NoteCommit"] = note.Commit
 		ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit)
+		ctx.Data["NoteRendered"], err = markup.RenderCommitMessage(&markup.RenderContext{
+			Links: markup.Links{
+				Base:       ctx.Repo.RepoLink,
+				BranchPath: path.Join("commit", util.PathEscapeSegments(commitID)),
+			},
+			Metas:   ctx.Repo.Repository.ComposeMetas(),
+			GitRepo: ctx.Repo.GitRepo,
+			Ctx:     ctx,
+		}, template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message))))
+		if err != nil {
+			ctx.ServerError("RenderCommitMessage", err)
+			return
+		}
 	}
 
 	ctx.Data["BranchName"], err = commit.GetBranchName()
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 87493dd8ad..fe42e86345 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -1424,12 +1424,13 @@ func ViewIssue(ctx *context.Context) {
 		}
 	}
 	ctx.Data["IssueWatch"] = iw
-
 	issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-		URLPrefix: ctx.Repo.RepoLink,
-		Metas:     ctx.Repo.Repository.ComposeMetas(),
-		GitRepo:   ctx.Repo.GitRepo,
-		Ctx:       ctx,
+		Links: markup.Links{
+			Base: ctx.Repo.RepoLink,
+		},
+		Metas:   ctx.Repo.Repository.ComposeMetas(),
+		GitRepo: ctx.Repo.GitRepo,
+		Ctx:     ctx,
 	}, issue.Content)
 	if err != nil {
 		ctx.ServerError("RenderString", err)
@@ -1589,10 +1590,12 @@ func ViewIssue(ctx *context.Context) {
 			}
 
 			comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-				URLPrefix: ctx.Repo.RepoLink,
-				Metas:     ctx.Repo.Repository.ComposeMetas(),
-				GitRepo:   ctx.Repo.GitRepo,
-				Ctx:       ctx,
+				Links: markup.Links{
+					Base: ctx.Repo.RepoLink,
+				},
+				Metas:   ctx.Repo.Repository.ComposeMetas(),
+				GitRepo: ctx.Repo.GitRepo,
+				Ctx:     ctx,
 			}, comment.Content)
 			if err != nil {
 				ctx.ServerError("RenderString", err)
@@ -1666,10 +1669,12 @@ func ViewIssue(ctx *context.Context) {
 			}
 		} else if comment.Type.HasContentSupport() {
 			comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-				URLPrefix: ctx.Repo.RepoLink,
-				Metas:     ctx.Repo.Repository.ComposeMetas(),
-				GitRepo:   ctx.Repo.GitRepo,
-				Ctx:       ctx,
+				Links: markup.Links{
+					Base: ctx.Repo.RepoLink,
+				},
+				Metas:   ctx.Repo.Repository.ComposeMetas(),
+				GitRepo: ctx.Repo.GitRepo,
+				Ctx:     ctx,
 			}, comment.Content)
 			if err != nil {
 				ctx.ServerError("RenderString", err)
@@ -2220,10 +2225,12 @@ func UpdateIssueContent(ctx *context.Context) {
 	}
 
 	content, err := markdown.RenderString(&markup.RenderContext{
-		URLPrefix: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
-		Metas:     ctx.Repo.Repository.ComposeMetas(),
-		GitRepo:   ctx.Repo.GitRepo,
-		Ctx:       ctx,
+		Links: markup.Links{
+			Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
+		},
+		Metas:   ctx.Repo.Repository.ComposeMetas(),
+		GitRepo: ctx.Repo.GitRepo,
+		Ctx:     ctx,
 	}, issue.Content)
 	if err != nil {
 		ctx.ServerError("RenderString", err)
@@ -3129,10 +3136,12 @@ func UpdateCommentContent(ctx *context.Context) {
 	}
 
 	content, err := markdown.RenderString(&markup.RenderContext{
-		URLPrefix: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
-		Metas:     ctx.Repo.Repository.ComposeMetas(),
-		GitRepo:   ctx.Repo.GitRepo,
-		Ctx:       ctx,
+		Links: markup.Links{
+			Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
+		},
+		Metas:   ctx.Repo.Repository.ComposeMetas(),
+		GitRepo: ctx.Repo.GitRepo,
+		Ctx:     ctx,
 	}, comment.Content)
 	if err != nil {
 		ctx.ServerError("RenderString", err)
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
index eb27c4b00f..a9fcba4bae 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -81,10 +81,12 @@ func Milestones(ctx *context.Context) {
 	}
 	for _, m := range miles {
 		m.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-			URLPrefix: ctx.Repo.RepoLink,
-			Metas:     ctx.Repo.Repository.ComposeMetas(),
-			GitRepo:   ctx.Repo.GitRepo,
-			Ctx:       ctx,
+			Links: markup.Links{
+				Base: ctx.Repo.RepoLink,
+			},
+			Metas:   ctx.Repo.Repository.ComposeMetas(),
+			GitRepo: ctx.Repo.GitRepo,
+			Ctx:     ctx,
 		}, m.Content)
 		if err != nil {
 			ctx.ServerError("RenderString", err)
@@ -275,10 +277,12 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
 	}
 
 	milestone.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-		URLPrefix: ctx.Repo.RepoLink,
-		Metas:     ctx.Repo.Repository.ComposeMetas(),
-		GitRepo:   ctx.Repo.GitRepo,
-		Ctx:       ctx,
+		Links: markup.Links{
+			Base: ctx.Repo.RepoLink,
+		},
+		Metas:   ctx.Repo.Repository.ComposeMetas(),
+		GitRepo: ctx.Repo.GitRepo,
+		Ctx:     ctx,
 	}, milestone.Content)
 	if err != nil {
 		ctx.ServerError("RenderString", err)
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 541f24c7f4..09d9c1148d 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -86,10 +86,12 @@ func Projects(ctx *context.Context) {
 
 	for i := range projects {
 		projects[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-			URLPrefix: ctx.Repo.RepoLink,
-			Metas:     ctx.Repo.Repository.ComposeMetas(),
-			GitRepo:   ctx.Repo.GitRepo,
-			Ctx:       ctx,
+			Links: markup.Links{
+				Base: ctx.Repo.RepoLink,
+			},
+			Metas:   ctx.Repo.Repository.ComposeMetas(),
+			GitRepo: ctx.Repo.GitRepo,
+			Ctx:     ctx,
 		}, projects[i].Description)
 		if err != nil {
 			ctx.ServerError("RenderString", err)
@@ -353,10 +355,12 @@ func ViewProject(ctx *context.Context) {
 	ctx.Data["LinkedPRs"] = linkedPrsMap
 
 	project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-		URLPrefix: ctx.Repo.RepoLink,
-		Metas:     ctx.Repo.Repository.ComposeMetas(),
-		GitRepo:   ctx.Repo.GitRepo,
-		Ctx:       ctx,
+		Links: markup.Links{
+			Base: ctx.Repo.RepoLink,
+		},
+		Metas:   ctx.Repo.Repository.ComposeMetas(),
+		GitRepo: ctx.Repo.GitRepo,
+		Ctx:     ctx,
 	}, project.Description)
 	if err != nil {
 		ctx.ServerError("RenderString", err)
diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index 338dc34cf4..0264242219 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -175,10 +175,12 @@ func releasesOrTags(ctx *context.Context, isTagList bool) {
 		}
 
 		r.Note, err = markdown.RenderString(&markup.RenderContext{
-			URLPrefix: ctx.Repo.RepoLink,
-			Metas:     ctx.Repo.Repository.ComposeMetas(),
-			GitRepo:   ctx.Repo.GitRepo,
-			Ctx:       ctx,
+			Links: markup.Links{
+				Base: ctx.Repo.RepoLink,
+			},
+			Metas:   ctx.Repo.Repository.ComposeMetas(),
+			GitRepo: ctx.Repo.GitRepo,
+			Ctx:     ctx,
 		}, r.Note)
 		if err != nil {
 			ctx.ServerError("RenderString", err)
@@ -281,10 +283,12 @@ func SingleRelease(ctx *context.Context) {
 		}
 	}
 	release.Note, err = markdown.RenderString(&markup.RenderContext{
-		URLPrefix: ctx.Repo.RepoLink,
-		Metas:     ctx.Repo.Repository.ComposeMetas(),
-		GitRepo:   ctx.Repo.GitRepo,
-		Ctx:       ctx,
+		Links: markup.Links{
+			Base: ctx.Repo.RepoLink,
+		},
+		Metas:   ctx.Repo.Repository.ComposeMetas(),
+		GitRepo: ctx.Repo.GitRepo,
+		Ctx:     ctx,
 	}, release.Note)
 	if err != nil {
 		ctx.ServerError("RenderString", err)
diff --git a/routers/web/repo/render.go b/routers/web/repo/render.go
index f07b4e8c11..c6bc0dcf19 100644
--- a/routers/web/repo/render.go
+++ b/routers/web/repo/render.go
@@ -57,16 +57,15 @@ func RenderFile(ctx *context.Context) {
 		return
 	}
 
-	treeLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
-	if ctx.Repo.TreePath != "" {
-		treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
-	}
-
 	ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts")
 	err = markup.Render(&markup.RenderContext{
-		Ctx:              ctx,
-		RelativePath:     ctx.Repo.TreePath,
-		URLPrefix:        path.Dir(treeLink),
+		Ctx:          ctx,
+		RelativePath: ctx.Repo.TreePath,
+		Links: markup.Links{
+			Base:       ctx.Repo.RepoLink,
+			BranchPath: ctx.Repo.BranchNameSubURL(),
+			TreePath:   path.Dir(ctx.Repo.TreePath),
+		},
 		Metas:            ctx.Repo.Repository.ComposeDocumentMetas(),
 		GitRepo:          ctx.Repo.GitRepo,
 		InStandalonePage: true,
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index b17d11ec63..759c011cb2 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -158,7 +158,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
 	return "", readmeFile, nil
 }
 
-func renderDirectory(ctx *context.Context, treeLink string) {
+func renderDirectory(ctx *context.Context) {
 	entries := renderDirectoryFiles(ctx, 1*time.Second)
 	if ctx.Written() {
 		return
@@ -175,7 +175,7 @@ func renderDirectory(ctx *context.Context, treeLink string) {
 		return
 	}
 
-	renderReadmeFile(ctx, subfolder, readmeFile, treeLink)
+	renderReadmeFile(ctx, subfolder, readmeFile)
 }
 
 // localizedExtensions prepends the provided language code with and without a
@@ -259,7 +259,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte,
 	return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil
 }
 
-func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry, readmeTreelink string) {
+func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) {
 	target := readmeFile
 	if readmeFile != nil && readmeFile.IsLink() {
 		target, _ = readmeFile.FollowLinks()
@@ -312,9 +312,13 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr
 		ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
 			Ctx:          ctx,
 			RelativePath: path.Join(ctx.Repo.TreePath, readmeFile.Name()), // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
-			URLPrefix:    path.Join(readmeTreelink, subfolder),
-			Metas:        ctx.Repo.Repository.ComposeDocumentMetas(),
-			GitRepo:      ctx.Repo.GitRepo,
+			Links: markup.Links{
+				Base:       ctx.Repo.RepoLink,
+				BranchPath: ctx.Repo.BranchNameSubURL(),
+				TreePath:   path.Join(ctx.Repo.TreePath, subfolder),
+			},
+			Metas:   ctx.Repo.Repository.ComposeDocumentMetas(),
+			GitRepo: ctx.Repo.GitRepo,
 		}, rd)
 		if err != nil {
 			log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err)
@@ -333,7 +337,7 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr
 	}
 }
 
-func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink string) {
+func renderFile(ctx *context.Context, entry *git.TreeEntry) {
 	ctx.Data["IsViewFile"] = true
 	ctx.Data["HideRepoInfo"] = true
 	blob := entry.Blob()
@@ -347,7 +351,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
 	ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName)
 	ctx.Data["FileIsSymlink"] = entry.IsLink()
 	ctx.Data["FileName"] = blob.Name()
-	ctx.Data["RawFileLink"] = rawLink + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
+	ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
 
 	if ctx.Repo.TreePath == ".editorconfig" {
 		_, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
@@ -475,9 +479,13 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
 				Ctx:          ctx,
 				Type:         markupType,
 				RelativePath: ctx.Repo.TreePath,
-				URLPrefix:    path.Dir(treeLink),
-				Metas:        metas,
-				GitRepo:      ctx.Repo.GitRepo,
+				Links: markup.Links{
+					Base:       ctx.Repo.RepoLink,
+					BranchPath: ctx.Repo.BranchNameSubURL(),
+					TreePath:   path.Dir(ctx.Repo.TreePath),
+				},
+				Metas:   metas,
+				GitRepo: ctx.Repo.GitRepo,
 			}, rd)
 			if err != nil {
 				ctx.ServerError("Render", err)
@@ -576,9 +584,13 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
 			ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
 				Ctx:          ctx,
 				RelativePath: ctx.Repo.TreePath,
-				URLPrefix:    path.Dir(treeLink),
-				Metas:        ctx.Repo.Repository.ComposeDocumentMetas(),
-				GitRepo:      ctx.Repo.GitRepo,
+				Links: markup.Links{
+					Base:       ctx.Repo.RepoLink,
+					BranchPath: ctx.Repo.BranchNameSubURL(),
+					TreePath:   path.Dir(ctx.Repo.TreePath),
+				},
+				Metas:   ctx.Repo.Repository.ComposeDocumentMetas(),
+				GitRepo: ctx.Repo.GitRepo,
 			}, rd)
 			if err != nil {
 				ctx.ServerError("Render", err)
@@ -936,14 +948,6 @@ func renderCode(ctx *context.Context) {
 	}
 	ctx.Data["Title"] = title
 
-	branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
-	treeLink := branchLink
-	rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL()
-
-	if len(ctx.Repo.TreePath) > 0 {
-		treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
-	}
-
 	// Get Topics of this repo
 	renderRepoTopics(ctx)
 	if ctx.Written() {
@@ -968,9 +972,9 @@ func renderCode(ctx *context.Context) {
 	}
 
 	if entry.IsDir() {
-		renderDirectory(ctx, treeLink)
+		renderDirectory(ctx)
 	} else {
-		renderFile(ctx, entry, treeLink, rawLink)
+		renderFile(ctx, entry)
 	}
 	if ctx.Written() {
 		return
@@ -1011,6 +1015,12 @@ func renderCode(ctx *context.Context) {
 	}
 
 	ctx.Data["Paths"] = paths
+
+	branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
+	treeLink := branchLink
+	if len(ctx.Repo.TreePath) > 0 {
+		treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
+	}
 	ctx.Data["TreeLink"] = treeLink
 	ctx.Data["TreeNames"] = treeNames
 	ctx.Data["BranchLink"] = branchLink
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index c9cec0313d..5634445b70 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -238,10 +238,12 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
 	}
 
 	rctx := &markup.RenderContext{
-		Ctx:       ctx,
-		URLPrefix: ctx.Repo.RepoLink,
-		Metas:     ctx.Repo.Repository.ComposeDocumentMetas(),
-		IsWiki:    true,
+		Ctx:   ctx,
+		Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
+		Links: markup.Links{
+			Base: ctx.Repo.RepoLink,
+		},
+		IsWiki: true,
 	}
 	buf := &strings.Builder{}
 
diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
index 2934f8206e..2e4a8b2a4c 100644
--- a/routers/web/shared/user/header.go
+++ b/routers/web/shared/user/header.go
@@ -44,10 +44,8 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
 
 	if len(ctx.ContextUser.Description) != 0 {
 		content, err := markdown.RenderString(&markup.RenderContext{
-			URLPrefix: ctx.Repo.RepoLink,
-			Metas:     map[string]string{"mode": "document"},
-			GitRepo:   ctx.Repo.GitRepo,
-			Ctx:       ctx,
+			Metas: map[string]string{"mode": "document"},
+			Ctx:   ctx,
 		}, ctx.ContextUser.Description)
 		if err != nil {
 			ctx.ServerError("RenderString", err)
@@ -84,7 +82,7 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
 	}
 }
 
-func FindUserProfileReadme(ctx *context.Context) (profileGitRepo *git.Repository, profileReadmeBlob *git.Blob, profileClose func()) {
+func FindUserProfileReadme(ctx *context.Context) (profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadmeBlob *git.Blob, profileClose func()) {
 	profileDbRepo, err := repo_model.GetRepositoryByName(ctx.ContextUser.ID, ".profile")
 	if err == nil && !profileDbRepo.IsEmpty && !profileDbRepo.IsPrivate {
 		if profileGitRepo, err = git.OpenRepository(ctx, profileDbRepo.RepoPath()); err != nil {
@@ -97,7 +95,7 @@ func FindUserProfileReadme(ctx *context.Context) (profileGitRepo *git.Repository
 			}
 		}
 	}
-	return profileGitRepo, profileReadmeBlob, func() {
+	return profileDbRepo, profileGitRepo, profileReadmeBlob, func() {
 		if profileGitRepo != nil {
 			_ = profileGitRepo.Close()
 		}
@@ -107,7 +105,7 @@ func FindUserProfileReadme(ctx *context.Context) (profileGitRepo *git.Repository
 func RenderUserHeader(ctx *context.Context) {
 	prepareContextForCommonProfile(ctx)
 
-	_, profileReadmeBlob, profileClose := FindUserProfileReadme(ctx)
+	_, _, profileReadmeBlob, profileClose := FindUserProfileReadme(ctx)
 	defer profileClose()
 	ctx.Data["HasProfileReadme"] = profileReadmeBlob != nil
 }
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index d0d24d7ed1..285c18c1f9 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -246,9 +246,11 @@ func Milestones(ctx *context.Context) {
 		}
 
 		milestones[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{
-			URLPrefix: milestones[i].Repo.Link(),
-			Metas:     milestones[i].Repo.ComposeMetas(),
-			Ctx:       ctx,
+			Links: markup.Links{
+				Base: milestones[i].Repo.Link(),
+			},
+			Metas: milestones[i].Repo.ComposeMetas(),
+			Ctx:   ctx,
 		}, milestones[i].Content)
 		if err != nil {
 			ctx.ServerError("RenderString", err)
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 48a4b94c19..04bbd196fc 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -7,6 +7,7 @@ package user
 import (
 	"fmt"
 	"net/http"
+	"path"
 	"strings"
 
 	activities_model "code.gitea.io/gitea/models/activities"
@@ -64,17 +65,17 @@ func userProfile(ctx *context.Context) {
 		ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
 	}
 
-	profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx)
+	profileDbRepo, profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx)
 	defer profileClose()
 
 	showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
-	prepareUserProfileTabData(ctx, showPrivate, profileGitRepo, profileReadmeBlob)
+	prepareUserProfileTabData(ctx, showPrivate, profileDbRepo, profileGitRepo, profileReadmeBlob)
 	// call PrepareContextForProfileBigAvatar later to avoid re-querying the NumFollowers & NumFollowing
 	shared_user.PrepareContextForProfileBigAvatar(ctx)
 	ctx.HTML(http.StatusOK, tplProfile)
 }
 
-func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileGitRepo *git.Repository, profileReadme *git.Blob) {
+func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDbRepo *repo_model.Repository, profileGitRepo *git.Repository, profileReadme *git.Blob) {
 	// if there is a profile readme, default to "overview" page, otherwise, default to "repositories" page
 	// if there is not a profile readme, the overview tab should be treated as the repositories tab
 	tab := ctx.FormString("tab")
@@ -236,7 +237,16 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileGi
 			if profileContent, err := markdown.RenderString(&markup.RenderContext{
 				Ctx:     ctx,
 				GitRepo: profileGitRepo,
-				Metas:   map[string]string{"mode": "document"},
+				Links: markup.Links{
+					// Give the repo link to the markdown render for the full link of media element.
+					// the media link usually be like /[user]/[repoName]/media/branch/[branchName],
+					// 	Eg. /Tom/.profile/media/branch/main
+					// The branch shown on the profile page is the default branch, this need to be in sync with doc, see:
+					//	https://docs.gitea.com/usage/profile-readme
+					Base:       profileDbRepo.Link(),
+					BranchPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
+				},
+				Metas: map[string]string{"mode": "document"},
 			}, bytes); err != nil {
 				log.Error("failed to RenderString: %v", err)
 			} else {
diff --git a/services/mailer/mail.go b/services/mailer/mail.go
index 83f58bad0d..08b52687d1 100644
--- a/services/mailer/mail.go
+++ b/services/mailer/mail.go
@@ -220,9 +220,11 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
 
 	// This is the body of the new issue or comment, not the mail body
 	body, err := markdown.RenderString(&markup.RenderContext{
-		Ctx:       ctx,
-		URLPrefix: ctx.Issue.Repo.HTMLURL(),
-		Metas:     ctx.Issue.Repo.ComposeMetas(),
+		Ctx: ctx,
+		Links: markup.Links{
+			Base: ctx.Issue.Repo.HTMLURL(),
+		},
+		Metas: ctx.Issue.Repo.ComposeMetas(),
 	}, ctx.Content)
 	if err != nil {
 		return nil, err
diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go
index 9c49c89e8b..983e55a436 100644
--- a/services/mailer/mail_release.go
+++ b/services/mailer/mail_release.go
@@ -57,9 +57,11 @@ func mailNewRelease(ctx context.Context, lang string, tos []string, rel *repo_mo
 
 	var err error
 	rel.RenderedNote, err = markdown.RenderString(&markup.RenderContext{
-		Ctx:       ctx,
-		URLPrefix: rel.Repo.Link(),
-		Metas:     rel.Repo.ComposeMetas(),
+		Ctx: ctx,
+		Links: markup.Links{
+			Base: rel.Repo.HTMLURL(),
+		},
+		Metas: rel.Repo.ComposeMetas(),
 	}, rel.Note)
 	if err != nil {
 		log.Error("markdown.RenderString(%d): %v", rel.RepoID, err)
diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl
index 370a412efb..e8f55c18a3 100644
--- a/templates/repo/branch/list.tmpl
+++ b/templates/repo/branch/list.tmpl
@@ -25,7 +25,7 @@
 									<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DefaultBranchBranch.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
 									{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}}
 								</div>
-								<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.DBBranch.CommitMessage .RepoLink .Repository.ComposeMetas}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DefaultBranchBranch.DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
+								<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DefaultBranchBranch.DBBranch.CommitMessage .Repository.ComposeMetas}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DefaultBranchBranch.DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DefaultBranchBranch.DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
 							</td>
 							<td class="right aligned middle aligned overflow-visible">
 								{{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}}
@@ -101,7 +101,7 @@
 										<button class="btn interact-fg gt-px-2" data-clipboard-text="{{.DBBranch.Name}}">{{svg "octicon-copy" 14}}</button>
 										{{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}}
 									</div>
-									<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage $.RepoLink $.Repository.ComposeMetas}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
+									<p class="info gt-df gt-ac gt-my-2">{{svg "octicon-git-commit" 16 "gt-mr-2"}}<a href="{{$.RepoLink}}/commit/{{PathEscape .DBBranch.CommitID}}">{{ShortSha .DBBranch.CommitID}}</a> · <span class="commit-message">{{RenderCommitMessage $.Context .DBBranch.CommitMessage $.Repository.ComposeMetas}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{TimeSince .DBBranch.CommitTime.AsTime ctx.Locale}}{{if .DBBranch.Pusher}} &nbsp;{{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}} &nbsp;{{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}</p>
 								{{end}}
 								</td>
 								<td class="two wide ui">
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index 7de4c2c8fc..17a71e7a75 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -19,7 +19,7 @@
 		{{end}}
 		<div class="ui top attached header clearing segment gt-relative commit-header {{$class}}">
 			<div class="gt-df gt-mb-4 gt-fw">
-				<h3 class="gt-mb-0 gt-f1"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message $.RepoLink $.Repository.ComposeMetas}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses "root" $}}</h3>
+				<h3 class="gt-mb-0 gt-f1"><span class="commit-summary" title="{{.Commit.Summary}}">{{RenderCommitMessage $.Context .Commit.Message $.Repository.ComposeMetas}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses "root" $}}</h3>
 				{{if not $.PageIsWiki}}
 					<div>
 						<a class="ui primary tiny button" href="{{.SourcePath}}">
@@ -135,7 +135,7 @@
 				{{end}}
 			</div>
 			{{if IsMultilineCommitMessage .Commit.Message}}
-				<pre class="commit-body">{{RenderCommitBody $.Context .Commit.Message $.RepoLink $.Repository.ComposeMetas}}</pre>
+				<pre class="commit-body">{{RenderCommitBody $.Context .Commit.Message $.Repository.ComposeMetas}}</pre>
 			{{end}}
 			{{template "repo/commit_load_branches_and_tags" .}}
 		</div>
@@ -258,7 +258,7 @@
 				</div>
 			</div>
 		{{end}}
-		{{if .Note}}
+		{{if .NoteRendered}}
 			<div class="ui top attached header segment git-notes">
 				{{svg "octicon-note" 16 "gt-mr-3"}}
 				{{ctx.Locale.Tr "repo.diff.git-notes"}}:
@@ -276,7 +276,7 @@
 				<span class="text grey" id="note-authored-time">{{TimeSince .NoteCommit.Author.When ctx.Locale}}</span>
 			</div>
 			<div class="ui bottom attached info segment git-notes">
-				<pre class="commit-body">{{RenderNote $.Context .Note $.RepoLink $.Repository.ComposeMetas}}</pre>
+				<pre class="commit-body">{{.NoteRendered | Str2html}}</pre>
 			</div>
 		{{end}}
 		{{template "repo/diff/box" .}}
diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl
index 9fb2788a45..9c0e38a2ad 100644
--- a/templates/repo/commits_list.tmpl
+++ b/templates/repo/commits_list.tmpl
@@ -60,7 +60,7 @@
 								<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{.Summary | RenderEmoji $.Context}}</span>
 							{{else}}
 								{{$commitLink:= printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String)}}
-								<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.Context .Message $commitRepoLink $commitLink $.Repository.ComposeMetas}}</span>
+								<span class="commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.Context .Message $commitLink $.Repository.ComposeMetas}}</span>
 							{{end}}
 							</span>
 							{{if IsMultilineCommitMessage .Message}}
@@ -68,7 +68,7 @@
 							{{end}}
 							{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses "root" $}}
 							{{if IsMultilineCommitMessage .Message}}
-							<pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .Message $commitRepoLink $.Repository.ComposeMetas}}</pre>
+							<pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .Message $.Repository.ComposeMetas}}</pre>
 							{{end}}
 						</td>
 						{{if .Committer}}
diff --git a/templates/repo/commits_list_small.tmpl b/templates/repo/commits_list_small.tmpl
index 645bd73bb8..f6d26195d3 100644
--- a/templates/repo/commits_list_small.tmpl
+++ b/templates/repo/commits_list_small.tmpl
@@ -38,12 +38,12 @@
 			</a>
 		</span>
 
-		<span class="gt-mono commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.Link|Escape) $commitLink $.comment.Issue.PullRequest.BaseRepo.ComposeMetas}}</span>
+		<span class="gt-mono commit-summary {{if gt .ParentCount 1}} grey text{{end}}" title="{{.Summary}}">{{RenderCommitMessageLinkSubject $.root.Context .Message $commitLink $.comment.Issue.PullRequest.BaseRepo.ComposeMetas}}</span>
 		{{if IsMultilineCommitMessage .Message}}
 			<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
 		{{end}}
 		{{if IsMultilineCommitMessage .Message}}
-			<pre class="commit-body gt-hidden">{{RenderCommitBody $.root.Context .Message ($.comment.Issue.PullRequest.BaseRepo.Link|Escape) $.comment.Issue.PullRequest.BaseRepo.ComposeMetas}}</pre>
+			<pre class="commit-body gt-hidden">{{RenderCommitBody $.root.Context .Message $.comment.Issue.PullRequest.BaseRepo.ComposeMetas}}</pre>
 		{{end}}
 	</div>
 {{end}}
diff --git a/templates/repo/diff/compare.tmpl b/templates/repo/diff/compare.tmpl
index 2892ea51b1..3e5df17f22 100644
--- a/templates/repo/diff/compare.tmpl
+++ b/templates/repo/diff/compare.tmpl
@@ -194,7 +194,7 @@
 				<div class="twelve wide column issue-title">
 					{{ctx.Locale.Tr "repo.pulls.has_pull_request" (print (Escape $.RepoLink) "/pulls/" .PullRequest.Issue.Index) (Escape $.RepoRelPath) .PullRequest.Index | Safe}}
 					<h1>
-						<span id="issue-title">{{RenderIssueTitle $.Context .PullRequest.Issue.Title $.RepoLink $.Repository.ComposeMetas}}</span>
+						<span id="issue-title">{{RenderIssueTitle $.Context .PullRequest.Issue.Title $.Repository.ComposeMetas}}</span>
 						<span class="index">#{{.PullRequest.Issue.Index}}</span>
 					</h1>
 				</div>
diff --git a/templates/repo/graph/commits.tmpl b/templates/repo/graph/commits.tmpl
index b8817f5c88..cc8086f8dc 100644
--- a/templates/repo/graph/commits.tmpl
+++ b/templates/repo/graph/commits.tmpl
@@ -29,7 +29,7 @@
 						</a>
 					</span>
 					<span class="message gt-dib gt-ellipsis gt-mr-3">
-						<span>{{RenderCommitMessage $.Context $commit.Subject $.RepoLink $.Repository.ComposeMetas}}</span>
+						<span>{{RenderCommitMessage $.Context $commit.Subject $.Repository.ComposeMetas}}</span>
 					</span>
 					<span class="commit-refs gt-df gt-ac gt-mr-2">
 						{{range $commit.Refs}}
diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index c18825f204..af99286cc1 100644
--- a/templates/repo/issue/view_title.tmpl
+++ b/templates/repo/issue/view_title.tmpl
@@ -6,7 +6,7 @@
 <div class="issue-title-header">
 	<div class="issue-title" id="issue-title-wrapper">
 		<h1 class="gt-word-break">
-			<span id="issue-title">{{RenderIssueTitle $.Context .Issue.Title $.RepoLink $.Repository.ComposeMetas | RenderCodeBlock}} <span class="index">#{{.Issue.Index}}</span>
+			<span id="issue-title">{{RenderIssueTitle $.Context .Issue.Title $.Repository.ComposeMetas | RenderCodeBlock}} <span class="index">#{{.Issue.Index}}</span>
 </span>
 			<div id="edit-title-input" class="ui input gt-f1 gt-hidden">
 				<input value="{{.Issue.Title}}" maxlength="255" autocomplete="off">
diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl
index bfd59839f5..0a83cdfc5b 100644
--- a/templates/repo/view_list.tmpl
+++ b/templates/repo/view_list.tmpl
@@ -26,10 +26,10 @@
 					</a>
 					{{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses "root" $}}
 					{{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}}
-					<span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{RenderCommitMessageLinkSubject $.Context .LatestCommit.Message $.RepoLink $commitLink $.Repository.ComposeMetas}}</span>
+					<span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{RenderCommitMessageLinkSubject $.Context .LatestCommit.Message $commitLink $.Repository.ComposeMetas}}</span>
 						{{if IsMultilineCommitMessage .LatestCommit.Message}}
 							<button class="ui button js-toggle-commit-body ellipsis-button" aria-expanded="false">...</button>
-							<pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .LatestCommit.Message $.RepoLink $.Repository.ComposeMetas}}</pre>
+							<pre class="commit-body gt-hidden">{{RenderCommitBody $.Context .LatestCommit.Message $.Repository.ComposeMetas}}</pre>
 						{{end}}
 					</span>
 				{{end}}
@@ -83,7 +83,7 @@
 					<span class="truncate">
 						{{if $commit}}
 							{{$commitLink := printf "%s/commit/%s" $.RepoLink (PathEscape $commit.ID.String)}}
-							{{RenderCommitMessageLinkSubject $.Context $commit.Message $.RepoLink $commitLink $.Repository.ComposeMetas}}
+							{{RenderCommitMessageLinkSubject $.Context $commit.Message $commitLink $.Repository.ComposeMetas}}
 						{{else}}
 							<div class="ui active tiny slow centered inline">…</div>
 						{{end}}
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl
index 974b59b7c3..a1ac3d8848 100644
--- a/templates/user/dashboard/feeds.tmpl
+++ b/templates/user/dashboard/feeds.tmpl
@@ -90,7 +90,7 @@
 								<img class="ui avatar" src="{{$push.AvatarLink $.Context .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16">
 								<a class="ui sha label" href="{{$commitLink}}">{{ShortSha .Sha1}}</a>
 								<span class="text truncate">
-									{{RenderCommitMessage $.Context .Message $repoLink $.ComposeMetas}}
+									{{RenderCommitMessage $.Context .Message $.ComposeMetas}}
 								</span>
 							</div>
 						{{end}}
diff --git a/tests/fuzz/fuzz_test.go b/tests/fuzz/fuzz_test.go
index 6a7d9d2d32..25a6ed8213 100644
--- a/tests/fuzz/fuzz_test.go
+++ b/tests/fuzz/fuzz_test.go
@@ -15,8 +15,10 @@ import (
 )
 
 var renderContext = markup.RenderContext{
-	Ctx:       context.Background(),
-	URLPrefix: "https://example.com/go-gitea/gitea",
+	Ctx: context.Background(),
+	Links: markup.Links{
+		Base: "https://example.com/go-gitea/gitea",
+	},
 	Metas: map[string]string{
 		"user": "go-gitea",
 		"repo": "gitea",