diff --git a/.dockerignore b/.dockerignore
index d1a08977a5..98ef522331 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -62,7 +62,6 @@ cpu.out
 /data
 /indexers
 /log
-/public/img/avatar
 /tests/integration/gitea-integration-*
 /tests/integration/indexers-*
 /tests/e2e/gitea-e2e-*
@@ -77,6 +76,7 @@ cpu.out
 /public/assets/js
 /public/assets/css
 /public/assets/fonts
+/public/assets/img/avatar
 /public/assets/img/webpack
 /vendor
 /web_src/fomantic/node_modules
diff --git a/.eslintrc.yaml b/.eslintrc.yaml
index b62b13cefe..b65fe56cf2 100644
--- a/.eslintrc.yaml
+++ b/.eslintrc.yaml
@@ -42,10 +42,6 @@ overrides:
       worker: true
     rules:
       no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, status, statusbar, stop, toolbar, top]
-  - files: ["build/generate-images.js"]
-    rules:
-      i/no-unresolved: [0]
-      i/no-extraneous-dependencies: [0]
   - files: ["*.config.*"]
     rules:
       i/no-unused-modules: [0]
diff --git a/.gitignore b/.gitignore
index 34c71b6973..b883e079d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -64,7 +64,7 @@ cpu.out
 /data
 /indexers
 /log
-/public/img/avatar
+/public/assets/img/avatar
 /tests/integration/gitea-integration-*
 /tests/integration/indexers-*
 /tests/e2e/gitea-e2e-*
