diff --git a/models/renderhelper/repo_comment.go b/models/renderhelper/repo_comment.go
index 6bd5e91ad1..eab85bf6d4 100644
--- a/models/renderhelper/repo_comment.go
+++ b/models/renderhelper/repo_comment.go
@@ -28,14 +28,14 @@ func (r *RepoComment) IsCommitIDExisting(commitID string) bool {
 	return r.commitChecker.IsCommitIDExisting(commitID)
 }
 
-func (r *RepoComment) ResolveLink(link string, likeType markup.LinkType) (finalLink string) {
-	switch likeType {
-	case markup.LinkTypeApp:
-		finalLink = r.ctx.ResolveLinkApp(link)
+func (r *RepoComment) ResolveLink(link, preferLinkType string) string {
+	linkType, link := markup.ParseRenderedLink(link, preferLinkType)
+	switch linkType {
+	case markup.LinkTypeRoot:
+		return r.ctx.ResolveLinkRoot(link)
 	default:
-		finalLink = r.ctx.ResolveLinkRelative(r.repoLink, r.opts.CurrentRefPath, link)
+		return r.ctx.ResolveLinkRelative(r.repoLink, r.opts.CurrentRefPath, link)
 	}
-	return finalLink
 }
 
 var _ markup.RenderHelper = (*RepoComment)(nil)
diff --git a/models/renderhelper/repo_file.go b/models/renderhelper/repo_file.go
index 794828c617..5bf754bf20 100644
--- a/models/renderhelper/repo_file.go
+++ b/models/renderhelper/repo_file.go
@@ -29,17 +29,17 @@ func (r *RepoFile) IsCommitIDExisting(commitID string) bool {
 	return r.commitChecker.IsCommitIDExisting(commitID)
 }
 
-func (r *RepoFile) ResolveLink(link string, likeType markup.LinkType) string {
-	finalLink := link
-	switch likeType {
-	case markup.LinkTypeApp:
-		finalLink = r.ctx.ResolveLinkApp(link)
-	case markup.LinkTypeDefault:
-		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
+func (r *RepoFile) ResolveLink(link, preferLinkType string) (finalLink string) {
+	linkType, link := markup.ParseRenderedLink(link, preferLinkType)
+	switch linkType {
+	case markup.LinkTypeRoot:
+		finalLink = r.ctx.ResolveLinkRoot(link)
 	case markup.LinkTypeRaw:
 		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "raw", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
 	case markup.LinkTypeMedia:
 		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "media", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
+	default:
+		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "src", r.opts.CurrentRefPath), r.opts.CurrentTreePath, link)
 	}
 	return finalLink
 }
diff --git a/models/renderhelper/repo_file_test.go b/models/renderhelper/repo_file_test.go
index 29cb45f6f7..3b48efba3a 100644
--- a/models/renderhelper/repo_file_test.go
+++ b/models/renderhelper/repo_file_test.go
@@ -48,8 +48,8 @@ func TestRepoFile(t *testing.T) {
 		assert.Equal(t,
 			`<p><a href="/user2/repo1/src/branch/main/test" rel="nofollow">/test</a>
 <a href="/user2/repo1/src/branch/main/test" rel="nofollow">./test</a>
-<a href="/user2/repo1/media/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="/image"/></a>
-<a href="/user2/repo1/media/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="./image"/></a></p>
+<a href="/user2/repo1/src/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="/image"/></a>
+<a href="/user2/repo1/src/branch/main/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/branch/main/image" alt="./image"/></a></p>
 `, rendered)
 	})
 
@@ -62,7 +62,7 @@ func TestRepoFile(t *testing.T) {
 `)
 		assert.NoError(t, err)
 		assert.Equal(t, `<p><a href="/user2/repo1/src/commit/1234/test" rel="nofollow">/test</a>
-<a href="/user2/repo1/media/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/image" alt="/image"/></a></p>
+<a href="/user2/repo1/src/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/image" alt="/image"/></a></p>
 `, rendered)
 	})
 
@@ -77,7 +77,7 @@ func TestRepoFile(t *testing.T) {
 <video src="LINK">
 `)
 		assert.NoError(t, err)
-		assert.Equal(t, `<a href="/user2/repo1/media/commit/1234/my-dir/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/my-dir/LINK"/></a>
+		assert.Equal(t, `<a href="/user2/repo1/src/commit/1234/my-dir/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/media/commit/1234/my-dir/LINK"/></a>
 <video src="/user2/repo1/media/commit/1234/my-dir/LINK">
 </video>`, rendered)
 	})
@@ -100,7 +100,7 @@ func TestRepoFileOrgMode(t *testing.T) {
 		assert.NoError(t, err)
 		assert.Equal(t, `<p>
 <a href="https://google.com/" rel="nofollow">https://google.com/</a>
-<a href="/user2/repo1/media/commit/1234/my-dir/ImageLink.svg" rel="nofollow">The Image Desc</a></p>
+<a href="/user2/repo1/src/commit/1234/my-dir/ImageLink.svg" rel="nofollow">The Image Desc</a></p>
 `, rendered)
 	})
 
diff --git a/models/renderhelper/repo_wiki.go b/models/renderhelper/repo_wiki.go
index aa456bf6ce..1e3e07295c 100644
--- a/models/renderhelper/repo_wiki.go
+++ b/models/renderhelper/repo_wiki.go
@@ -30,18 +30,16 @@ func (r *RepoWiki) IsCommitIDExisting(commitID string) bool {
 	return r.commitChecker.IsCommitIDExisting(commitID)
 }
 
-func (r *RepoWiki) ResolveLink(link string, likeType markup.LinkType) string {
-	finalLink := link
-	switch likeType {
-	case markup.LinkTypeApp:
-		finalLink = r.ctx.ResolveLinkApp(link)
-	case markup.LinkTypeDefault:
-		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki", r.opts.currentRefPath), r.opts.currentTreePath, link)
-	case markup.LinkTypeMedia:
+func (r *RepoWiki) ResolveLink(link, preferLinkType string) (finalLink string) {
+	linkType, link := markup.ParseRenderedLink(link, preferLinkType)
+	switch linkType {
+	case markup.LinkTypeRoot:
+		finalLink = r.ctx.ResolveLinkRoot(link)
+	case markup.LinkTypeMedia, markup.LinkTypeRaw:
 		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki/raw", r.opts.currentRefPath), r.opts.currentTreePath, link)
-	case markup.LinkTypeRaw: // wiki doesn't use it
+	default:
+		finalLink = r.ctx.ResolveLinkRelative(path.Join(r.repoLink, "wiki", r.opts.currentRefPath), r.opts.currentTreePath, link)
 	}
-
 	return finalLink
 }
 
diff --git a/models/renderhelper/repo_wiki_test.go b/models/renderhelper/repo_wiki_test.go
index b24508f1f2..4f6da541a5 100644
--- a/models/renderhelper/repo_wiki_test.go
+++ b/models/renderhelper/repo_wiki_test.go
@@ -45,8 +45,8 @@ func TestRepoWiki(t *testing.T) {
 		assert.Equal(t,
 			`<p><a href="/user2/repo1/wiki/test" rel="nofollow">/test</a>
 <a href="/user2/repo1/wiki/test" rel="nofollow">./test</a>
-<a href="/user2/repo1/wiki/raw/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="/image"/></a>
-<a href="/user2/repo1/wiki/raw/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="./image"/></a></p>
+<a href="/user2/repo1/wiki/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="/image"/></a>
+<a href="/user2/repo1/wiki/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/image" alt="./image"/></a></p>
 `, rendered)
 	})
 
@@ -57,7 +57,7 @@ func TestRepoWiki(t *testing.T) {
 <video src="LINK">
 `)
 		assert.NoError(t, err)
-		assert.Equal(t, `<a href="/user2/repo1/wiki/raw/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/LINK"/></a>
+		assert.Equal(t, `<a href="/user2/repo1/wiki/LINK" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/wiki/raw/LINK"/></a>
 <video src="/user2/repo1/wiki/raw/LINK">
 </video>`, rendered)
 	})
diff --git a/models/renderhelper/simple_document.go b/models/renderhelper/simple_document.go
index 91d888aa87..9b3dacaea3 100644
--- a/models/renderhelper/simple_document.go
+++ b/models/renderhelper/simple_document.go
@@ -15,8 +15,14 @@ type SimpleDocument struct {
 	baseLink string
 }
 
-func (r *SimpleDocument) ResolveLink(link string, likeType markup.LinkType) string {
-	return r.ctx.ResolveLinkRelative(r.baseLink, "", link)
+func (r *SimpleDocument) ResolveLink(link, preferLinkType string) string {
+	linkType, link := markup.ParseRenderedLink(link, preferLinkType)
+	switch linkType {
+	case markup.LinkTypeRoot:
+		return r.ctx.ResolveLinkRoot(link)
+	default:
+		return r.ctx.ResolveLinkRelative(r.baseLink, "", link)
+	}
 }
 
 var _ markup.RenderHelper = (*SimpleDocument)(nil)
diff --git a/models/renderhelper/simple_document_test.go b/models/renderhelper/simple_document_test.go
index 908e640f9c..890592860a 100644
--- a/models/renderhelper/simple_document_test.go
+++ b/models/renderhelper/simple_document_test.go
@@ -30,7 +30,7 @@ func TestSimpleDocument(t *testing.T) {
 	assert.Equal(t,
 		`<p>65f1bf27bc3bf70f64657658635e66094edbcb4d
 #1
-<a href="/base/user2" rel="nofollow">@user2</a></p>
+<a href="/user2" rel="nofollow">@user2</a></p>
 <p><a href="/base/test" rel="nofollow">/test</a>
 <a href="/base/test" rel="nofollow">./test</a>
 <a href="/base/image" target="_blank" rel="nofollow noopener"><img src="/base/image" alt="/image"/></a>
diff --git a/modules/markup/external/external.go b/modules/markup/external/external.go
index 03242e569e..f708457853 100644
--- a/modules/markup/external/external.go
+++ b/modules/markup/external/external.go
@@ -77,14 +77,14 @@ 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 (
-		command = strings.NewReplacer(
-			envMark("GITEA_PREFIX_SRC"), ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault),
-			envMark("GITEA_PREFIX_RAW"), ctx.RenderHelper.ResolveLink("", markup.LinkTypeRaw),
-		).Replace(p.Command)
-		commands = strings.Fields(command)
-		args     = commands[1:]
-	)
+	baseLinkSrc := ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault)
+	baseLinkRaw := ctx.RenderHelper.ResolveLink("", markup.LinkTypeRaw)
+	command := strings.NewReplacer(
+		envMark("GITEA_PREFIX_SRC"), baseLinkSrc,
+		envMark("GITEA_PREFIX_RAW"), baseLinkRaw,
+	).Replace(p.Command)
+	commands := strings.Fields(command)
+	args := commands[1:]
 
 	if p.IsInputFile {
 		// write to temp file
@@ -112,14 +112,14 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
 		args = append(args, f.Name())
 	}
 
