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",