diff --git a/Makefile b/Makefile
index 6735ffd1a5..4cf8837f72 100644
--- a/Makefile
+++ b/Makefile
@@ -148,6 +148,8 @@ TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(FOMAN
 GO_DIRS := build cmd models modules routers services tests
 WEB_DIRS := web_src/js web_src/css
 
+ESLINT_FILES := web_src/js tools *.config.js tests/e2e
+STYLELINT_FILES := web_src/css web_src/js/components/*.vue
 SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) docs/content templates options/locale/locale_en-US.ini .github
 
 GO_SOURCES := $(wildcard *.go)
@@ -396,19 +398,19 @@ lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig
 
 .PHONY: lint-js
 lint-js: node_modules
-	npx eslint --color --max-warnings=0 --ext js,vue web_src/js build *.config.js tests/e2e
+	npx eslint --color --max-warnings=0 --ext js,vue $(ESLINT_FILES)
 
 .PHONY: lint-js-fix
 lint-js-fix: node_modules
-	npx eslint --color --max-warnings=0 --ext js,vue web_src/js build *.config.js tests/e2e --fix
+	npx eslint --color --max-warnings=0 --ext js,vue $(ESLINT_FILES) --fix
 
 .PHONY: lint-css
 lint-css: node_modules
-	npx stylelint --color --max-warnings=0 web_src/css web_src/js/components/*.vue
+	npx stylelint --color --max-warnings=0 $(STYLELINT_FILES)
 
 .PHONY: lint-css-fix
 lint-css-fix: node_modules
-	npx stylelint --color --max-warnings=0 web_src/css web_src/js/components/*.vue --fix
+	npx stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix
 
 .PHONY: lint-swagger
 lint-swagger: node_modules
@@ -468,7 +470,7 @@ lint-yaml: .venv
 
 .PHONY: watch
 watch:
-	@bash build/watch.sh
+	@bash tools/watch.sh
 
 .PHONY: watch-frontend
 watch-frontend: node-check node_modules
@@ -962,7 +964,7 @@ $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json
 .PHONY: svg
 svg: node-check | node_modules
 	rm -rf $(SVG_DEST_DIR)
-	node build/generate-svg.js
+	node tools/generate-svg.js
 
 .PHONY: svg-check
 svg-check: svg
@@ -997,7 +999,7 @@ generate-gitignore:
 .PHONY: generate-images
 generate-images: | node_modules
 	npm install --no-save fabric@6.0.0-beta19 imagemin-zopfli@7
-	node build/generate-images.js $(TAGS)
+	node tools/generate-images.js $(TAGS)
 
 .PHONY: generate-manpage
 generate-manpage:
diff --git a/docs/content/administration/cmd-embedded.zh-cn.md b/docs/content/administration/cmd-embedded.zh-cn.md
index 4570bb58a3..a2df1aa2f5 100644
--- a/docs/content/administration/cmd-embedded.zh-cn.md
+++ b/docs/content/administration/cmd-embedded.zh-cn.md
@@ -37,7 +37,7 @@ gitea embedded list [--include-vendored] [patterns...]
 
 - 列出所有模板文件,无论在哪个虚拟目录下:`**.tmpl`
 - 列出所有邮件模板文件:`templates/mail/**.tmpl`
-- 列出 `public/img` 目录下的所有文件:`public/img/**`
+列出 `public/assets/img` 目录下的所有文件:`public/assets/img/**`
 
 不要忘记为模式使用引号,因为空格、`*` 和其他字符可能对命令行解释器有特殊含义。
 
@@ -49,8 +49,8 @@ gitea embedded list [--include-vendored] [patterns...]
 
 ```sh
 $ gitea embedded list '**openid**'
-public/img/auth/openid_connect.svg
-public/img/openid-16x16.png
+public/assets/img/auth/openid_connect.svg
+public/assets/img/openid-16x16.png
 templates/user/auth/finalize_openid.tmpl
 templates/user/auth/signin_openid.tmpl
 templates/user/auth/signup_openid_connect.tmpl
diff --git a/docs/content/development/hacking-on-gitea.en-us.md b/docs/content/development/hacking-on-gitea.en-us.md
index 982dbcf6ea..004e803827 100644
--- a/docs/content/development/hacking-on-gitea.en-us.md
+++ b/docs/content/development/hacking-on-gitea.en-us.md
@@ -214,7 +214,7 @@ REPO_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200
 
 ### Building and adding SVGs
 
-SVG icons are built using the `make svg` target which compiles the icon sources defined in `build/generate-svg.js` into the output directory `public/assets/img/svg`. Custom icons can be added in the `web_src/svg` directory.
+SVG icons are built using the `make svg` target which compiles the icon sources into the output directory `public/assets/img/svg`. Custom icons can be added in the `web_src/svg` directory.
 
 ### Building the Logo
 
diff --git a/docs/content/development/hacking-on-gitea.zh-cn.md b/docs/content/development/hacking-on-gitea.zh-cn.md
index a31e1dc511..7dfea30538 100644
--- a/docs/content/development/hacking-on-gitea.zh-cn.md
+++ b/docs/content/development/hacking-on-gitea.zh-cn.md
@@ -201,7 +201,7 @@ REPO_INDEXER_CONN_STR = http://elastic:changeme@localhost:9200
 
 ### 构建和添加 SVGs
 
-SVG 图标是使用 `make svg` 目标构建的,该目标将 `build/generate-svg.js` 中定义的图标源编译到输出目录 `public/img/svg` 中。可以在 `web_src/svg` 目录中添加自定义图标。
+SVG 图标是使用 `make svg` 命令构建的,该命令将图标资源编译到输出目录 `public/assets/img/svg` 中。可以在 `web_src/svg` 目录中添加自定义图标。
 
 ### 构建 Logo
 
diff --git a/build/generate-images.js b/tools/generate-images.js
similarity index 95%
rename from build/generate-images.js
rename to tools/generate-images.js
index db31d19e2a..cc2855c18e 100755
--- a/build/generate-images.js
+++ b/tools/generate-images.js
@@ -1,7 +1,7 @@
 #!/usr/bin/env node
-import imageminZopfli from 'imagemin-zopfli';
+import imageminZopfli from 'imagemin-zopfli'; // eslint-disable-line i/no-unresolved
+import {loadSVGFromString, Canvas, Rect, util} from 'fabric/node'; // eslint-disable-line i/no-unresolved
 import {optimize} from 'svgo';
-import {loadSVGFromString, Canvas, Rect, util} from 'fabric/node';
 import {readFile, writeFile} from 'node:fs/promises';
 import {argv, exit} from 'node:process';
 
diff --git a/build/generate-svg.js b/tools/generate-svg.js
similarity index 100%
rename from build/generate-svg.js
rename to tools/generate-svg.js
diff --git a/build/watch.sh b/tools/watch.sh
similarity index 100%
rename from build/watch.sh
rename to tools/watch.sh