-	processCtx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Render [%s] for %s", commands[0], ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault)))
+	processCtx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Render [%s] for %s", commands[0], baseLinkSrc))
 	defer finished()
 
 	cmd := exec.CommandContext(processCtx, commands[0], args...)
 	cmd.Env = append(
 		os.Environ(),
-		"GITEA_PREFIX_SRC="+ctx.RenderHelper.ResolveLink("", markup.LinkTypeDefault),
-		"GITEA_PREFIX_RAW="+ctx.RenderHelper.ResolveLink("", markup.LinkTypeRaw),
+		"GITEA_PREFIX_SRC="+baseLinkSrc,
+		"GITEA_PREFIX_RAW="+baseLinkRaw,
 	)
 	if !p.IsInputFile {
 		cmd.Stdin = input
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 3aaf669c63..05701eebde 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -32,7 +32,6 @@ type globalVarsType struct {
 	comparePattern          *regexp.Regexp
 	fullURLPattern          *regexp.Regexp
 	emailRegex              *regexp.Regexp
-	blackfridayExtRegex     *regexp.Regexp
 	emojiShortCodeRegex     *regexp.Regexp
 	issueFullPattern        *regexp.Regexp
 	filesChangedFullPattern *regexp.Regexp
@@ -74,9 +73,6 @@ var globalVars = sync.OnceValue(func() *globalVarsType {
 	//   https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail)
 	v.emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))")
 
-	// blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote
-	v.blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
-
 	// emojiShortCodeRegex find emoji by alias like :smile:
 	v.emojiShortCodeRegex = regexp.MustCompile(`:[-+\w]+:`)
 
@@ -94,17 +90,12 @@ var globalVars = sync.OnceValue(func() *globalVarsType {
 	return v
 })
 
-// IsFullURLBytes reports whether link fits valid format.
-func IsFullURLBytes(link []byte) bool {
-	return globalVars().fullURLPattern.Match(link)
-}
-
 func IsFullURLString(link string) bool {
 	return globalVars().fullURLPattern.MatchString(link)
 }
 
 func IsNonEmptyRelativePath(link string) bool {
-	return link != "" && !IsFullURLString(link) && link[0] != '/' && link[0] != '?' && link[0] != '#'
+	return link != "" && !IsFullURLString(link) && link[0] != '?' && link[0] != '#'
 }
 
 // CustomLinkURLSchemes allows for additional schemes to be detected when parsing links within text
@@ -316,44 +307,38 @@ func isEmojiNode(node *html.Node) bool {
 }
 
 func visitNode(ctx *RenderContext, procs []processor, node *html.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, "#")
-		notHasPrefix := !(strings.HasPrefix(val, "user-content-") || globalVars().blackfridayExtRegex.MatchString(val))
-
-		if attr.Key == "id" && notHasPrefix {
-			node.Attr[idx].Val = "user-content-" + attr.Val
-		}
-
-		if attr.Key == "href" && strings.HasPrefix(attr.Val, "#") && notHasPrefix {
-			node.Attr[idx].Val = "#user-content-" + val
-		}
-	}
-
-	switch node.Type {
-	case html.TextNode:
+	if node.Type == html.TextNode {
 		for _, proc := range procs {
 			proc(ctx, node) // it might add siblings
 		}
+		return node.NextSibling
+	}
+	if node.Type != html.ElementNode {
+		return node.NextSibling
+	}
 
-	case html.ElementNode:
-		if isEmojiNode(node) {
-			// TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span"
-			// if we don't stop it, it will go into the TextNode again and create an infinite recursion
-			return node.NextSibling
-		} else if node.Data == "code" || node.Data == "pre" {
-			return node.NextSibling // ignore code and pre nodes
-		} else if node.Data == "img" {
-			return visitNodeImg(ctx, node)
-		} else if node.Data == "video" {
-			return visitNodeVideo(ctx, node)
-		} else if node.Data == "a" {
-			procs = emojiProcessors // Restrict text in links to emojis
-		}
-		for n := node.FirstChild; n != nil; {
-			n = visitNode(ctx, procs, n)
-		}
-	default:
+	processNodeAttrID(node)
+
+	if isEmojiNode(node) {
+		// TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span"
+		// if we don't stop it, it will go into the TextNode again and create an infinite recursion
+		return node.NextSibling
+	} else if node.Data == "code" || node.Data == "pre" {
+		return node.NextSibling // ignore code and pre nodes
+	} else if node.Data == "img" {
+		return visitNodeImg(ctx, node)
+	} else if node.Data == "video" {
+		return visitNodeVideo(ctx, node)
+	}
+
+	if node.Data == "a" {
+		processNodeA(ctx, node)
+		// only use emoji processors for the content in the "A" tag,
+		// because the content there is not processable, for example: the content is a commit id or a full URL.
+		procs = emojiProcessors
+	}
+	for n := node.FirstChild; n != nil; {
+		n = visitNode(ctx, procs, n)
 	}
 	return node.NextSibling
 }
diff --git a/modules/markup/html_commit.go b/modules/markup/html_commit.go
index aa1b7d034a..967c327f36 100644
--- a/modules/markup/html_commit.go
+++ b/modules/markup/html_commit.go
@@ -43,7 +43,6 @@ func createCodeLink(href, content, class string) *html.Node {
 	code := &html.Node{
 		Type: html.ElementNode,
 		Data: atom.Code.String(),
-		Attr: []html.Attribute{{Key: "class", Val: "nohighlight"}},
 	}
 
 	code.AppendChild(text)
@@ -189,7 +188,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
 			continue
 		}
 
-		link := ctx.RenderHelper.ResolveLink(util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], "commit", hash), LinkTypeApp)
+		link := "/:root/" + util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], "commit", hash)
 		replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit"))
 		start = 0
 		node = node.NextSibling.NextSibling
