diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 539715b3f9..fbada5472c 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1238,6 +1238,7 @@ file_view_rendered = View Rendered
 file_view_raw = View Raw
 file_permalink = Permalink
 file_too_large = The file is too large to be shown.
+file_is_empty = The file is empty.
 code_preview_line_from_to = Lines %[1]d to %[2]d in %[3]s
 code_preview_line_in = Line %[1]d in %[2]s
 invisible_runes_header = `This file contains invisible Unicode characters`
diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index 1887e4d95d..3e76ea6df4 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -99,8 +99,6 @@ func RefBlame(ctx *context.Context) {
 	}
 
 	ctx.Data["NumLines"], err = blob.GetBlobLineCount()
-	ctx.Data["NumLinesSet"] = true
-
 	if err != nil {
 		ctx.NotFound("GetBlobLineCount", err)
 		return
diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go
index 6dddade066..2891556d6f 100644
--- a/routers/web/repo/setting/lfs.go
+++ b/routers/web/repo/setting/lfs.go
@@ -303,6 +303,7 @@ func LFSFileGet(ctx *context.Context) {
 		rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
 
 		// Building code view blocks with line number on server side.
+		// FIXME: the logic is not right here: it first calls EscapeControlReader then calls HTMLEscapeString: double-escaping
 		escapedContent := &bytes.Buffer{}
 		ctx.Data["EscapeStatus"], _ = charset.EscapeControlReader(rd, escapedContent, ctx.Locale)
 
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 386ef7be5c..0aa3fe1efd 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -286,6 +286,7 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr
 
 	ctx.Data["FileIsText"] = fInfo.isTextFile
 	ctx.Data["FileName"] = path.Join(subfolder, readmeFile.Name())
+	ctx.Data["FileSize"] = fInfo.fileSize
 	ctx.Data["IsLFSFile"] = fInfo.isLFSFile
 
 	if fInfo.isLFSFile {
@@ -301,7 +302,6 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr
 		// Pretend that this is a normal text file to display 'This file is too large to be shown'
 		ctx.Data["IsFileTooLarge"] = true
 		ctx.Data["IsTextFile"] = true
-		ctx.Data["FileSize"] = fInfo.fileSize
 		return
 	}
 
@@ -552,7 +552,6 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
 			} else {
 				ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1
 			}
-			ctx.Data["NumLinesSet"] = true
 
 			language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
 			if err != nil {
@@ -606,8 +605,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
 			break
 		}
 
-		// TODO: this logic seems strange, it duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
-		// maybe for this case, the file is a binary file, and shouldn't be rendered?
+		// TODO: this logic duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
+		// It is used by "external renders", markupRender will execute external programs to get rendered content.
 		if markupType := markup.Type(blob.Name()); markupType != "" {
 			rd := io.MultiReader(bytes.NewReader(buf), dataRc)
 			ctx.Data["IsMarkup"] = true
diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl
index 4ad3ed85c9..3e7cd92066 100644
--- a/templates/repo/blame.tmpl
+++ b/templates/repo/blame.tmpl
@@ -32,6 +32,8 @@
 		<div class="file-view code-view unicode-escaped">
 			{{if .IsFileTooLarge}}
 				{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
+			{{else if not .FileSize}}
+				{{template "shared/fileisempty"}}
 			{{else}}
 			<table>
 				<tbody>
diff --git a/templates/repo/file_info.tmpl b/templates/repo/file_info.tmpl
index 823cf1b7d8..b63af68973 100644
--- a/templates/repo/file_info.tmpl
+++ b/templates/repo/file_info.tmpl
@@ -4,12 +4,12 @@
 			{{ctx.Locale.Tr "repo.symbolic_link"}}
 		</div>
 	{{end}}
-	{{if .NumLinesSet}}{{/* Explicit attribute needed to show 0 line changes */}}
+	{{if ne .NumLines nil}}
 		<div class="file-info-entry">
 			{{.NumLines}} {{ctx.Locale.TrN .NumLines "repo.line" "repo.lines"}}
 		</div>
 	{{end}}
-	{{if .FileSize}}
+	{{if ne .FileSize nil}}
 		<div class="file-info-entry">
 			{{FileSize .FileSize}}{{if .IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}}
 		</div>
diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl
index a015cc8bd1..f6fac05b69 100644
--- a/templates/repo/settings/lfs_file.tmpl
+++ b/templates/repo/settings/lfs_file.tmpl
@@ -16,10 +16,8 @@
 				<div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextFile}} code-view{{end}}">
 					{{if .IsFileTooLarge}}
 						{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
-					{{else if .IsMarkup}}
-						{{if .FileContent}}{{.FileContent | SafeHTML}}{{end}}
-					{{else if .IsPlainText}}
-						<pre>{{if .FileContent}}{{.FileContent | SafeHTML}}{{end}}</pre>
+					{{else if not .FileSize}}
+						{{template "shared/fileisempty"}}
 					{{else if not .IsTextFile}}
 						<div class="view-raw">
 							{{if .IsImageFile}}
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl
index 0a34b6c325..0ec400cfe9 100644
--- a/templates/repo/view_file.tmpl
+++ b/templates/repo/view_file.tmpl
@@ -91,6 +91,8 @@
 		<div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextSource}} code-view{{end}}">
 			{{if .IsFileTooLarge}}
 				{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
+			{{else if not .FileSize}}
+				{{template "shared/fileisempty"}}
 			{{else if .IsMarkup}}
 				{{if .FileContent}}{{.FileContent}}{{end}}
 			{{else if .IsPlainText}}
diff --git a/templates/shared/fileisempty.tmpl b/templates/shared/fileisempty.tmpl
new file mode 100644
index 0000000000..a92bcbcdbc
--- /dev/null
+++ b/templates/shared/fileisempty.tmpl
@@ -0,0 +1,3 @@
+<div class="file-not-rendered-prompt">
+	{{ctx.Locale.Tr "repo.file_is_empty"}}
+</div>
diff --git a/templates/shared/filetoolarge.tmpl b/templates/shared/filetoolarge.tmpl
index 8842fb1b91..cb23864ec8 100644
--- a/templates/shared/filetoolarge.tmpl
+++ b/templates/shared/filetoolarge.tmpl
@@ -1,4 +1,4 @@
-<div class="tw-p-4">
+<div class="file-not-rendered-prompt">
 	{{ctx.Locale.Tr "repo.file_too_large"}}
 	{{if .RawFileLink}}<a href="{{.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>{{end}}
 </div>
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 0e3d06650e..357a4ee195 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -1706,6 +1706,18 @@ td .commit-summary {
 .file-view.markup {
   padding: 1em 2em;
 }
+
+.file-view.markup:has(.file-not-rendered-prompt) {
+  padding: 0; /* let the file-not-rendered-prompt layout itself */
+}
+
+.file-not-rendered-prompt {
+  padding: 1rem;
+  text-align: center;
+  font-size: 1rem !important; /* use consistent styles for various containers (code, markup, etc) */
+  line-height: var(--line-height-default) !important; /* same as above */
+}
+
 .repository .activity-header {
   display: flex;
   justify-content: space-between;