@@ -205,9 +204,9 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
 			return
 		}
 
-		reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
-		linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp)
-		link := createLink(ctx, linkHref, reftext, "commit")
+		refText := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
+		linkHref := "/:root/" + util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha)
+		link := createLink(ctx, linkHref, refText, "commit")
 
 		replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
 		node = node.NextSibling.NextSibling
diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go
index e0655bf0a7..467cc509d0 100644
--- a/modules/markup/html_internal_test.go
+++ b/modules/markup/html_internal_test.go
@@ -107,7 +107,7 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
 		isExternal := false
 		if marker == "!" {
 			path = "pulls"
-			prefix = "http://localhost:3000/someUser/someRepo/pulls/"
+			prefix = "/someUser/someRepo/pulls/"
 		} else {
 			path = "issues"
 			prefix = "https://someurl.com/someUser/someRepo/"
@@ -116,7 +116,7 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
 
 		links := make([]any, len(indices))
 		for i, index := range indices {
-			links[i] = numericIssueLink(util.URLJoin(TestRepoURL, path), "ref-issue", index, marker)
+			links[i] = numericIssueLink(util.URLJoin("/test-owner/test-repo", path), "ref-issue", index, marker)
 		}
 		expectedNil := fmt.Sprintf(expectedFmt, links...)
 		testRenderIssueIndexPattern(t, s, expectedNil, NewTestRenderContext(TestAppURL, localMetas))
@@ -293,13 +293,13 @@ func TestRender_AutoLink(t *testing.T) {
 
 	// render valid commit URLs
 	tmp := util.URLJoin(TestRepoURL, "commit", "d8a994ef243349f321568f9e36d5c3f444b99cae")
-	test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24</code></a>")
+	test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24</code></a>")
 	tmp += "#diff-2"
-	test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24 (diff-2)</code></a>")
+	test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24 (diff-2)</code></a>")
 
 	// render other commit URLs
 	tmp = "https://external-link.gitea.io/go-gitea/gitea/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2"
-	test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code class=\"nohighlight\">d8a994ef24 (diff-2)</code></a>")
+	test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24 (diff-2)</code></a>")
 }
 
 func TestRender_FullIssueURLs(t *testing.T) {
diff --git a/modules/markup/html_issue.go b/modules/markup/html_issue.go
index 7a6f33011a..85bec5db20 100644
--- a/modules/markup/html_issue.go
+++ b/modules/markup/html_issue.go
@@ -82,7 +82,7 @@ func createIssueLinkContentWithSummary(ctx *RenderContext, linkHref string, ref
 	h, err := DefaultRenderHelperFuncs.RenderRepoIssueIconTitle(ctx, RenderIssueIconTitleOptions{
 		OwnerName:  ref.Owner,
 		RepoName:   ref.Name,
-		LinkHref:   linkHref,
+		LinkHref:   ctx.RenderHelper.ResolveLink(linkHref, LinkTypeDefault),
 		IssueIndex: issueIndex,
 	})
 	if err != nil {
@@ -162,7 +162,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
 			issueOwner := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["user"], ref.Owner)
 			issueRepo := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["repo"], ref.Name)
 			issuePath := util.Iif(ref.IsPull, "pulls", "issues")
-			linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(issueOwner, issueRepo, issuePath, ref.Issue), LinkTypeApp)
+			linkHref := "/:root/" + util.URLJoin(issueOwner, issueRepo, issuePath, ref.Issue)
 
 			// at the moment, only render the issue index in a full line (or simple line) as icon+title
 			// otherwise it would be too noisy for "take #1 as an example" in a sentence
diff --git a/modules/markup/html_issue_test.go b/modules/markup/html_issue_test.go
index 8d189fbdf6..c68429641f 100644
--- a/modules/markup/html_issue_test.go
+++ b/modules/markup/html_issue_test.go
@@ -39,7 +39,7 @@ func TestRender_IssueList(t *testing.T) {
 	t.Run("NormalIssueRef", func(t *testing.T) {
 		test(
 			"#12345",
-			`<p><a href="http://localhost:3000/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a></p>`,
+			`<p><a href="/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a></p>`,
 		)
 	})
 
@@ -56,7 +56,7 @@ func TestRender_IssueList(t *testing.T) {
 		test(
 			"* foo #12345 bar",
 			`<ul>
-<li>foo <a href="http://localhost:3000/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a> bar</li>
+<li>foo <a href="/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a> bar</li>
 </ul>`,
 		)
 	})
diff --git a/modules/markup/html_link.go b/modules/markup/html_link.go
index 0e7a988d36..1ea0b14028 100644
--- a/modules/markup/html_link.go
+++ b/modules/markup/html_link.go
@@ -125,7 +125,6 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
 			}
 		}
 		if image {
-			link = ctx.RenderHelper.ResolveLink(link, LinkTypeMedia)
 			title := props["title"]
 			if title == "" {
 				title = props["alt"]
@@ -151,7 +150,6 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
 				childNode.Attr = childNode.Attr[:2]
 			}
 		} else {
-			link = ctx.RenderHelper.ResolveLink(link, LinkTypeDefault)
 			childNode.Type = html.TextNode
 			childNode.Data = name
 		}
diff --git a/modules/markup/html_mention.go b/modules/markup/html_mention.go
index fffa12e7b7..f97c034cf3 100644
--- a/modules/markup/html_mention.go
+++ b/modules/markup/html_mention.go
@@ -33,7 +33,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
 		if ok && strings.Contains(mention, "/") {
 			mentionOrgAndTeam := strings.Split(mention, "/")
 			if mentionOrgAndTeam[0][1:] == ctx.RenderOptions.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") {
-				link := ctx.RenderHelper.ResolveLink(util.URLJoin("org", ctx.RenderOptions.Metas["org"], "teams", mentionOrgAndTeam[1]), LinkTypeApp)
+				link := "/:root/" + util.URLJoin("org", ctx.RenderOptions.Metas["org"], "teams", mentionOrgAndTeam[1])
 				replaceContent(node, loc.Start, loc.End, createLink(ctx, link, mention, "" /*mention*/))
 				node = node.NextSibling.NextSibling
 				start = 0
@@ -45,7 +45,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) {
 		mentionedUsername := mention[1:]
 
 		if DefaultRenderHelperFuncs != nil && DefaultRenderHelperFuncs.IsUsernameMentionable(ctx, mentionedUsername) {
-			link := ctx.RenderHelper.ResolveLink(mentionedUsername, LinkTypeApp)
+			link := "/:root/" + mentionedUsername
 			replaceContent(node, loc.Start, loc.End, createLink(ctx, link, mention, "" /*mention*/))
 			node = node.NextSibling.NextSibling
 			start = 0
diff --git a/modules/markup/html_node.go b/modules/markup/html_node.go
index 6e8ca67900..68858b024a 100644
--- a/modules/markup/html_node.go
+++ b/modules/markup/html_node.go
@@ -4,42 +4,79 @@
 package markup
 
 import (
+	"strings"
+
 	"golang.org/x/net/html"
 )
 
+func isAnchorIDUserContent(s string) bool {
+	// blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote
+	// old logic: blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
+	return strings.HasPrefix(s, "user-content-") || strings.Contains(s, ":user-content-")
+}
+
+func processNodeAttrID(node *html.Node) {
+	// Add user-content- to IDs and "#" links if they don't already have them,
+	// and convert the link href to a relative link to the host root
+	for idx, attr := range node.Attr {
+		if attr.Key == "id" {
+			if !isAnchorIDUserContent(attr.Val) {
+				node.Attr[idx].Val = "user-content-" + attr.Val
+			}
+		}
+	}
+}
+
+func processNodeA(ctx *RenderContext, node *html.Node) {
+	for idx, attr := range node.Attr {
+		if attr.Key == "href" {
+			if anchorID, ok := strings.CutPrefix(attr.Val, "#"); ok {
+				if !isAnchorIDUserContent(attr.Val) {
+					node.Attr[idx].Val = "#user-content-" + anchorID
+				}
+			} else {
+				node.Attr[idx].Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeDefault)
+			}
+		}
+	}
+}
+
 func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
 	next = img.NextSibling
-	for i, attr := range img.Attr {
-		if attr.Key != "src" {
+	for i, imgAttr := range img.Attr {
+		if imgAttr.Key != "src" {
 			continue
 		}
 
-		if IsNonEmptyRelativePath(attr.Val) {
-			attr.Val = ctx.RenderHelper.ResolveLink(attr.Val, LinkTypeMedia)
+		imgSrcOrigin := imgAttr.Val
+		isLinkable := imgSrcOrigin != "" && !strings.HasPrefix(imgSrcOrigin, "data:")
 
-			// By default, the "<img>" tag should also be clickable,
-			// because frontend use `<img>` to paste the re-scaled image into the markdown,
-			// so it must match the default markdown image behavior.
-			hasParentAnchor := false
-			for p := img.Parent; p != nil; p = p.Parent {
-				if hasParentAnchor = p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor {
-					break
-				}
-			}
-			if !hasParentAnchor {
-				imgA := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{
-					{Key: "href", Val: attr.Val},
-					{Key: "target", Val: "_blank"},
-				}}
-				parent := img.Parent
-				imgNext := img.NextSibling
-				parent.RemoveChild(img)
-				parent.InsertBefore(imgA, imgNext)
-				imgA.AppendChild(img)
+		// By default, the "<img>" tag should also be clickable,
+		// because frontend use `<img>` to paste the re-scaled image into the markdown,
+		// so it must match the default markdown image behavior.
+		cnt := 0
+		for p := img.Parent; isLinkable && p != nil && cnt < 2; p = p.Parent {
+			if hasParentAnchor := p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor {
+				isLinkable = false
+				break
 			}
+			cnt++
 		}
-		attr.Val = camoHandleLink(attr.Val)
-		img.Attr[i] = attr
+		if isLinkable {
+			wrapper := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{
+				{Key: "href", Val: ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeDefault)},
+				{Key: "target", Val: "_blank"},
+			}}
+			parent := img.Parent
+			imgNext := img.NextSibling
+			parent.RemoveChild(img)
+			parent.InsertBefore(wrapper, imgNext)
+			wrapper.AppendChild(img)
+		}
+
+		imgAttr.Val = ctx.RenderHelper.ResolveLink(imgSrcOrigin, LinkTypeMedia)
+		imgAttr.Val = camoHandleLink(imgAttr.Val)
+		img.Attr[i] = imgAttr
 	}
 	return next
 }
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index f0f062fa64..24dc7c9d3d 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -35,6 +35,7 @@ func TestRender_Commits(t *testing.T) {
 	sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
 	repo := markup.TestAppURL + testRepoOwnerName + "/" + testRepoName + "/"
 	commit := util.URLJoin(repo, "commit", sha)
+	commitPath := "/user13/repo11/commit/" + sha
 	tree := util.URLJoin(repo, "tree", sha, "src")
 
 	file := util.URLJoin(repo, "commit", sha, "example.txt")
@@ -44,9 +45,9 @@ func TestRender_Commits(t *testing.T) {
 	commitCompare := util.URLJoin(repo, "compare", sha+"..."+sha)
 	commitCompareWithHash := commitCompare + "#L2"
 
-	test(sha, `<p><a href="`+commit+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
-	test(sha[:7], `<p><a href="`+commit[:len(commit)-(40-7)]+`" rel="nofollow"><code>65f1bf2</code></a></p>`)
-	test(sha[:39], `<p><a href="`+commit[:len(commit)-(40-39)]+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
+	test(sha, `<p><a href="`+commitPath+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
+	test(sha[:7], `<p><a href="`+commitPath[:len(commitPath)-(40-7)]+`" rel="nofollow"><code>65f1bf2</code></a></p>`)
+	test(sha[:39], `<p><a href="`+commitPath[:len(commitPath)-(40-39)]+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
 	test(commit, `<p><a href="`+commit+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
 	test(tree, `<p><a href="`+tree+`" rel="nofollow"><code>65f1bf27bc/src</code></a></p>`)
 
@@ -57,13 +58,13 @@ func TestRender_Commits(t *testing.T) {
 	test(commitCompare, `<p><a href="`+commitCompare+`" rel="nofollow"><code>65f1bf27bc...65f1bf27bc</code></a></p>`)
 	test(commitCompareWithHash, `<p><a href="`+commitCompareWithHash+`" rel="nofollow"><code>65f1bf27bc...65f1bf27bc (L2)</code></a></p>`)
 
-	test("commit "+sha, `<p>commit <a href="`+commit+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
+	test("commit "+sha, `<p>commit <a href="`+commitPath+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
 	test("/home/gitea/"+sha, "<p>/home/gitea/"+sha+"</p>")
 	test("deadbeef", `<p>deadbeef</p>`)
 	test("d27ace93", `<p>d27ace93</p>`)
 	test(sha[:14]+".x", `<p>`+sha[:14]+`.x</p>`)
 
-	expected14 := `<a href="` + commit[:len(commit)-(40-14)] + `" rel="nofollow"><code>` + sha[:10] + `</code></a>`
+	expected14 := `<a href="` + commitPath[:len(commitPath)-(40-14)] + `" rel="nofollow"><code>` + sha[:10] + `</code></a>`
 	test(sha[:14]+".", `<p>`+expected14+`.</p>`)
 	test(sha[:14]+",", `<p>`+expected14+`,</p>`)
 	test("["+sha[:14]+"]", `<p>[`+expected14+`]</p>`)
@@ -80,10 +81,10 @@ func TestRender_CrossReferences(t *testing.T) {
 
 	test(
 		"test-owner/test-repo#12345",
-		`<p><a href="`+util.URLJoin(markup.TestAppURL, "test-owner", "test-repo", "issues", "12345")+`" class="ref-issue" rel="nofollow">test-owner/test-repo#12345</a></p>`)
+		`<p><a href="/test-owner/test-repo/issues/12345" class="ref-issue" rel="nofollow">test-owner/test-repo#12345</a></p>`)
 	test(
 		"go-gitea/gitea#12345",
-		`<p><a href="`+util.URLJoin(markup.TestAppURL, "go-gitea", "gitea", "issues", "12345")+`" class="ref-issue" rel="nofollow">go-gitea/gitea#12345</a></p>`)
+		`<p><a href="/go-gitea/gitea/issues/12345" class="ref-issue" rel="nofollow">go-gitea/gitea#12345</a></p>`)
 	test(
 		"/home/gitea/go-gitea/gitea#12345",
 		`<p>/home/gitea/go-gitea/gitea#12345</p>`)
@@ -487,7 +488,7 @@ func TestPostProcess_RenderDocument(t *testing.T) {
 	// But cross-referenced issue index should work.
 	test(
 		"go-gitea/gitea#12345",
-		`<a href="`+util.URLJoin(markup.TestAppURL, "go-gitea", "gitea", "issues", "12345")+`" class="ref-issue">go-gitea/gitea#12345</a>`)
+		`<a href="/go-gitea/gitea/issues/12345" class="ref-issue">go-gitea/gitea#12345</a>`)
 
 	// Test that other post processing still works.
 	test(
@@ -543,7 +544,7 @@ func TestIssue18471(t *testing.T) {
 	err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
 
 	assert.NoError(t, err)
-	assert.Equal(t, `<a href="http://domain/org/repo/compare/783b039...da951ce" class="compare"><code class="nohighlight">783b039...da951ce</code></a>`, res.String())
+	assert.Equal(t, `<a href="http://domain/org/repo/compare/783b039...da951ce" class="compare"><code>783b039...da951ce</code></a>`, res.String())
 }
 
 func TestIsFullURL(t *testing.T) {
diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go
index e178431fa8..c2b3389245 100644
--- a/modules/markup/markdown/goldmark.go
+++ b/modules/markup/markdown/goldmark.go
@@ -65,10 +65,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
 			g.transformHeading(ctx, v, reader, &tocList)
 		case *ast.Paragraph:
 			g.applyElementDir(v)
-		case *ast.Image:
-			g.transformImage(ctx, v)
-		case *ast.Link:
-			g.transformLink(ctx, v)
 		case *ast.List:
 			g.transformList(ctx, v, rc)
 		case *ast.Text:
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index 268a543835..2310895fc3 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -308,12 +308,12 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) {
 	testcase := `![image1](/image1)
 ![image2](/image2)
 `
-	expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1"></a>
-<a href="/image2" target="_blank" rel="nofollow noopener"><img src="/image2" alt="image2"></a></p>
+	expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1"/></a>
+<a href="/image2" target="_blank" rel="nofollow noopener"><img src="/image2" alt="image2"/></a></p>
 `
-	res, err := markdown.RenderRawString(markup.NewTestRenderContext(), testcase)
+	res, err := markdown.RenderString(markup.NewTestRenderContext(), testcase)
 	assert.NoError(t, err)
-	assert.Equal(t, expected, res)
+	assert.Equal(t, expected, string(res))
 }
 
 func TestRenderEmojiInLinks_Issue12331(t *testing.T) {
@@ -529,3 +529,16 @@ space</p>
 	assert.NoError(t, err)
 	assert.Equal(t, expected, string(result))
 }
+
+func TestMarkdownLink(t *testing.T) {
+	defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
+	input := `<a href=foo>link1</a>
+<a href='/foo'>link2</a>
+<a href="#foo">link3</a>`
+	result, err := markdown.RenderString(markup.NewTestRenderContext("/base", localMetas), input)
+	assert.NoError(t, err)
+	assert.Equal(t, `<p><a href="/base/foo" rel="nofollow">link1</a>
+<a href="/base/foo" rel="nofollow">link2</a>
+<a href="#user-content-foo" rel="nofollow">link3</a></p>
+`, string(result))
+}
diff --git a/modules/markup/markdown/transform_image.go b/modules/markup/markdown/transform_image.go
deleted file mode 100644
index 36512e59a8..0000000000
--- a/modules/markup/markdown/transform_image.go
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright 2024 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package markdown
-
-import (
-	"code.gitea.io/gitea/modules/markup"
-
-	"github.com/yuin/goldmark/ast"
-)
-
-func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image) {
-	// Images need two things:
-	//
-	// 1. Their src needs to munged to be a real value
-	// 2. If they're not wrapped with a link they need a link wrapper
-
-	// Check if the destination is a real link
-	if len(v.Destination) > 0 && !markup.IsFullURLBytes(v.Destination) {
-		v.Destination = []byte(ctx.RenderHelper.ResolveLink(string(v.Destination), markup.LinkTypeMedia))
-	}
-
-	parent := v.Parent()
-	// Create a link around image only if parent is not already a link
-	if _, ok := parent.(*ast.Link); !ok && parent != nil {
-		next := v.NextSibling()
-
-		// Create a link wrapper
-		wrap := ast.NewLink()
-		wrap.Destination = v.Destination
-		wrap.Title = v.Title
-		wrap.SetAttributeString("target", []byte("_blank"))
-
-		// Duplicate the current image node
-		image := ast.NewImage(ast.NewLink())
-		image.Destination = v.Destination
-		image.Title = v.Title
-		for _, attr := range v.Attributes() {
-			image.SetAttribute(attr.Name, attr.Value)
-		}
-		for child := v.FirstChild(); child != nil; {
-			next := child.NextSibling()
-			image.AppendChild(image, child)
-			child = next
-		}
-
-		// Append our duplicate image to the wrapper link
-		wrap.AppendChild(wrap, image)
-
-		// Wire in the next sibling
-		wrap.SetNextSibling(next)
-
-		// Replace the current node with the wrapper link
-		parent.ReplaceChild(parent, v, wrap)
-
-		// But most importantly ensure the next sibling is still on the old image too
-		v.SetNextSibling(next)
-	}
-}
diff --git a/modules/markup/markdown/transform_link.go b/modules/markup/markdown/transform_link.go
deleted file mode 100644
index 51c2c915d8..0000000000
--- a/modules/markup/markdown/transform_link.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2024 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package markdown
-
-import (
-	"code.gitea.io/gitea/modules/markup"
-
-	"github.com/yuin/goldmark/ast"
-)
-
-func resolveLink(ctx *markup.RenderContext, link, userContentAnchorPrefix string) (result string, resolved bool) {
-	isAnchorFragment := link != "" && link[0] == '#'
-	if !isAnchorFragment && !markup.IsFullURLString(link) {
-		link, resolved = ctx.RenderHelper.ResolveLink(link, markup.LinkTypeDefault), true
-	}
-	if isAnchorFragment && userContentAnchorPrefix != "" {
-		link, resolved = userContentAnchorPrefix+link[1:], true
-	}
-	return link, resolved
-}
-
-func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link) {
-	if link, resolved := resolveLink(ctx, string(v.Destination), "#user-content-"); resolved {
-		v.Destination = []byte(link)
-	}
-}
diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go
index 70d02c1321..93c335d244 100644
--- a/modules/markup/orgmode/orgmode.go
+++ b/modules/markup/orgmode/orgmode.go
@@ -1,7 +1,7 @@
 // Copyright 2017 The Gitea Authors. All rights reserved.
 // SPDX-License-Identifier: MIT
 
-package markup
+package orgmode
 
 import (
 	"fmt"
@@ -125,27 +125,13 @@ type orgWriter struct {
 
 var _ org.Writer = (*orgWriter)(nil)
 
-func (r *orgWriter) resolveLink(kind, link string) string {
-	link = strings.TrimPrefix(link, "file:")
-	if !strings.HasPrefix(link, "#") && // not a URL fragment
-		!markup.IsFullURLString(link) {
-		if kind == "regular" {
-			// orgmode reports the link kind as "regular" for "[[ImageLink.svg][The Image Desc]]"
-			// so we need to try to guess the link kind again here
-			kind = org.RegularLink{URL: link}.Kind()
-		}
-		if kind == "image" || kind == "video" {
-			link = r.rctx.RenderHelper.ResolveLink(link, markup.LinkTypeMedia)
-		} else {
-			link = r.rctx.RenderHelper.ResolveLink(link, markup.LinkTypeDefault)
-		}
-	}
-	return link
+func (r *orgWriter) resolveLink(link string) string {
+	return strings.TrimPrefix(link, "file:")
 }
 
 // WriteRegularLink renders images, links or videos
 func (r *orgWriter) WriteRegularLink(l org.RegularLink) {
-	link := r.resolveLink(l.Kind(), l.URL)
+	link := r.resolveLink(l.URL)
 
 	printHTML := func(html template.HTML, a ...any) {
 		_, _ = fmt.Fprint(r, htmlutil.HTMLFormat(html, a...))
@@ -156,14 +142,14 @@ func (r *orgWriter) WriteRegularLink(l org.RegularLink) {
 		if l.Description == nil {
 			printHTML(`<img src="%s" alt="%s">`, link, link)
 		} else {
-			imageSrc := r.resolveLink(l.Kind(), org.String(l.Description...))
+			imageSrc := r.resolveLink(org.String(l.Description...))
 			printHTML(`<a href="%s"><img src="%s" alt="%s"></a>`, link, imageSrc, imageSrc)
 		}
 	case "video":
 		if l.Description == nil {
 			printHTML(`<video src="%s">%s</video>`, link, link)
 		} else {
-			videoSrc := r.resolveLink(l.Kind(), org.String(l.Description...))
+			videoSrc := r.resolveLink(org.String(l.Description...))
 			printHTML(`<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc)
 		}
 	default:
diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go
index de39bafebe..df4bb38ad1 100644
--- a/modules/markup/orgmode/orgmode_test.go
+++ b/modules/markup/orgmode/orgmode_test.go
@@ -1,7 +1,7 @@
 // Copyright 2017 The Gitea Authors. All rights reserved.
 // SPDX-License-Identifier: MIT
 
-package markup
+package orgmode_test
 
 import (
 	"os"
@@ -9,6 +9,7 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/modules/markup"
+	"code.gitea.io/gitea/modules/markup/orgmode"
 	"code.gitea.io/gitea/modules/setting"
 
 	"github.com/stretchr/testify/assert"
@@ -22,7 +23,7 @@ func TestMain(m *testing.M) {
 
 func TestRender_StandardLinks(t *testing.T) {
 	test := func(input, expected string) {
-		buffer, err := RenderString(markup.NewTestRenderContext("/relative-path/media/branch/main/"), input)
+		buffer, err := orgmode.RenderString(markup.NewTestRenderContext(), input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
 	}
@@ -30,37 +31,37 @@ func TestRender_StandardLinks(t *testing.T) {
 	test("[[https://google.com/]]",
 		`<p><a href="https://google.com/">https://google.com/</a></p>`)
 	test("[[ImageLink.svg][The Image Desc]]",
-		`<p><a href="/relative-path/media/branch/main/ImageLink.svg">The Image Desc</a></p>`)
+		`<p><a href="ImageLink.svg">The Image Desc</a></p>`)
 }
 
 func TestRender_InternalLinks(t *testing.T) {
 	test := func(input, expected string) {
-		buffer, err := RenderString(markup.NewTestRenderContext("/relative-path/src/branch/main"), input)
+		buffer, err := orgmode.RenderString(markup.NewTestRenderContext(), input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
 	}
 
 	test("[[file:test.org][Test]]",
-		`<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`)
+		`<p><a href="test.org">Test</a></p>`)
 	test("[[./test.org][Test]]",
-		`<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`)
+		`<p><a href="./test.org">Test</a></p>`)
 	test("[[test.org][Test]]",
-		`<p><a href="/relative-path/src/branch/main/test.org">Test</a></p>`)
+		`<p><a href="test.org">Test</a></p>`)
 	test("[[path/to/test.org][Test]]",
-		`<p><a href="/relative-path/src/branch/main/path/to/test.org">Test</a></p>`)
+		`<p><a href="path/to/test.org">Test</a></p>`)
 }
 
 func TestRender_Media(t *testing.T) {
 	test := func(input, expected string) {
-		buffer, err := RenderString(markup.NewTestRenderContext("./relative-path"), input)
+		buffer, err := orgmode.RenderString(markup.NewTestRenderContext(), input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
 	}
 
 	test("[[file:../../.images/src/02/train.jpg]]",
-		`<p><img src=".images/src/02/train.jpg" alt=".images/src/02/train.jpg"></p>`)
+		`<p><img src="../../.images/src/02/train.jpg" alt="../../.images/src/02/train.jpg"></p>`)
 	test("[[file:train.jpg]]",
-		`<p><img src="relative-path/train.jpg" alt="relative-path/train.jpg"></p>`)
+		`<p><img src="train.jpg" alt="train.jpg"></p>`)
 
 	// With description.
 	test("[[https://example.com][https://example.com/example.svg]]",
@@ -91,7 +92,7 @@ func TestRender_Media(t *testing.T) {
 
 func TestRender_Source(t *testing.T) {
 	test := func(input, expected string) {
-		buffer, err := RenderString(markup.NewTestRenderContext(), input)
+		buffer, err := orgmode.RenderString(markup.NewTestRenderContext(), input)
 		assert.NoError(t, err)
 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
 	}
diff --git a/modules/markup/render.go b/modules/markup/render.go
index 37a2a86687..eb621b30a7 100644
--- a/modules/markup/render.go
+++ b/modules/markup/render.go
@@ -261,8 +261,14 @@ func (r *TestRenderHelper) IsCommitIDExisting(commitID string) bool {
 	return strings.HasPrefix(commitID, "65f1bf2") //|| strings.HasPrefix(commitID, "88fc37a")
 }
 
-func (r *TestRenderHelper) ResolveLink(link string, likeType LinkType) string {
-	return r.ctx.ResolveLinkRelative(r.BaseLink, "", link)
+func (r *TestRenderHelper) ResolveLink(link, preferLinkType string) string {
+	linkType, link := ParseRenderedLink(link, preferLinkType)
+	switch linkType {
+	case LinkTypeRoot:
+		return r.ctx.ResolveLinkRoot(link)
+	default:
+		return r.ctx.ResolveLinkRelative(r.BaseLink, "", link)
+	}
 }
 
 var _ RenderHelper = (*TestRenderHelper)(nil)
diff --git a/modules/markup/render_helper.go b/modules/markup/render_helper.go
index 8ff0e7d6fb..b16f1189c5 100644
--- a/modules/markup/render_helper.go
+++ b/modules/markup/render_helper.go
@@ -10,13 +10,11 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 )
 
-type LinkType string
-
 const (
-	LinkTypeApp     LinkType = "app"     // the link is relative to the AppSubURL
-	LinkTypeDefault LinkType = "default" // the link is relative to the default base (eg: repo link, or current ref tree path)
-	LinkTypeMedia   LinkType = "media"   // the link should be used to access media files (images, videos)
-	LinkTypeRaw     LinkType = "raw"     // not really useful, mainly for environment GITEA_PREFIX_RAW for external renders
+	LinkTypeDefault = ""
+	LinkTypeRoot    = "/:root"  // the link is relative to the AppSubURL(ROOT_URL)
+	LinkTypeMedia   = "/:media" // the link should be used to access media files (images, videos)
+	LinkTypeRaw     = "/:raw"   // not really useful, mainly for environment GITEA_PREFIX_RAW for external renders
 )
 
 type RenderHelper interface {
@@ -27,7 +25,7 @@ type RenderHelper interface {
 	// but not make processors to guess "is it rendering a comment or a wiki?" or "does it need to check commit ID?"
 
 	IsCommitIDExisting(commitID string) bool
-	ResolveLink(link string, likeType LinkType) string
+	ResolveLink(link, preferLinkType string) string
 }
 
 // RenderHelperFuncs is used to decouple cycle-import
@@ -51,7 +49,8 @@ func (r *SimpleRenderHelper) IsCommitIDExisting(commitID string) bool {
 	return false
 }
 
-func (r *SimpleRenderHelper) ResolveLink(link string, likeType LinkType) string {
+func (r *SimpleRenderHelper) ResolveLink(link, preferLinkType string) string {
+	_, link = ParseRenderedLink(link, preferLinkType)
 	return resolveLinkRelative(context.Background(), setting.AppSubURL+"/", "", link, false)
 }
 
diff --git a/modules/markup/render_link.go b/modules/markup/render_link.go
index b2e0699681..046544ce81 100644
--- a/modules/markup/render_link.go
+++ b/modules/markup/render_link.go
@@ -33,10 +33,24 @@ func resolveLinkRelative(ctx context.Context, base, cur, link string, absolute b
 	return finalLink
 }
 
-func (ctx *RenderContext) ResolveLinkRelative(base, cur, link string) (finalLink string) {
+func (ctx *RenderContext) ResolveLinkRelative(base, cur, link string) string {
+	if strings.HasPrefix(link, "/:") {
+		setting.PanicInDevOrTesting("invalid link %q, forgot to cut?", link)
+	}
 	return resolveLinkRelative(ctx, base, cur, link, ctx.RenderOptions.UseAbsoluteLink)
 }
 
-func (ctx *RenderContext) ResolveLinkApp(link string) string {
+func (ctx *RenderContext) ResolveLinkRoot(link string) string {
 	return ctx.ResolveLinkRelative(setting.AppSubURL+"/", "", link)
 }
+
+func ParseRenderedLink(s, preferLinkType string) (linkType, link string) {
+	if strings.HasPrefix(s, "/:") {
+		p := strings.IndexByte(s[1:], '/')
+		if p == -1 {
+			return s, ""
+		}
+		return s[:p+1], s[p+2:]
+	}
+	return preferLinkType, s
+}
diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go
index 9cdd3663b5..7b53fdb1e2 100644
--- a/modules/templates/util_render_test.go
+++ b/modules/templates/util_render_test.go
@@ -123,9 +123,9 @@ func TestRenderCommitBody(t *testing.T) {
 ![remote image](<a href="https://example.com/image.jpg">https://example.com/image.jpg</a>)
 [[local image|image.jpg]]
 [[remote link|<a href="https://example.com/image.jpg">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>
+<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" class="compare"><code>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>
+<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" class="commit"><code>88fc37a3c0</code></a>
 com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
 <span class="emoji" aria-label="thumbs up">👍</span>
 <a href="mailto:mail@domain.com">mail@domain.com</a>
diff --git a/routers/api/v1/misc/markup_test.go b/routers/api/v1/misc/markup_test.go
index 6063e54cdc..38a1a3be9e 100644
--- a/routers/api/v1/misc/markup_test.go
+++ b/routers/api/v1/misc/markup_test.go
@@ -134,7 +134,7 @@ Here are some links to the most important topics. You can find the full list of
 <h2 id="user-content-quick-links">Quick Links</h2>
 <p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
 <p><a href="http://localhost:3000/user2/repo1/wiki/Configuration" rel="nofollow">Configuration</a>
-<a href="http://localhost:3000/user2/repo1/wiki/raw/images/icon-bug.png" rel="nofollow"><img src="http://localhost:3000/user2/repo1/wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
+<a href="http://localhost:3000/user2/repo1/wiki/images/icon-bug.png" rel="nofollow"><img src="http://localhost:3000/user2/repo1/wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
 `,
 	}
 
@@ -158,19 +158,19 @@ Here are some links to the most important topics. You can find the full list of
 
 	input := "[Link](test.md)\n![Image](image.png)"
 	testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a>
-<a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
+<a href="http://localhost:3000/user2/repo1/src/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
 `, http.StatusOK)
 
 	testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a>
-<a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
+<a href="http://localhost:3000/user2/repo1/src/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
 `, http.StatusOK)
 
 	testRenderMarkup(t, "gfm", false, "", input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/test.md" rel="nofollow">Link</a>
-<a href="http://localhost:3000/user2/repo1/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
+<a href="http://localhost:3000/user2/repo1/src/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/image.png" alt="Image"/></a></p>
 `, http.StatusOK)
 
 	testRenderMarkup(t, "file", false, "path/new-file.md", input, `<p><a href="http://localhost:3000/user2/repo1/src/branch/main/path/test.md" rel="nofollow">Link</a>
-<a href="http://localhost:3000/user2/repo1/media/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/path/image.png" alt="Image"/></a></p>
+<a href="http://localhost:3000/user2/repo1/src/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/user2/repo1/media/branch/main/path/image.png" alt="Image"/></a></p>
 `, http.StatusOK)
 
 	testRenderMarkup(t, "file", false, "path/test.unknown", "## Test", "unsupported file to render: \"path/test.unknown\"\n", http.StatusUnprocessableEntity)