diff --git a/.golangci.yml b/.golangci.yml
index 0d7f90e263c40..c3dd47ec29da6 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -70,9 +70,6 @@ issues:
     - path: modules/log/
       linters:
         - errcheck
-    - path: routers/routes/web.go
-      linters:
-        - dupl
     - path: routers/api/v1/repo/issue_subscription.go
       linters:
         - dupl
@@ -114,3 +111,4 @@ issues:
       linters:
         - staticcheck
       text: "svc.IsAnInteractiveSession is deprecated: Use IsWindowsService instead."
+
diff --git a/cmd/web.go b/cmd/web.go
index 9c7d493339f04..0ba14ae70642c 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -17,7 +17,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/routers"
-	"code.gitea.io/gitea/routers/routes"
+	"code.gitea.io/gitea/routers/install"
 
 	context2 "github.com/gorilla/context"
 	"github.com/urfave/cli"
@@ -88,7 +88,7 @@ func runWeb(ctx *cli.Context) error {
 	}
 
 	// Perform pre-initialization
-	needsInstall := routers.PreInstallInit(graceful.GetManager().HammerContext())
+	needsInstall := install.PreloadSettings(graceful.GetManager().HammerContext())
 	if needsInstall {
 		// Flag for port number in case first time run conflict
 		if ctx.IsSet("port") {
@@ -101,7 +101,7 @@ func runWeb(ctx *cli.Context) error {
 				return err
 			}
 		}
-		c := routes.InstallRoutes()
+		c := install.Routes()
 		err := listen(c, false)
 		select {
 		case <-graceful.GetManager().IsShutdown():
@@ -134,7 +134,7 @@ func runWeb(ctx *cli.Context) error {
 	}
 
 	// Set up Chi routes
-	c := routes.NormalRoutes()
+	c := routers.NormalRoutes()
 	err := listen(c, true)
 	<-graceful.GetManager().Done()
 	log.Info("PID: %d Gitea Web Finished", os.Getpid())
diff --git a/contrib/pr/checkout.go b/contrib/pr/checkout.go
index 9ee692fd35b1a..9ce84f762cecd 100644
--- a/contrib/pr/checkout.go
+++ b/contrib/pr/checkout.go
@@ -31,7 +31,6 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/routers"
-	"code.gitea.io/gitea/routers/routes"
 
 	"github.com/go-git/go-git/v5"
 	"github.com/go-git/go-git/v5/config"
@@ -116,7 +115,7 @@ func runPR() {
 	//routers.GlobalInit()
 	external.RegisterRenderers()
 	markup.Init()
-	c := routes.NormalRoutes()
+	c := routers.NormalRoutes()
 
 	log.Printf("[PR] Ready for testing !\n")
 	log.Printf("[PR] Login with user1, user2, user3, ... with pass: password\n")
diff --git a/integrations/create_no_session_test.go b/integrations/create_no_session_test.go
index c864b9c7ae125..46f111b6f7d84 100644
--- a/integrations/create_no_session_test.go
+++ b/integrations/create_no_session_test.go
@@ -14,7 +14,7 @@ import (
 
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
-	"code.gitea.io/gitea/routers/routes"
+	"code.gitea.io/gitea/routers"
 
 	"gitea.com/go-chi/session"
 	jsoniter "github.com/json-iterator/go"
@@ -58,7 +58,7 @@ func TestSessionFileCreation(t *testing.T) {
 	oldSessionConfig := setting.SessionConfig.ProviderConfig
 	defer func() {
 		setting.SessionConfig.ProviderConfig = oldSessionConfig
-		c = routes.NormalRoutes()
+		c = routers.NormalRoutes()
 	}()
 
 	var config session.Options
@@ -84,7 +84,7 @@ func TestSessionFileCreation(t *testing.T) {
 
 	setting.SessionConfig.ProviderConfig = string(newConfigBytes)
 
-	c = routes.NormalRoutes()
+	c = routers.NormalRoutes()
 
 	t.Run("NoSessionOnViewIssue", func(t *testing.T) {
 		defer PrintCurrentTest(t)()
diff --git a/integrations/integration_test.go b/integrations/integration_test.go
index 74227416c4be5..d755977d1ab16 100644
--- a/integrations/integration_test.go
+++ b/integrations/integration_test.go
@@ -34,7 +34,6 @@ import (
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers"
-	"code.gitea.io/gitea/routers/routes"
 
 	"github.com/PuerkitoBio/goquery"
 	jsoniter "github.com/json-iterator/go"
@@ -88,7 +87,7 @@ func TestMain(m *testing.M) {
 	defer cancel()
 
 	initIntegrationTest()
-	c = routes.NormalRoutes()
+	c = routers.NormalRoutes()
 
 	// integration test settings...
 	if setting.Cfg != nil {
diff --git a/integrations/lfs_getobject_test.go b/integrations/lfs_getobject_test.go
index b7423a2dbe55f..337a93567a49f 100644
--- a/integrations/lfs_getobject_test.go
+++ b/integrations/lfs_getobject_test.go
@@ -15,7 +15,7 @@ import (
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/routers/routes"
+	"code.gitea.io/gitea/routers/web"
 
 	jsoniter "github.com/json-iterator/go"
 	gzipp "github.com/klauspost/compress/gzip"
@@ -99,7 +99,7 @@ func TestGetLFSLarge(t *testing.T) {
 		t.Skip()
 		return
 	}
-	content := make([]byte, routes.GzipMinSize*10)
+	content := make([]byte, web.GzipMinSize*10)
 	for i := range content {
 		content[i] = byte(i % 256)
 	}
@@ -115,7 +115,7 @@ func TestGetLFSGzip(t *testing.T) {
 		t.Skip()
 		return
 	}
-	b := make([]byte, routes.GzipMinSize*10)
+	b := make([]byte, web.GzipMinSize*10)
 	for i := range b {
 		b[i] = byte(i % 256)
 	}
@@ -136,7 +136,7 @@ func TestGetLFSZip(t *testing.T) {
 		t.Skip()
 		return
 	}
-	b := make([]byte, routes.GzipMinSize*10)
+	b := make([]byte, web.GzipMinSize*10)
 	for i := range b {
 		b[i] = byte(i % 256)
 	}
diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go
index 37e02874b4ffd..39a60df33f01b 100644
--- a/routers/api/v1/repo/file.go
+++ b/routers/api/v1/repo/file.go
@@ -17,7 +17,7 @@ import (
 	"code.gitea.io/gitea/modules/repofiles"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
-	"code.gitea.io/gitea/routers/repo"
+	"code.gitea.io/gitea/routers/common"
 )
 
 // GetRawFile get a file by path on a repository
@@ -83,7 +83,7 @@ func GetRawFile(ctx *context.APIContext) {
 		}
 		return
 	}
-	if err = repo.ServeBlob(ctx.Context, blob); err != nil {
+	if err = common.ServeBlob(ctx.Context, blob); err != nil {
 		ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
 	}
 }
@@ -126,7 +126,7 @@ func GetArchive(ctx *context.APIContext) {
 	ctx.Repo.GitRepo = gitRepo
 	defer gitRepo.Close()
 
-	repo.Download(ctx.Context)
+	common.Download(ctx.Context)
 }
 
 // GetEditorconfig get editor config of a repository
diff --git a/routers/common/db.go b/routers/common/db.go
new file mode 100644
index 0000000000000..069a46f64fe4d
--- /dev/null
+++ b/routers/common/db.go
@@ -0,0 +1,39 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package common
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/models/migrations"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+)
+
+// InitDBEngine In case of problems connecting to DB, retry connection. Eg, PGSQL in Docker Container on Synology
+func InitDBEngine(ctx context.Context) (err error) {
+	log.Info("Beginning ORM engine initialization.")
+	for i := 0; i < setting.Database.DBConnectRetries; i++ {
+		select {
+		case <-ctx.Done():
+			return fmt.Errorf("Aborted due to shutdown:\nin retry ORM engine initialization")
+		default:
+		}
+		log.Info("ORM engine initialization attempt #%d/%d...", i+1, setting.Database.DBConnectRetries)
+		if err = models.NewEngine(ctx, migrations.Migrate); err == nil {
+			break
+		} else if i == setting.Database.DBConnectRetries-1 {
+			return err
+		}
+		log.Error("ORM engine initialization attempt #%d/%d failed. Error: %v", i+1, setting.Database.DBConnectRetries, err)
+		log.Info("Backing off for %d seconds", int64(setting.Database.DBConnectBackoff/time.Second))
+		time.Sleep(setting.Database.DBConnectBackoff)
+	}
+	models.HasEngine = true
+	return nil
+}
diff --git a/routers/common/logger.go b/routers/common/logger.go
new file mode 100644
index 0000000000000..bc1149543c94e
--- /dev/null
+++ b/routers/common/logger.go
@@ -0,0 +1,33 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package common
+
+import (
+	"net/http"
+	"time"
+
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/log"
+)
+
+// LoggerHandler is a handler that will log the routing to the default gitea log
+func LoggerHandler(level log.Level) func(next http.Handler) http.Handler {
+	return func(next http.Handler) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+			start := time.Now()
+
+			_ = log.GetLogger("router").Log(0, level, "Started %s %s for %s", log.ColoredMethod(req.Method), req.URL.RequestURI(), req.RemoteAddr)
+
+			next.ServeHTTP(w, req)
+
+			var status int
+			if v, ok := w.(context.ResponseWriter); ok {
+				status = v.Status()
+			}
+
+			_ = log.GetLogger("router").Log(0, level, "Completed %s %s %v %s in %v", log.ColoredMethod(req.Method), req.URL.RequestURI(), log.ColoredStatus(status), log.ColoredStatus(status, http.StatusText(status)), log.ColoredTime(time.Since(start)))
+		})
+	}
+}
diff --git a/routers/common/middleware.go b/routers/common/middleware.go
new file mode 100644
index 0000000000000..1d96522dd9d19
--- /dev/null
+++ b/routers/common/middleware.go
@@ -0,0 +1,76 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package common
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+
+	"github.com/chi-middleware/proxy"
+	"github.com/go-chi/chi/middleware"
+)
+
+// Middlewares returns common middlewares
+func Middlewares() []func(http.Handler) http.Handler {
+	var handlers = []func(http.Handler) http.Handler{
+		func(next http.Handler) http.Handler {
+			return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+				next.ServeHTTP(context.NewResponse(resp), req)
+			})
+		},
+	}
+
+	if setting.ReverseProxyLimit > 0 {
+		opt := proxy.NewForwardedHeadersOptions().
+			WithForwardLimit(setting.ReverseProxyLimit).
+			ClearTrustedProxies()
+		for _, n := range setting.ReverseProxyTrustedProxies {
+			if !strings.Contains(n, "/") {
+				opt.AddTrustedProxy(n)
+			} else {
+				opt.AddTrustedNetwork(n)
+			}
+		}
+		handlers = append(handlers, proxy.ForwardedHeaders(opt))
+	}
+
+	handlers = append(handlers, middleware.StripSlashes)
+
+	if !setting.DisableRouterLog && setting.RouterLogLevel != log.NONE {
+		if log.GetLogger("router").GetLevel() <= setting.RouterLogLevel {
+			handlers = append(handlers, LoggerHandler(setting.RouterLogLevel))
+		}
+	}
+	if setting.EnableAccessLog {
+		handlers = append(handlers, context.AccessLogger())
+	}
+
+	handlers = append(handlers, func(next http.Handler) http.Handler {
+		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
+			// Why we need this? The Recovery() will try to render a beautiful
+			// error page for user, but the process can still panic again, and other
+			// middleware like session also may panic then we have to recover twice
+			// and send a simple error page that should not panic any more.
+			defer func() {
+				if err := recover(); err != nil {
+					combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
+					log.Error("%v", combinedErr)
+					if setting.IsProd() {
+						http.Error(resp, http.StatusText(500), 500)
+					} else {
+						http.Error(resp, combinedErr, 500)
+					}
+				}
+			}()
+			next.ServeHTTP(resp, req)
+		})
+	})
+	return handlers
+}
diff --git a/routers/common/repo.go b/routers/common/repo.go
new file mode 100644
index 0000000000000..c61b5ec57f525
--- /dev/null
+++ b/routers/common/repo.go
@@ -0,0 +1,127 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package common
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"path"
+	"path/filepath"
+	"strings"
+
+	"code.gitea.io/gitea/modules/charset"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/git"
+	"code.gitea.io/gitea/modules/httpcache"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/typesniffer"
+	"code.gitea.io/gitea/services/archiver"
+)
+
+// ServeBlob download a git.Blob
+func ServeBlob(ctx *context.Context, blob *git.Blob) error {
+	if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`) {
+		return nil
+	}
+
+	dataRc, err := blob.DataAsync()
+	if err != nil {
+		return err
+	}
+	defer func() {
+		if err = dataRc.Close(); err != nil {
+			log.Error("ServeBlob: Close: %v", err)
+		}
+	}()
+
+	return ServeData(ctx, ctx.Repo.TreePath, blob.Size(), dataRc)
+}
+
+// Download an archive of a repository
+func Download(ctx *context.Context) {
+	uri := ctx.Params("*")
+	aReq := archiver.DeriveRequestFrom(ctx, uri)
+
+	if aReq == nil {
+		ctx.Error(http.StatusNotFound)
+		return
+	}
+
+	downloadName := ctx.Repo.Repository.Name + "-" + aReq.GetArchiveName()
+	complete := aReq.IsComplete()
+	if !complete {
+		aReq = archiver.ArchiveRepository(aReq)
+		complete = aReq.WaitForCompletion(ctx)
+	}
+
+	if complete {
+		ctx.ServeFile(aReq.GetArchivePath(), downloadName)
+	} else {
+		ctx.Error(http.StatusNotFound)
+	}
+}
+
+// ServeData download file from io.Reader
+func ServeData(ctx *context.Context, name string, size int64, reader io.Reader) error {
+	buf := make([]byte, 1024)
+	n, err := reader.Read(buf)
+	if err != nil && err != io.EOF {
+		return err
+	}
+	if n >= 0 {
+		buf = buf[:n]
+	}
+
+	ctx.Resp.Header().Set("Cache-Control", "public,max-age=86400")
+
+	if size >= 0 {
+		ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
+	} else {
+		log.Error("ServeData called to serve data: %s with size < 0: %d", name, size)
+	}
+	name = path.Base(name)
+
+	// Google Chrome dislike commas in filenames, so let's change it to a space
+	name = strings.ReplaceAll(name, ",", " ")
+
+	st := typesniffer.DetectContentType(buf)
+
+	if st.IsText() || ctx.QueryBool("render") {
+		cs, err := charset.DetectEncoding(buf)
+		if err != nil {
+			log.Error("Detect raw file %s charset failed: %v, using by default utf-8", name, err)
+			cs = "utf-8"
+		}
+		ctx.Resp.Header().Set("Content-Type", "text/plain; charset="+strings.ToLower(cs))
+	} else {
+		ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
+
+		if (st.IsImage() || st.IsPDF()) && (setting.UI.SVG.Enabled || !st.IsSvgImage()) {
+			ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, name))
+			if st.IsSvgImage() {
+				ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
+				ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
+				ctx.Resp.Header().Set("Content-Type", typesniffer.SvgMimeType)
+			}
+		} else {
+			ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
+			if setting.MimeTypeMap.Enabled {
+				fileExtension := strings.ToLower(filepath.Ext(name))
+				if mimetype, ok := setting.MimeTypeMap.Map[fileExtension]; ok {
+					ctx.Resp.Header().Set("Content-Type", mimetype)
+				}
+			}
+		}
+	}
+
+	_, err = ctx.Resp.Write(buf)
+	if err != nil {
+		return err
+	}
+	_, err = io.Copy(ctx.Resp, reader)
+	return err
+}
diff --git a/routers/home.go b/routers/home.go
deleted file mode 100644
index 7eaebc081fd4e..0000000000000
--- a/routers/home.go
+++ /dev/null
@@ -1,413 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2019 The Gitea Authors. All rights reserved.
-// Use of this source code is governed by a MIT-style
-// license that can be found in the LICENSE file.
-
-package routers
-
-import (
-	"bytes"
-	"net/http"
-	"strings"
-
-	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/modules/base"
-	"code.gitea.io/gitea/modules/context"
-	code_indexer "code.gitea.io/gitea/modules/indexer/code"
-	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/modules/util"
-	"code.gitea.io/gitea/modules/web/middleware"
-	"code.gitea.io/gitea/routers/user"
-)
-
-const (
-	// tplHome home page template
-	tplHome base.TplName = "home"
-	// tplExploreRepos explore repositories page template
-	tplExploreRepos base.TplName = "explore/repos"
-	// tplExploreUsers explore users page template
-	tplExploreUsers base.TplName = "explore/users"
-	// tplExploreOrganizations explore organizations page template
-	tplExploreOrganizations base.TplName = "explore/organizations"
-	// tplExploreCode explore code page template
-	tplExploreCode base.TplName = "explore/code"
-)
-
-// Home render home page
-func Home(ctx *context.Context) {
-	if ctx.IsSigned {
-		if !ctx.User.IsActive && setting.Service.RegisterEmailConfirm {
-			ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
-			ctx.HTML(http.StatusOK, user.TplActivate)
-		} else if !ctx.User.IsActive || ctx.User.ProhibitLogin {
-			log.Info("Failed authentication attempt for %s from %s", ctx.User.Name, ctx.RemoteAddr())
-			ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
-			ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
-		} else if ctx.User.MustChangePassword {
-			ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
-			ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password"
-			middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
-			ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
-		} else {
-			user.Dashboard(ctx)
-		}
-		return
-		// Check non-logged users landing page.
-	} else if setting.LandingPageURL != setting.LandingPageHome {
-		ctx.Redirect(setting.AppSubURL + string(setting.LandingPageURL))
-		return
-	}
-
-	// Check auto-login.
-	uname := ctx.GetCookie(setting.CookieUserName)
-	if len(uname) != 0 {
-		ctx.Redirect(setting.AppSubURL + "/user/login")
-		return
-	}
-
-	ctx.Data["PageIsHome"] = true
-	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-	ctx.HTML(http.StatusOK, tplHome)
-}
-
-// RepoSearchOptions when calling search repositories
-type RepoSearchOptions struct {
-	OwnerID    int64
-	Private    bool
-	Restricted bool
-	PageSize   int
-	TplName    base.TplName
-}
-
-var (
-	nullByte = []byte{0x00}
-)
-
-func isKeywordValid(keyword string) bool {
-	return !bytes.Contains([]byte(keyword), nullByte)
-}
-
-// RenderRepoSearch render repositories search page
-func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
-	page := ctx.QueryInt("page")
-	if page <= 0 {
-		page = 1
-	}
-
-	var (
-		repos   []*models.Repository
-		count   int64
-		err     error
-		orderBy models.SearchOrderBy
-	)
-
-	ctx.Data["SortType"] = ctx.Query("sort")
-	switch ctx.Query("sort") {
-	case "newest":
-		orderBy = models.SearchOrderByNewest
-	case "oldest":
-		orderBy = models.SearchOrderByOldest
-	case "recentupdate":
-		orderBy = models.SearchOrderByRecentUpdated
-	case "leastupdate":
-		orderBy = models.SearchOrderByLeastUpdated
-	case "reversealphabetically":
-		orderBy = models.SearchOrderByAlphabeticallyReverse
-	case "alphabetically":
-		orderBy = models.SearchOrderByAlphabetically
-	case "reversesize":
-		orderBy = models.SearchOrderBySizeReverse
-	case "size":
-		orderBy = models.SearchOrderBySize
-	case "moststars":
-		orderBy = models.SearchOrderByStarsReverse
-	case "feweststars":
-		orderBy = models.SearchOrderByStars
-	case "mostforks":
-		orderBy = models.SearchOrderByForksReverse
-	case "fewestforks":
-		orderBy = models.SearchOrderByForks
-	default:
-		ctx.Data["SortType"] = "recentupdate"
-		orderBy = models.SearchOrderByRecentUpdated
-	}
-
-	keyword := strings.Trim(ctx.Query("q"), " ")
-	topicOnly := ctx.QueryBool("topic")
-	ctx.Data["TopicOnly"] = topicOnly
-
-	repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
-		ListOptions: models.ListOptions{
-			Page:     page,
-			PageSize: opts.PageSize,
-		},
-		Actor:              ctx.User,
-		OrderBy:            orderBy,
-		Private:            opts.Private,
-		Keyword:            keyword,
-		OwnerID:            opts.OwnerID,
-		AllPublic:          true,
-		AllLimited:         true,
-		TopicOnly:          topicOnly,
-		IncludeDescription: setting.UI.SearchRepoDescription,
-	})
-	if err != nil {
-		ctx.ServerError("SearchRepository", err)
-		return
-	}
-	ctx.Data["Keyword"] = keyword
-	ctx.Data["Total"] = count
-	ctx.Data["Repos"] = repos
-	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-
-	pager := context.NewPagination(int(count), opts.PageSize, page, 5)
-	pager.SetDefaultParams(ctx)
-	pager.AddParam(ctx, "topic", "TopicOnly")
-	ctx.Data["Page"] = pager
-
-	ctx.HTML(http.StatusOK, opts.TplName)
-}
-
-// ExploreRepos render explore repositories page
-func ExploreRepos(ctx *context.Context) {
-	ctx.Data["UsersIsDisabled"] = setting.Service.Explore.DisableUsersPage
-	ctx.Data["Title"] = ctx.Tr("explore")
-	ctx.Data["PageIsExplore"] = true
-	ctx.Data["PageIsExploreRepositories"] = true
-	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-
-	var ownerID int64
-	if ctx.User != nil && !ctx.User.IsAdmin {
-		ownerID = ctx.User.ID
-	}
-
-	RenderRepoSearch(ctx, &RepoSearchOptions{
-		PageSize: setting.UI.ExplorePagingNum,
-		OwnerID:  ownerID,
-		Private:  ctx.User != nil,
-		TplName:  tplExploreRepos,
-	})
-}
-
-// RenderUserSearch render user search page
-func RenderUserSearch(ctx *context.Context, opts *models.SearchUserOptions, tplName base.TplName) {
-	opts.Page = ctx.QueryInt("page")
-	if opts.Page <= 1 {
-		opts.Page = 1
-	}
-
-	var (
-		users   []*models.User
-		count   int64
-		err     error
-		orderBy models.SearchOrderBy
-	)
-
-	ctx.Data["SortType"] = ctx.Query("sort")
-	switch ctx.Query("sort") {
-	case "newest":
-		orderBy = models.SearchOrderByIDReverse
-	case "oldest":
-		orderBy = models.SearchOrderByID
-	case "recentupdate":
-		orderBy = models.SearchOrderByRecentUpdated
-	case "leastupdate":
-		orderBy = models.SearchOrderByLeastUpdated
-	case "reversealphabetically":
-		orderBy = models.SearchOrderByAlphabeticallyReverse
-	case "alphabetically":
-		orderBy = models.SearchOrderByAlphabetically
-	default:
-		ctx.Data["SortType"] = "alphabetically"
-		orderBy = models.SearchOrderByAlphabetically
-	}
-
-	opts.Keyword = strings.Trim(ctx.Query("q"), " ")
-	opts.OrderBy = orderBy
-	if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
-		users, count, err = models.SearchUsers(opts)
-		if err != nil {
-			ctx.ServerError("SearchUsers", err)
-			return
-		}
-	}
-	ctx.Data["Keyword"] = opts.Keyword
-	ctx.Data["Total"] = count
-	ctx.Data["Users"] = users
-	ctx.Data["UsersTwoFaStatus"] = models.UserList(users).GetTwoFaStatus()
-	ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail
-	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-
-	pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
-	pager.SetDefaultParams(ctx)
-	ctx.Data["Page"] = pager
-
-	ctx.HTML(http.StatusOK, tplName)
-}
-
-// ExploreUsers render explore users page
-func ExploreUsers(ctx *context.Context) {
-	if setting.Service.Explore.DisableUsersPage {
-		ctx.Redirect(setting.AppSubURL + "/explore/repos")
-		return
-	}
-	ctx.Data["Title"] = ctx.Tr("explore")
-	ctx.Data["PageIsExplore"] = true
-	ctx.Data["PageIsExploreUsers"] = true
-	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-
-	RenderUserSearch(ctx, &models.SearchUserOptions{
-		Actor:       ctx.User,
-		Type:        models.UserTypeIndividual,
-		ListOptions: models.ListOptions{PageSize: setting.UI.ExplorePagingNum},
-		IsActive:    util.OptionalBoolTrue,
-		Visible:     []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
-	}, tplExploreUsers)
-}
-
-// ExploreOrganizations render explore organizations page
-func ExploreOrganizations(ctx *context.Context) {
-	ctx.Data["UsersIsDisabled"] = setting.Service.Explore.DisableUsersPage
-	ctx.Data["Title"] = ctx.Tr("explore")
-	ctx.Data["PageIsExplore"] = true
-	ctx.Data["PageIsExploreOrganizations"] = true
-	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-
-	visibleTypes := []structs.VisibleType{structs.VisibleTypePublic}
-	if ctx.User != nil {
-		visibleTypes = append(visibleTypes, structs.VisibleTypeLimited, structs.VisibleTypePrivate)
-	}
-
-	RenderUserSearch(ctx, &models.SearchUserOptions{
-		Actor:       ctx.User,
-		Type:        models.UserTypeOrganization,
-		ListOptions: models.ListOptions{PageSize: setting.UI.ExplorePagingNum},
-		Visible:     visibleTypes,
-	}, tplExploreOrganizations)
-}
-
-// ExploreCode render explore code page
-func ExploreCode(ctx *context.Context) {
-	if !setting.Indexer.RepoIndexerEnabled {
-		ctx.Redirect(setting.AppSubURL+"/explore", 302)
-		return
-	}
-
-	ctx.Data["UsersIsDisabled"] = setting.Service.Explore.DisableUsersPage
-	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
-	ctx.Data["Title"] = ctx.Tr("explore")
-	ctx.Data["PageIsExplore"] = true
-	ctx.Data["PageIsExploreCode"] = true
-
-	language := strings.TrimSpace(ctx.Query("l"))
-	keyword := strings.TrimSpace(ctx.Query("q"))
-	page := ctx.QueryInt("page")
-	if page <= 0 {
-		page = 1
-	}
-
-	queryType := strings.TrimSpace(ctx.Query("t"))
-	isMatch := queryType == "match"
-
-	var (
-		repoIDs []int64
-		err     error
-		isAdmin bool
-	)
-	if ctx.User != nil {
-		isAdmin = ctx.User.IsAdmin
-	}
-
-	// guest user or non-admin user
-	if ctx.User == nil || !isAdmin {
-		repoIDs, err = models.FindUserAccessibleRepoIDs(ctx.User)
-		if err != nil {
-			ctx.ServerError("SearchResults", err)
-			return
-		}
-	}
-
-	var (
-		total                 int
-		searchResults         []*code_indexer.Result
-		searchResultLanguages []*code_indexer.SearchResultLanguages
-	)
-
-	// if non-admin login user, we need check UnitTypeCode at first
-	if ctx.User != nil && len(repoIDs) > 0 {
-		repoMaps, err := models.GetRepositoriesMapByIDs(repoIDs)
-		if err != nil {
-			ctx.ServerError("SearchResults", err)
-			return
-		}
-
-		var rightRepoMap = make(map[int64]*models.Repository, len(repoMaps))
-		repoIDs = make([]int64, 0, len(repoMaps))
-		for id, repo := range repoMaps {
-			if repo.CheckUnitUser(ctx.User, models.UnitTypeCode) {
-				rightRepoMap[id] = repo
-				repoIDs = append(repoIDs, id)
-			}
-		}
-
-		ctx.Data["RepoMaps"] = rightRepoMap
-
-		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
-		if err != nil {
-			ctx.ServerError("SearchResults", err)
-			return
-		}
-		// if non-login user or isAdmin, no need to check UnitTypeCode
-	} else if (ctx.User == nil && len(repoIDs) > 0) || isAdmin {
-		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
-		if err != nil {
-			ctx.ServerError("SearchResults", err)
-			return
-		}
-
-		var loadRepoIDs = make([]int64, 0, len(searchResults))
-		for _, result := range searchResults {
-			var find bool
-			for _, id := range loadRepoIDs {
-				if id == result.RepoID {
-					find = true
-					break
-				}
-			}
-			if !find {
-				loadRepoIDs = append(loadRepoIDs, result.RepoID)
-			}
-		}
-
-		repoMaps, err := models.GetRepositoriesMapByIDs(loadRepoIDs)
-		if err != nil {
-			ctx.ServerError("SearchResults", err)
-			return
-		}
-
-		ctx.Data["RepoMaps"] = repoMaps
-	}
-
-	ctx.Data["Keyword"] = keyword
-	ctx.Data["Language"] = language
-	ctx.Data["queryType"] = queryType
-	ctx.Data["SearchResults"] = searchResults
-	ctx.Data["SearchResultLanguages"] = searchResultLanguages
-	ctx.Data["RequireHighlightJS"] = true
-	ctx.Data["PageIsViewCode"] = true
-
-	pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
-	pager.SetDefaultParams(ctx)
-	pager.AddParam(ctx, "l", "Language")
-	ctx.Data["Page"] = pager
-
-	ctx.HTML(http.StatusOK, tplExploreCode)
-}
-
-// NotFound render 404 page
-func NotFound(ctx *context.Context) {
-	ctx.Data["Title"] = "Page Not Found"
-	ctx.NotFound("home.NotFound", nil)
-}
diff --git a/routers/init.go b/routers/init.go
index 220d87a29da87..5e2eca439eb68 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -6,12 +6,9 @@ package routers
 
 import (
 	"context"
-	"fmt"
 	"strings"
-	"time"
 
 	"code.gitea.io/gitea/models"
-	"code.gitea.io/gitea/models/migrations"
 	"code.gitea.io/gitea/modules/auth/sso"
 	"code.gitea.io/gitea/modules/cache"
 	"code.gitea.io/gitea/modules/cron"
@@ -32,6 +29,11 @@ import (
 	"code.gitea.io/gitea/modules/svg"
 	"code.gitea.io/gitea/modules/task"
 	"code.gitea.io/gitea/modules/translation"
+	"code.gitea.io/gitea/modules/web"
+	apiv1 "code.gitea.io/gitea/routers/api/v1"
+	"code.gitea.io/gitea/routers/common"
+	"code.gitea.io/gitea/routers/private"
+	web_routers "code.gitea.io/gitea/routers/web"
 	"code.gitea.io/gitea/services/mailer"
 	mirror_service "code.gitea.io/gitea/services/mirror"
 	pull_service "code.gitea.io/gitea/services/pull"
@@ -63,63 +65,6 @@ func NewServices() {
 	notification.NewContext()
 }
 
-// In case of problems connecting to DB, retry connection. Eg, PGSQL in Docker Container on Synology
-func initDBEngine(ctx context.Context) (err error) {
-	log.Info("Beginning ORM engine initialization.")
-	for i := 0; i < setting.Database.DBConnectRetries; i++ {
-		select {
-		case <-ctx.Done():
-			return fmt.Errorf("Aborted due to shutdown:\nin retry ORM engine initialization")
-		default:
-		}
-		log.Info("ORM engine initialization attempt #%d/%d...", i+1, setting.Database.DBConnectRetries)
-		if err = models.NewEngine(ctx, migrations.Migrate); err == nil {
-			break
-		} else if i == setting.Database.DBConnectRetries-1 {
-			return err
-		}
-		log.Error("ORM engine initialization attempt #%d/%d failed. Error: %v", i+1, setting.Database.DBConnectRetries, err)
-		log.Info("Backing off for %d seconds", int64(setting.Database.DBConnectBackoff/time.Second))
-		time.Sleep(setting.Database.DBConnectBackoff)
-	}
-	models.HasEngine = true
-	return nil
-}
-
-// PreInstallInit preloads the configuration to check if we need to run install
-func PreInstallInit(ctx context.Context) bool {
-	setting.NewContext()
-	if !setting.InstallLock {
-		log.Trace("AppPath: %s", setting.AppPath)
-		log.Trace("AppWorkPath: %s", setting.AppWorkPath)
-		log.Trace("Custom path: %s", setting.CustomPath)
-		log.Trace("Log path: %s", setting.LogRootPath)
-		log.Trace("Preparing to run install page")
-		translation.InitLocales()
-		if setting.EnableSQLite3 {
-			log.Info("SQLite3 Supported")
-		}
-		setting.InitDBConfig()
-		svg.Init()
-	}
-
-	return !setting.InstallLock
-}
-
-// PostInstallInit rereads the settings and starts up the database
-func PostInstallInit(ctx context.Context) {
-	setting.NewContext()
-	setting.InitDBConfig()
-	if setting.InstallLock {
-		if err := initDBEngine(ctx); err == nil {
-			log.Info("ORM engine initialization successful!")
-		} else {
-			log.Fatal("ORM engine initialization failed: %v", err)
-		}
-		svg.Init()
-	}
-}
-
 // GlobalInit is for global configuration reload-able.
 func GlobalInit(ctx context.Context) {
 	setting.NewContext()
@@ -151,7 +96,7 @@ func GlobalInit(ctx context.Context) {
 	} else if setting.Database.UseSQLite3 {
 		log.Fatal("SQLite3 is set in settings but NOT Supported")
 	}
-	if err := initDBEngine(ctx); err == nil {
+	if err := common.InitDBEngine(ctx); err == nil {
 		log.Info("ORM engine initialization successful!")
 	} else {
 		log.Fatal("ORM engine initialization failed: %v", err)
@@ -193,3 +138,16 @@ func GlobalInit(ctx context.Context) {
 
 	svg.Init()
 }
+
+// NormalRoutes represents non install routes
+func NormalRoutes() *web.Route {
+	r := web.NewRoute()
+	for _, middle := range common.Middlewares() {
+		r.Use(middle)
+	}
+
+	r.Mount("/", web_routers.Routes())
+	r.Mount("/api/v1", apiv1.Routes())
+	r.Mount("/api/internal", private.Routes())
+	return r
+}
diff --git a/routers/install.go b/routers/install/install.go
similarity index 98%
rename from routers/install.go
rename to routers/install/install.go
index 6c460a887d81c..a7040bccad9f7 100644
--- a/routers/install.go
+++ b/routers/install/install.go
@@ -1,8 +1,9 @@
 // Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2021 The Gitea Authors. All rights reserved.
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package routers
+package install
 
 import (
 	"fmt"
@@ -38,8 +39,8 @@ const (
 	tplPostInstall base.TplName = "post-install"
 )
 
-// InstallInit prepare for rendering installation page
-func InstallInit(next http.Handler) http.Handler {
+// Init prepare for rendering installation page
+func Init(next http.Handler) http.Handler {
 	var rnd = templates.HTMLRenderer()
 
 	return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
@@ -158,8 +159,8 @@ func Install(ctx *context.Context) {
 	ctx.HTML(http.StatusOK, tplInstall)
 }
 
-// InstallPost response for submit install items
-func InstallPost(ctx *context.Context) {
+// SubmitInstall response for submit install items
+func SubmitInstall(ctx *context.Context) {
 	form := *web.GetForm(ctx).(*forms.InstallForm)
 	var err error
 	ctx.Data["CurDbOption"] = form.DbType
@@ -409,7 +410,7 @@ func InstallPost(ctx *context.Context) {
 	}
 
 	// Re-read settings
-	PostInstallInit(ctx)
+	ReloadSettings(ctx)
 
 	// Create admin account
 	if len(form.AdminName) > 0 {
diff --git a/routers/routes/install.go b/routers/install/routes.go
similarity index 81%
rename from routers/routes/install.go
rename to routers/install/routes.go
index 0918da1a4b8ab..36130d4b3f398 100644
--- a/routers/routes/install.go
+++ b/routers/install/routes.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package routes
+package install
 
 import (
 	"fmt"
@@ -15,12 +15,18 @@ import (
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/modules/web/middleware"
-	"code.gitea.io/gitea/routers"
+	"code.gitea.io/gitea/routers/common"
 	"code.gitea.io/gitea/services/forms"
 
 	"gitea.com/go-chi/session"
 )
 
+type dataStore map[string]interface{}
+
+func (d *dataStore) GetData() map[string]interface{} {
+	return *d
+}
+
 func installRecovery() func(next http.Handler) http.Handler {
 	var rnd = templates.HTMLRenderer()
 	return func(next http.Handler) http.Handler {
@@ -48,21 +54,19 @@ func installRecovery() func(next http.Handler) http.Handler {
 
 					lc := middleware.Locale(w, req)
 					var store = dataStore{
-						Data: templates.Vars{
-							"Language":       lc.Language(),
-							"CurrentURL":     setting.AppSubURL + req.URL.RequestURI(),
-							"i18n":           lc,
-							"SignedUserID":   int64(0),
-							"SignedUserName": "",
-						},
+						"Language":       lc.Language(),
+						"CurrentURL":     setting.AppSubURL + req.URL.RequestURI(),
+						"i18n":           lc,
+						"SignedUserID":   int64(0),
+						"SignedUserName": "",
 					}
 
 					w.Header().Set(`X-Frame-Options`, `SAMEORIGIN`)
 
 					if !setting.IsProd() {
-						store.Data["ErrorMsg"] = combinedErr
+						store["ErrorMsg"] = combinedErr
 					}
-					err = rnd.HTML(w, 500, "status/500", templates.BaseVars().Merge(store.Data))
+					err = rnd.HTML(w, 500, "status/500", templates.BaseVars().Merge(store))
 					if err != nil {
 						log.Error("%v", err)
 					}
@@ -74,10 +78,10 @@ func installRecovery() func(next http.Handler) http.Handler {
 	}
 }
 
-// InstallRoutes registers the install routes
-func InstallRoutes() *web.Route {
+// Routes registers the install routes
+func Routes() *web.Route {
 	r := web.NewRoute()
-	for _, middle := range commonMiddlewares() {
+	for _, middle := range common.Middlewares() {
 		r.Use(middle)
 	}
 
@@ -99,9 +103,9 @@ func InstallRoutes() *web.Route {
 	}))
 
 	r.Use(installRecovery())
-	r.Use(routers.InstallInit)
-	r.Get("/", routers.Install)
-	r.Post("/", web.Bind(forms.InstallForm{}), routers.InstallPost)
+	r.Use(Init)
+	r.Get("/", Install)
+	r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall)
 	r.NotFound(func(w http.ResponseWriter, req *http.Request) {
 		http.Redirect(w, req, setting.AppURL, http.StatusFound)
 	})
diff --git a/routers/install/setting.go b/routers/install/setting.go
new file mode 100644
index 0000000000000..50bb6aa355bfa
--- /dev/null
+++ b/routers/install/setting.go
@@ -0,0 +1,49 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package install
+
+import (
+	"context"
+
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/svg"
+	"code.gitea.io/gitea/modules/translation"
+	"code.gitea.io/gitea/routers/common"
+)
+
+// PreloadSettings preloads the configuration to check if we need to run install
+func PreloadSettings(ctx context.Context) bool {
+	setting.NewContext()
+	if !setting.InstallLock {
+		log.Trace("AppPath: %s", setting.AppPath)
+		log.Trace("AppWorkPath: %s", setting.AppWorkPath)
+		log.Trace("Custom path: %s", setting.CustomPath)
+		log.Trace("Log path: %s", setting.LogRootPath)
+		log.Trace("Preparing to run install page")
+		translation.InitLocales()
+		if setting.EnableSQLite3 {
+			log.Info("SQLite3 Supported")
+		}
+		setting.InitDBConfig()
+		svg.Init()
+	}
+
+	return !setting.InstallLock
+}
+
+// ReloadSettings rereads the settings and starts up the database
+func ReloadSettings(ctx context.Context) {
+	setting.NewContext()
+	setting.InitDBConfig()
+	if setting.InstallLock {
+		if err := common.InitDBEngine(ctx); err == nil {
+			log.Info("ORM engine initialization successful!")
+		} else {
+			log.Fatal("ORM engine initialization failed: %v", err)
+		}
+		svg.Init()
+	}
+}
diff --git a/routers/admin/admin.go b/routers/web/admin/admin.go
similarity index 100%
rename from routers/admin/admin.go
rename to routers/web/admin/admin.go
diff --git a/routers/admin/admin_test.go b/routers/web/admin/admin_test.go
similarity index 100%
rename from routers/admin/admin_test.go
rename to routers/web/admin/admin_test.go
diff --git a/routers/admin/auths.go b/routers/web/admin/auths.go
similarity index 100%
rename from routers/admin/auths.go
rename to routers/web/admin/auths.go
diff --git a/routers/admin/emails.go b/routers/web/admin/emails.go
similarity index 100%
rename from routers/admin/emails.go
rename to routers/web/admin/emails.go
diff --git a/routers/admin/hooks.go b/routers/web/admin/hooks.go
similarity index 100%
rename from routers/admin/hooks.go
rename to routers/web/admin/hooks.go
diff --git a/routers/user/setting/main_test.go b/routers/web/admin/main_test.go
similarity index 95%
rename from routers/user/setting/main_test.go
rename to routers/web/admin/main_test.go
index d343c02f484ba..352907c73717c 100644
--- a/routers/user/setting/main_test.go
+++ b/routers/web/admin/main_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package setting
+package admin
 
 import (
 	"path/filepath"
diff --git a/routers/admin/notice.go b/routers/web/admin/notice.go
similarity index 100%
rename from routers/admin/notice.go
rename to routers/web/admin/notice.go
diff --git a/routers/admin/orgs.go b/routers/web/admin/orgs.go
similarity index 90%
rename from routers/admin/orgs.go
rename to routers/web/admin/orgs.go
index 627f56eaecdfc..618f945704492 100644
--- a/routers/admin/orgs.go
+++ b/routers/web/admin/orgs.go
@@ -11,7 +11,7 @@ import (
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
-	"code.gitea.io/gitea/routers"
+	"code.gitea.io/gitea/routers/web/explore"
 )
 
 const (
@@ -24,7 +24,7 @@ func Organizations(ctx *context.Context) {
 	ctx.Data["PageIsAdmin"] = true
 	ctx.Data["PageIsAdminOrganizations"] = true
 
-	routers.RenderUserSearch(ctx, &models.SearchUserOptions{
+	explore.RenderUserSearch(ctx, &models.SearchUserOptions{
 		Type: models.UserTypeOrganization,
 		ListOptions: models.ListOptions{
 			PageSize: setting.UI.Admin.OrgPagingNum,
diff --git a/routers/admin/repos.go b/routers/web/admin/repos.go
similarity index 97%
rename from routers/admin/repos.go
rename to routers/web/admin/repos.go
index d23f7c3d5a613..6128992f5a336 100644
--- a/routers/admin/repos.go
+++ b/routers/web/admin/repos.go
@@ -17,7 +17,7 @@ import (
 	"code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
-	"code.gitea.io/gitea/routers"
+	"code.gitea.io/gitea/routers/web/explore"
 	repo_service "code.gitea.io/gitea/services/repository"
 )
 
@@ -32,7 +32,7 @@ func Repos(ctx *context.Context) {
 	ctx.Data["PageIsAdmin"] = true
 	ctx.Data["PageIsAdminRepositories"] = true
 
-	routers.RenderRepoSearch(ctx, &routers.RepoSearchOptions{
+	explore.RenderRepoSearch(ctx, &explore.RepoSearchOptions{
 		Private:  true,
 		PageSize: setting.UI.Admin.RepoPagingNum,
 		TplName:  tplRepos,
diff --git a/routers/admin/users.go b/routers/web/admin/users.go
similarity index 98%
rename from routers/admin/users.go
rename to routers/web/admin/users.go
index a71a11dd8a225..1b65795865fa7 100644
--- a/routers/admin/users.go
+++ b/routers/web/admin/users.go
@@ -18,8 +18,8 @@ import (
 	"code.gitea.io/gitea/modules/password"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
-	"code.gitea.io/gitea/routers"
-	router_user_setting "code.gitea.io/gitea/routers/user/setting"
+	"code.gitea.io/gitea/routers/web/explore"
+	router_user_setting "code.gitea.io/gitea/routers/web/user/setting"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
 )
@@ -36,7 +36,7 @@ func Users(ctx *context.Context) {
 	ctx.Data["PageIsAdmin"] = true
 	ctx.Data["PageIsAdminUsers"] = true
 
-	routers.RenderUserSearch(ctx, &models.SearchUserOptions{
+	explore.RenderUserSearch(ctx, &models.SearchUserOptions{
 		Type: models.UserTypeIndividual,
 		ListOptions: models.ListOptions{
 			PageSize: setting.UI.Admin.UserPagingNum,
diff --git a/routers/admin/users_test.go b/routers/web/admin/users_test.go
similarity index 100%
rename from routers/admin/users_test.go
rename to routers/web/admin/users_test.go
diff --git a/routers/routes/base.go b/routers/web/base.go
similarity index 76%
rename from routers/routes/base.go
rename to routers/web/base.go
index 0b784508a7902..8a44736434bdc 100644
--- a/routers/routes/base.go
+++ b/routers/web/base.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package routes
+package web
 
 import (
 	"errors"
@@ -13,7 +13,6 @@ import (
 	"path"
 	"path/filepath"
 	"strings"
-	"time"
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/auth/sso"
@@ -28,26 +27,6 @@ import (
 	"gitea.com/go-chi/session"
 )
 
-// LoggerHandler is a handler that will log the routing to the default gitea log
-func LoggerHandler(level log.Level) func(next http.Handler) http.Handler {
-	return func(next http.Handler) http.Handler {
-		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-			start := time.Now()
-
-			_ = log.GetLogger("router").Log(0, level, "Started %s %s for %s", log.ColoredMethod(req.Method), req.URL.RequestURI(), req.RemoteAddr)
-
-			next.ServeHTTP(w, req)
-
-			var status int
-			if v, ok := w.(context.ResponseWriter); ok {
-				status = v.Status()
-			}
-
-			_ = log.GetLogger("router").Log(0, level, "Completed %s %s %v %s in %v", log.ColoredMethod(req.Method), req.URL.RequestURI(), log.ColoredStatus(status), log.ColoredStatus(status, http.StatusText(status)), log.ColoredTime(time.Since(start)))
-		})
-	}
-}
-
 func storageHandler(storageSetting setting.Storage, prefix string, objStore storage.ObjectStorage) func(next http.Handler) http.Handler {
 	return func(next http.Handler) http.Handler {
 		if storageSetting.ServeDirect {
@@ -134,12 +113,10 @@ func storageHandler(storageSetting setting.Storage, prefix string, objStore stor
 	}
 }
 
-type dataStore struct {
-	Data map[string]interface{}
-}
+type dataStore map[string]interface{}
 
 func (d *dataStore) GetData() map[string]interface{} {
-	return d.Data
+	return *d
 }
 
 // Recovery returns a middleware that recovers from any panics and writes a 500 and a log if so.
@@ -165,11 +142,9 @@ func Recovery() func(next http.Handler) http.Handler {
 
 					var lc = middleware.Locale(w, req)
 					var store = dataStore{
-						Data: templates.Vars{
-							"Language":   lc.Language(),
-							"CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
-							"i18n":       lc,
-						},
+						"Language":   lc.Language(),
+						"CurrentURL": setting.AppSubURL + req.URL.RequestURI(),
+						"i18n":       lc,
 					}
 
 					var user *models.User
@@ -186,22 +161,22 @@ func Recovery() func(next http.Handler) http.Handler {
 						user = sso.SessionUser(sessionStore)
 					}
 					if user != nil {
-						store.Data["IsSigned"] = true
-						store.Data["SignedUser"] = user
-						store.Data["SignedUserID"] = user.ID
-						store.Data["SignedUserName"] = user.Name
-						store.Data["IsAdmin"] = user.IsAdmin
+						store["IsSigned"] = true
+						store["SignedUser"] = user
+						store["SignedUserID"] = user.ID
+						store["SignedUserName"] = user.Name
+						store["IsAdmin"] = user.IsAdmin
 					} else {
-						store.Data["SignedUserID"] = int64(0)
-						store.Data["SignedUserName"] = ""
+						store["SignedUserID"] = int64(0)
+						store["SignedUserName"] = ""
 					}
 
 					w.Header().Set(`X-Frame-Options`, `SAMEORIGIN`)
 
 					if !setting.IsProd() {
-						store.Data["ErrorMsg"] = combinedErr
+						store["ErrorMsg"] = combinedErr
 					}
-					err = rnd.HTML(w, 500, "status/500", templates.BaseVars().Merge(store.Data))
+					err = rnd.HTML(w, 500, "status/500", templates.BaseVars().Merge(store))
 					if err != nil {
 						log.Error("%v", err)
 					}
diff --git a/routers/dev/template.go b/routers/web/dev/template.go
similarity index 100%
rename from routers/dev/template.go
rename to routers/web/dev/template.go
diff --git a/routers/events/events.go b/routers/web/events/events.go
similarity index 98%
rename from routers/events/events.go
rename to routers/web/events/events.go
index b140bf660cace..f9cc274851756 100644
--- a/routers/events/events.go
+++ b/routers/web/events/events.go
@@ -15,7 +15,7 @@ import (
 	"code.gitea.io/gitea/modules/graceful"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/routers/user"
+	"code.gitea.io/gitea/routers/web/user"
 	jsoniter "github.com/json-iterator/go"
 )
 
diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go
new file mode 100644
index 0000000000000..bf15b93cffd4c
--- /dev/null
+++ b/routers/web/explore/code.go
@@ -0,0 +1,139 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package explore
+
+import (
+	"net/http"
+	"strings"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/context"
+	code_indexer "code.gitea.io/gitea/modules/indexer/code"
+	"code.gitea.io/gitea/modules/setting"
+)
+
+const (
+	// tplExploreCode explore code page template
+	tplExploreCode base.TplName = "explore/code"
+)
+
+// Code render explore code page
+func Code(ctx *context.Context) {
+	if !setting.Indexer.RepoIndexerEnabled {
+		ctx.Redirect(setting.AppSubURL+"/explore", 302)
+		return
+	}
+
+	ctx.Data["UsersIsDisabled"] = setting.Service.Explore.DisableUsersPage
+	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+	ctx.Data["Title"] = ctx.Tr("explore")
+	ctx.Data["PageIsExplore"] = true
+	ctx.Data["PageIsExploreCode"] = true
+
+	language := strings.TrimSpace(ctx.Query("l"))
+	keyword := strings.TrimSpace(ctx.Query("q"))
+	page := ctx.QueryInt("page")
+	if page <= 0 {
+		page = 1
+	}
+
+	queryType := strings.TrimSpace(ctx.Query("t"))
+	isMatch := queryType == "match"
+
+	var (
+		repoIDs []int64
+		err     error
+		isAdmin bool
+	)
+	if ctx.User != nil {
+		isAdmin = ctx.User.IsAdmin
+	}
+
+	// guest user or non-admin user
+	if ctx.User == nil || !isAdmin {
+		repoIDs, err = models.FindUserAccessibleRepoIDs(ctx.User)
+		if err != nil {
+			ctx.ServerError("SearchResults", err)
+			return
+		}
+	}
+
+	var (
+		total                 int
+		searchResults         []*code_indexer.Result
+		searchResultLanguages []*code_indexer.SearchResultLanguages
+	)
+
+	// if non-admin login user, we need check UnitTypeCode at first
+	if ctx.User != nil && len(repoIDs) > 0 {
+		repoMaps, err := models.GetRepositoriesMapByIDs(repoIDs)
+		if err != nil {
+			ctx.ServerError("SearchResults", err)
+			return
+		}
+
+		var rightRepoMap = make(map[int64]*models.Repository, len(repoMaps))
+		repoIDs = make([]int64, 0, len(repoMaps))
+		for id, repo := range repoMaps {
+			if repo.CheckUnitUser(ctx.User, models.UnitTypeCode) {
+				rightRepoMap[id] = repo
+				repoIDs = append(repoIDs, id)
+			}
+		}
+
+		ctx.Data["RepoMaps"] = rightRepoMap
+
+		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
+		if err != nil {
+			ctx.ServerError("SearchResults", err)
+			return
+		}
+		// if non-login user or isAdmin, no need to check UnitTypeCode
+	} else if (ctx.User == nil && len(repoIDs) > 0) || isAdmin {
+		total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(repoIDs, language, keyword, page, setting.UI.RepoSearchPagingNum, isMatch)
+		if err != nil {
+			ctx.ServerError("SearchResults", err)
+			return
+		}
+
+		var loadRepoIDs = make([]int64, 0, len(searchResults))
+		for _, result := range searchResults {
+			var find bool
+			for _, id := range loadRepoIDs {
+				if id == result.RepoID {
+					find = true
+					break
+				}
+			}
+			if !find {
+				loadRepoIDs = append(loadRepoIDs, result.RepoID)
+			}
+		}
+
+		repoMaps, err := models.GetRepositoriesMapByIDs(loadRepoIDs)
+		if err != nil {
+			ctx.ServerError("SearchResults", err)
+			return
+		}
+
+		ctx.Data["RepoMaps"] = repoMaps
+	}
+
+	ctx.Data["Keyword"] = keyword
+	ctx.Data["Language"] = language
+	ctx.Data["queryType"] = queryType
+	ctx.Data["SearchResults"] = searchResults
+	ctx.Data["SearchResultLanguages"] = searchResultLanguages
+	ctx.Data["RequireHighlightJS"] = true
+	ctx.Data["PageIsViewCode"] = true
+
+	pager := context.NewPagination(total, setting.UI.RepoSearchPagingNum, page, 5)
+	pager.SetDefaultParams(ctx)
+	pager.AddParam(ctx, "l", "Language")
+	ctx.Data["Page"] = pager
+
+	ctx.HTML(http.StatusOK, tplExploreCode)
+}
diff --git a/routers/web/explore/org.go b/routers/web/explore/org.go
new file mode 100644
index 0000000000000..470e0eb8530b1
--- /dev/null
+++ b/routers/web/explore/org.go
@@ -0,0 +1,39 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package explore
+
+import (
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/structs"
+)
+
+const (
+	// tplExploreOrganizations explore organizations page template
+	tplExploreOrganizations base.TplName = "explore/organizations"
+)
+
+// Organizations render explore organizations page
+func Organizations(ctx *context.Context) {
+	ctx.Data["UsersIsDisabled"] = setting.Service.Explore.DisableUsersPage
+	ctx.Data["Title"] = ctx.Tr("explore")
+	ctx.Data["PageIsExplore"] = true
+	ctx.Data["PageIsExploreOrganizations"] = true
+	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+
+	visibleTypes := []structs.VisibleType{structs.VisibleTypePublic}
+	if ctx.User != nil {
+		visibleTypes = append(visibleTypes, structs.VisibleTypeLimited, structs.VisibleTypePrivate)
+	}
+
+	RenderUserSearch(ctx, &models.SearchUserOptions{
+		Actor:       ctx.User,
+		Type:        models.UserTypeOrganization,
+		ListOptions: models.ListOptions{PageSize: setting.UI.ExplorePagingNum},
+		Visible:     visibleTypes,
+	}, tplExploreOrganizations)
+}
diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go
new file mode 100644
index 0000000000000..e9efae5688d7d
--- /dev/null
+++ b/routers/web/explore/repo.go
@@ -0,0 +1,131 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package explore
+
+import (
+	"net/http"
+	"strings"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/setting"
+)
+
+const (
+	// tplExploreRepos explore repositories page template
+	tplExploreRepos base.TplName = "explore/repos"
+)
+
+// RepoSearchOptions when calling search repositories
+type RepoSearchOptions struct {
+	OwnerID    int64
+	Private    bool
+	Restricted bool
+	PageSize   int
+	TplName    base.TplName
+}
+
+// RenderRepoSearch render repositories search page
+func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
+	page := ctx.QueryInt("page")
+	if page <= 0 {
+		page = 1
+	}
+
+	var (
+		repos   []*models.Repository
+		count   int64
+		err     error
+		orderBy models.SearchOrderBy
+	)
+
+	ctx.Data["SortType"] = ctx.Query("sort")
+	switch ctx.Query("sort") {
+	case "newest":
+		orderBy = models.SearchOrderByNewest
+	case "oldest":
+		orderBy = models.SearchOrderByOldest
+	case "recentupdate":
+		orderBy = models.SearchOrderByRecentUpdated
+	case "leastupdate":
+		orderBy = models.SearchOrderByLeastUpdated
+	case "reversealphabetically":
+		orderBy = models.SearchOrderByAlphabeticallyReverse
+	case "alphabetically":
+		orderBy = models.SearchOrderByAlphabetically
+	case "reversesize":
+		orderBy = models.SearchOrderBySizeReverse
+	case "size":
+		orderBy = models.SearchOrderBySize
+	case "moststars":
+		orderBy = models.SearchOrderByStarsReverse
+	case "feweststars":
+		orderBy = models.SearchOrderByStars
+	case "mostforks":
+		orderBy = models.SearchOrderByForksReverse
+	case "fewestforks":
+		orderBy = models.SearchOrderByForks
+	default:
+		ctx.Data["SortType"] = "recentupdate"
+		orderBy = models.SearchOrderByRecentUpdated
+	}
+
+	keyword := strings.Trim(ctx.Query("q"), " ")
+	topicOnly := ctx.QueryBool("topic")
+	ctx.Data["TopicOnly"] = topicOnly
+
+	repos, count, err = models.SearchRepository(&models.SearchRepoOptions{
+		ListOptions: models.ListOptions{
+			Page:     page,
+			PageSize: opts.PageSize,
+		},
+		Actor:              ctx.User,
+		OrderBy:            orderBy,
+		Private:            opts.Private,
+		Keyword:            keyword,
+		OwnerID:            opts.OwnerID,
+		AllPublic:          true,
+		AllLimited:         true,
+		TopicOnly:          topicOnly,
+		IncludeDescription: setting.UI.SearchRepoDescription,
+	})
+	if err != nil {
+		ctx.ServerError("SearchRepository", err)
+		return
+	}
+	ctx.Data["Keyword"] = keyword
+	ctx.Data["Total"] = count
+	ctx.Data["Repos"] = repos
+	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+
+	pager := context.NewPagination(int(count), opts.PageSize, page, 5)
+	pager.SetDefaultParams(ctx)
+	pager.AddParam(ctx, "topic", "TopicOnly")
+	ctx.Data["Page"] = pager
+
+	ctx.HTML(http.StatusOK, opts.TplName)
+}
+
+// Repos render explore repositories page
+func Repos(ctx *context.Context) {
+	ctx.Data["UsersIsDisabled"] = setting.Service.Explore.DisableUsersPage
+	ctx.Data["Title"] = ctx.Tr("explore")
+	ctx.Data["PageIsExplore"] = true
+	ctx.Data["PageIsExploreRepositories"] = true
+	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+
+	var ownerID int64
+	if ctx.User != nil && !ctx.User.IsAdmin {
+		ownerID = ctx.User.ID
+	}
+
+	RenderRepoSearch(ctx, &RepoSearchOptions{
+		PageSize: setting.UI.ExplorePagingNum,
+		OwnerID:  ownerID,
+		Private:  ctx.User != nil,
+		TplName:  tplExploreRepos,
+	})
+}
diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go
new file mode 100644
index 0000000000000..52f543fe6696a
--- /dev/null
+++ b/routers/web/explore/user.go
@@ -0,0 +1,107 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package explore
+
+import (
+	"bytes"
+	"net/http"
+	"strings"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/util"
+)
+
+const (
+	// tplExploreUsers explore users page template
+	tplExploreUsers base.TplName = "explore/users"
+)
+
+var (
+	nullByte = []byte{0x00}
+)
+
+func isKeywordValid(keyword string) bool {
+	return !bytes.Contains([]byte(keyword), nullByte)
+}
+
+// RenderUserSearch render user search page
+func RenderUserSearch(ctx *context.Context, opts *models.SearchUserOptions, tplName base.TplName) {
+	opts.Page = ctx.QueryInt("page")
+	if opts.Page <= 1 {
+		opts.Page = 1
+	}
+
+	var (
+		users   []*models.User
+		count   int64
+		err     error
+		orderBy models.SearchOrderBy
+	)
+
+	ctx.Data["SortType"] = ctx.Query("sort")
+	switch ctx.Query("sort") {
+	case "newest":
+		orderBy = models.SearchOrderByIDReverse
+	case "oldest":
+		orderBy = models.SearchOrderByID
+	case "recentupdate":
+		orderBy = models.SearchOrderByRecentUpdated
+	case "leastupdate":
+		orderBy = models.SearchOrderByLeastUpdated
+	case "reversealphabetically":
+		orderBy = models.SearchOrderByAlphabeticallyReverse
+	case "alphabetically":
+		orderBy = models.SearchOrderByAlphabetically
+	default:
+		ctx.Data["SortType"] = "alphabetically"
+		orderBy = models.SearchOrderByAlphabetically
+	}
+
+	opts.Keyword = strings.Trim(ctx.Query("q"), " ")
+	opts.OrderBy = orderBy
+	if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
+		users, count, err = models.SearchUsers(opts)
+		if err != nil {
+			ctx.ServerError("SearchUsers", err)
+			return
+		}
+	}
+	ctx.Data["Keyword"] = opts.Keyword
+	ctx.Data["Total"] = count
+	ctx.Data["Users"] = users
+	ctx.Data["UsersTwoFaStatus"] = models.UserList(users).GetTwoFaStatus()
+	ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail
+	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+
+	pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
+	pager.SetDefaultParams(ctx)
+	ctx.Data["Page"] = pager
+
+	ctx.HTML(http.StatusOK, tplName)
+}
+
+// Users render explore users page
+func Users(ctx *context.Context) {
+	if setting.Service.Explore.DisableUsersPage {
+		ctx.Redirect(setting.AppSubURL + "/explore/repos")
+		return
+	}
+	ctx.Data["Title"] = ctx.Tr("explore")
+	ctx.Data["PageIsExplore"] = true
+	ctx.Data["PageIsExploreUsers"] = true
+	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+
+	RenderUserSearch(ctx, &models.SearchUserOptions{
+		Actor:       ctx.User,
+		Type:        models.UserTypeIndividual,
+		ListOptions: models.ListOptions{PageSize: setting.UI.ExplorePagingNum},
+		IsActive:    util.OptionalBoolTrue,
+		Visible:     []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
+	}, tplExploreUsers)
+}
diff --git a/routers/routes/goget.go b/routers/web/goget.go
similarity index 99%
rename from routers/routes/goget.go
rename to routers/web/goget.go
index 518f5e3073402..77934e7f55efc 100644
--- a/routers/routes/goget.go
+++ b/routers/web/goget.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package routes
+package web
 
 import (
 	"net/http"
diff --git a/routers/web/home.go b/routers/web/home.go
new file mode 100644
index 0000000000000..f50197691ffdd
--- /dev/null
+++ b/routers/web/home.go
@@ -0,0 +1,65 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package web
+
+import (
+	"net/http"
+
+	"code.gitea.io/gitea/modules/base"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/setting"
+	"code.gitea.io/gitea/modules/web/middleware"
+	"code.gitea.io/gitea/routers/web/user"
+)
+
+const (
+	// tplHome home page template
+	tplHome base.TplName = "home"
+)
+
+// Home render home page
+func Home(ctx *context.Context) {
+	if ctx.IsSigned {
+		if !ctx.User.IsActive && setting.Service.RegisterEmailConfirm {
+			ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
+			ctx.HTML(http.StatusOK, user.TplActivate)
+		} else if !ctx.User.IsActive || ctx.User.ProhibitLogin {
+			log.Info("Failed authentication attempt for %s from %s", ctx.User.Name, ctx.RemoteAddr())
+			ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
+			ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
+		} else if ctx.User.MustChangePassword {
+			ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
+			ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password"
+			middleware.SetRedirectToCookie(ctx.Resp, setting.AppSubURL+ctx.Req.URL.RequestURI())
+			ctx.Redirect(setting.AppSubURL + "/user/settings/change_password")
+		} else {
+			user.Dashboard(ctx)
+		}
+		return
+		// Check non-logged users landing page.
+	} else if setting.LandingPageURL != setting.LandingPageHome {
+		ctx.Redirect(setting.AppSubURL + string(setting.LandingPageURL))
+		return
+	}
+
+	// Check auto-login.
+	uname := ctx.GetCookie(setting.CookieUserName)
+	if len(uname) != 0 {
+		ctx.Redirect(setting.AppSubURL + "/user/login")
+		return
+	}
+
+	ctx.Data["PageIsHome"] = true
+	ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+	ctx.HTML(http.StatusOK, tplHome)
+}
+
+// NotFound render 404 page
+func NotFound(ctx *context.Context) {
+	ctx.Data["Title"] = "Page Not Found"
+	ctx.NotFound("home.NotFound", nil)
+}
diff --git a/routers/metrics.go b/routers/web/metrics.go
similarity index 98%
rename from routers/metrics.go
rename to routers/web/metrics.go
index db2fb8de44386..37558ee337646 100644
--- a/routers/metrics.go
+++ b/routers/web/metrics.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package routers
+package web
 
 import (
 	"crypto/subtle"
diff --git a/routers/org/home.go b/routers/web/org/home.go
similarity index 100%
rename from routers/org/home.go
rename to routers/web/org/home.go
diff --git a/routers/org/members.go b/routers/web/org/members.go
similarity index 100%
rename from routers/org/members.go
rename to routers/web/org/members.go
diff --git a/routers/org/org.go b/routers/web/org/org.go
similarity index 100%
rename from routers/org/org.go
rename to routers/web/org/org.go
diff --git a/routers/org/org_labels.go b/routers/web/org/org_labels.go
similarity index 100%
rename from routers/org/org_labels.go
rename to routers/web/org/org_labels.go
diff --git a/routers/org/setting.go b/routers/web/org/setting.go
similarity index 99%
rename from routers/org/setting.go
rename to routers/web/org/setting.go
index 0e28a93acef95..aed90c66f7455 100644
--- a/routers/org/setting.go
+++ b/routers/web/org/setting.go
@@ -15,7 +15,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
-	userSetting "code.gitea.io/gitea/routers/user/setting"
+	userSetting "code.gitea.io/gitea/routers/web/user/setting"
 	"code.gitea.io/gitea/services/forms"
 )
 
diff --git a/routers/org/teams.go b/routers/web/org/teams.go
similarity index 100%
rename from routers/org/teams.go
rename to routers/web/org/teams.go
diff --git a/routers/repo/activity.go b/routers/web/repo/activity.go
similarity index 100%
rename from routers/repo/activity.go
rename to routers/web/repo/activity.go
diff --git a/routers/repo/attachment.go b/routers/web/repo/attachment.go
similarity index 97%
rename from routers/repo/attachment.go
rename to routers/web/repo/attachment.go
index f53e7450ae028..5becbea2713a1 100644
--- a/routers/repo/attachment.go
+++ b/routers/web/repo/attachment.go
@@ -15,6 +15,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/storage"
 	"code.gitea.io/gitea/modules/upload"
+	"code.gitea.io/gitea/routers/common"
 )
 
 // UploadIssueAttachment response for Issue/PR attachments
@@ -152,7 +153,7 @@ func GetAttachment(ctx *context.Context) {
 	}
 	defer fr.Close()
 
-	if err = ServeData(ctx, attach.Name, attach.Size, fr); err != nil {
+	if err = common.ServeData(ctx, attach.Name, attach.Size, fr); err != nil {
 		ctx.ServerError("ServeData", err)
 		return
 	}
diff --git a/routers/repo/blame.go b/routers/web/repo/blame.go
similarity index 100%
rename from routers/repo/blame.go
rename to routers/web/repo/blame.go
diff --git a/routers/repo/branch.go b/routers/web/repo/branch.go
similarity index 100%
rename from routers/repo/branch.go
rename to routers/web/repo/branch.go
diff --git a/routers/repo/commit.go b/routers/web/repo/commit.go
similarity index 100%
rename from routers/repo/commit.go
rename to routers/web/repo/commit.go
diff --git a/routers/repo/compare.go b/routers/web/repo/compare.go
similarity index 100%
rename from routers/repo/compare.go
rename to routers/web/repo/compare.go
diff --git a/routers/repo/download.go b/routers/web/repo/download.go
similarity index 52%
rename from routers/repo/download.go
rename to routers/web/repo/download.go
index bbf4684b2ec7b..6f43d4b839198 100644
--- a/routers/repo/download.go
+++ b/routers/web/repo/download.go
@@ -6,102 +6,14 @@
 package repo
 
 import (
-	"fmt"
-	"io"
-	"path"
-	"path/filepath"
-	"strings"
-
-	"code.gitea.io/gitea/modules/charset"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/httpcache"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/setting"
-	"code.gitea.io/gitea/modules/typesniffer"
+	"code.gitea.io/gitea/routers/common"
 )
 
-// ServeData download file from io.Reader
-func ServeData(ctx *context.Context, name string, size int64, reader io.Reader) error {
-	buf := make([]byte, 1024)
-	n, err := reader.Read(buf)
-	if err != nil && err != io.EOF {
-		return err
-	}
-	if n >= 0 {
-		buf = buf[:n]
-	}
-
-	ctx.Resp.Header().Set("Cache-Control", "public,max-age=86400")
-
-	if size >= 0 {
-		ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size))
-	} else {
-		log.Error("ServeData called to serve data: %s with size < 0: %d", name, size)
-	}
-	name = path.Base(name)
-
-	// Google Chrome dislike commas in filenames, so let's change it to a space
-	name = strings.ReplaceAll(name, ",", " ")
-
-	st := typesniffer.DetectContentType(buf)
-
-	if st.IsText() || ctx.QueryBool("render") {
-		cs, err := charset.DetectEncoding(buf)
-		if err != nil {
-			log.Error("Detect raw file %s charset failed: %v, using by default utf-8", name, err)
-			cs = "utf-8"
-		}
-		ctx.Resp.Header().Set("Content-Type", "text/plain; charset="+strings.ToLower(cs))
-	} else {
-		ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
-
-		if (st.IsImage() || st.IsPDF()) && (setting.UI.SVG.Enabled || !st.IsSvgImage()) {
-			ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, name))
-			if st.IsSvgImage() {
-				ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
-				ctx.Resp.Header().Set("X-Content-Type-Options", "nosniff")
-				ctx.Resp.Header().Set("Content-Type", typesniffer.SvgMimeType)
-			}
-		} else {
-			ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name))
-			if setting.MimeTypeMap.Enabled {
-				fileExtension := strings.ToLower(filepath.Ext(name))
-				if mimetype, ok := setting.MimeTypeMap.Map[fileExtension]; ok {
-					ctx.Resp.Header().Set("Content-Type", mimetype)
-				}
-			}
-		}
-	}
-
-	_, err = ctx.Resp.Write(buf)
-	if err != nil {
-		return err
-	}
-	_, err = io.Copy(ctx.Resp, reader)
-	return err
-}
-
-// ServeBlob download a git.Blob
-func ServeBlob(ctx *context.Context, blob *git.Blob) error {
-	if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`) {
-		return nil
-	}
-
-	dataRc, err := blob.DataAsync()
-	if err != nil {
-		return err
-	}
-	defer func() {
-		if err = dataRc.Close(); err != nil {
-			log.Error("ServeBlob: Close: %v", err)
-		}
-	}()
-
-	return ServeData(ctx, ctx.Repo.TreePath, blob.Size(), dataRc)
-}
-
 // ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
 func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob) error {
 	if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`) {
@@ -130,7 +42,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob) error {
 				log.Error("ServeBlobOrLFS: Close: %v", err)
 			}
 			closed = true
-			return ServeBlob(ctx, blob)
+			return common.ServeBlob(ctx, blob)
 		}
 		if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
 			return nil
@@ -144,14 +56,14 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob) error {
 				log.Error("ServeBlobOrLFS: Close: %v", err)
 			}
 		}()
-		return ServeData(ctx, ctx.Repo.TreePath, meta.Size, lfsDataRc)
+		return common.ServeData(ctx, ctx.Repo.TreePath, meta.Size, lfsDataRc)
 	}
 	if err = dataRc.Close(); err != nil {
 		log.Error("ServeBlobOrLFS: Close: %v", err)
 	}
 	closed = true
 
-	return ServeBlob(ctx, blob)
+	return common.ServeBlob(ctx, blob)
 }
 
 // SingleDownload download a file by repos path
@@ -165,7 +77,7 @@ func SingleDownload(ctx *context.Context) {
 		}
 		return
 	}
-	if err = ServeBlob(ctx, blob); err != nil {
+	if err = common.ServeBlob(ctx, blob); err != nil {
 		ctx.ServerError("ServeBlob", err)
 	}
 }
@@ -197,7 +109,7 @@ func DownloadByID(ctx *context.Context) {
 		}
 		return
 	}
-	if err = ServeBlob(ctx, blob); err != nil {
+	if err = common.ServeBlob(ctx, blob); err != nil {
 		ctx.ServerError("ServeBlob", err)
 	}
 }
diff --git a/routers/repo/editor.go b/routers/web/repo/editor.go
similarity index 100%
rename from routers/repo/editor.go
rename to routers/web/repo/editor.go
diff --git a/routers/repo/editor_test.go b/routers/web/repo/editor_test.go
similarity index 100%
rename from routers/repo/editor_test.go
rename to routers/web/repo/editor_test.go
diff --git a/routers/repo/http.go b/routers/web/repo/http.go
similarity index 100%
rename from routers/repo/http.go
rename to routers/web/repo/http.go
diff --git a/routers/repo/issue.go b/routers/web/repo/issue.go
similarity index 100%
rename from routers/repo/issue.go
rename to routers/web/repo/issue.go
diff --git a/routers/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go
similarity index 100%
rename from routers/repo/issue_dependency.go
rename to routers/web/repo/issue_dependency.go
diff --git a/routers/repo/issue_label.go b/routers/web/repo/issue_label.go
similarity index 100%
rename from routers/repo/issue_label.go
rename to routers/web/repo/issue_label.go
diff --git a/routers/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go
similarity index 100%
rename from routers/repo/issue_label_test.go
rename to routers/web/repo/issue_label_test.go
diff --git a/routers/repo/issue_lock.go b/routers/web/repo/issue_lock.go
similarity index 100%
rename from routers/repo/issue_lock.go
rename to routers/web/repo/issue_lock.go
diff --git a/routers/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go
similarity index 100%
rename from routers/repo/issue_stopwatch.go
rename to routers/web/repo/issue_stopwatch.go
diff --git a/routers/repo/issue_test.go b/routers/web/repo/issue_test.go
similarity index 100%
rename from routers/repo/issue_test.go
rename to routers/web/repo/issue_test.go
diff --git a/routers/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go
similarity index 100%
rename from routers/repo/issue_timetrack.go
rename to routers/web/repo/issue_timetrack.go
diff --git a/routers/repo/issue_watch.go b/routers/web/repo/issue_watch.go
similarity index 100%
rename from routers/repo/issue_watch.go
rename to routers/web/repo/issue_watch.go
diff --git a/routers/repo/lfs.go b/routers/web/repo/lfs.go
similarity index 100%
rename from routers/repo/lfs.go
rename to routers/web/repo/lfs.go
diff --git a/routers/repo/main_test.go b/routers/web/repo/main_test.go
similarity index 84%
rename from routers/repo/main_test.go
rename to routers/web/repo/main_test.go
index 04bbeeb211780..47f266365fd7f 100644
--- a/routers/repo/main_test.go
+++ b/routers/web/repo/main_test.go
@@ -12,5 +12,5 @@ import (
 )
 
 func TestMain(m *testing.M) {
-	models.MainTest(m, filepath.Join("..", ".."))
+	models.MainTest(m, filepath.Join("..", "..", ".."))
 }
diff --git a/routers/repo/middlewares.go b/routers/web/repo/middlewares.go
similarity index 100%
rename from routers/repo/middlewares.go
rename to routers/web/repo/middlewares.go
diff --git a/routers/repo/migrate.go b/routers/web/repo/migrate.go
similarity index 100%
rename from routers/repo/migrate.go
rename to routers/web/repo/migrate.go
diff --git a/routers/repo/milestone.go b/routers/web/repo/milestone.go
similarity index 100%
rename from routers/repo/milestone.go
rename to routers/web/repo/milestone.go
diff --git a/routers/repo/projects.go b/routers/web/repo/projects.go
similarity index 100%
rename from routers/repo/projects.go
rename to routers/web/repo/projects.go
diff --git a/routers/repo/projects_test.go b/routers/web/repo/projects_test.go
similarity index 100%
rename from routers/repo/projects_test.go
rename to routers/web/repo/projects_test.go
diff --git a/routers/repo/pull.go b/routers/web/repo/pull.go
similarity index 100%
rename from routers/repo/pull.go
rename to routers/web/repo/pull.go
diff --git a/routers/repo/pull_review.go b/routers/web/repo/pull_review.go
similarity index 100%
rename from routers/repo/pull_review.go
rename to routers/web/repo/pull_review.go
diff --git a/routers/repo/release.go b/routers/web/repo/release.go
similarity index 100%
rename from routers/repo/release.go
rename to routers/web/repo/release.go
diff --git a/routers/repo/release_test.go b/routers/web/repo/release_test.go
similarity index 100%
rename from routers/repo/release_test.go
rename to routers/web/repo/release_test.go
diff --git a/routers/repo/repo.go b/routers/web/repo/repo.go
similarity index 95%
rename from routers/repo/repo.go
rename to routers/web/repo/repo.go
index 69471a83d398b..f149e92a8b6b5 100644
--- a/routers/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -364,30 +364,6 @@ func RedirectDownload(ctx *context.Context) {
 	ctx.Error(http.StatusNotFound)
 }
 
-// Download an archive of a repository
-func Download(ctx *context.Context) {
-	uri := ctx.Params("*")
-	aReq := archiver_service.DeriveRequestFrom(ctx, uri)
-
-	if aReq == nil {
-		ctx.Error(http.StatusNotFound)
-		return
-	}
-
-	downloadName := ctx.Repo.Repository.Name + "-" + aReq.GetArchiveName()
-	complete := aReq.IsComplete()
-	if !complete {
-		aReq = archiver_service.ArchiveRepository(aReq)
-		complete = aReq.WaitForCompletion(ctx)
-	}
-
-	if complete {
-		ctx.ServeFile(aReq.GetArchivePath(), downloadName)
-	} else {
-		ctx.Error(http.StatusNotFound)
-	}
-}
-
 // InitiateDownload will enqueue an archival request, as needed.  It may submit
 // a request that's already in-progress, but the archiver service will just
 // kind of drop it on the floor if this is the case.
diff --git a/routers/repo/search.go b/routers/web/repo/search.go
similarity index 100%
rename from routers/repo/search.go
rename to routers/web/repo/search.go
diff --git a/routers/repo/setting.go b/routers/web/repo/setting.go
similarity index 100%
rename from routers/repo/setting.go
rename to routers/web/repo/setting.go
diff --git a/routers/repo/setting_protected_branch.go b/routers/web/repo/setting_protected_branch.go
similarity index 100%
rename from routers/repo/setting_protected_branch.go
rename to routers/web/repo/setting_protected_branch.go
diff --git a/routers/repo/settings_test.go b/routers/web/repo/settings_test.go
similarity index 100%
rename from routers/repo/settings_test.go
rename to routers/web/repo/settings_test.go
diff --git a/routers/repo/topic.go b/routers/web/repo/topic.go
similarity index 100%
rename from routers/repo/topic.go
rename to routers/web/repo/topic.go
diff --git a/routers/repo/view.go b/routers/web/repo/view.go
similarity index 100%
rename from routers/repo/view.go
rename to routers/web/repo/view.go
diff --git a/routers/repo/webhook.go b/routers/web/repo/webhook.go
similarity index 100%
rename from routers/repo/webhook.go
rename to routers/web/repo/webhook.go
diff --git a/routers/repo/wiki.go b/routers/web/repo/wiki.go
similarity index 99%
rename from routers/repo/wiki.go
rename to routers/web/repo/wiki.go
index 1bdd06dce57d7..cceb8451e58f5 100644
--- a/routers/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -24,6 +24,7 @@ import (
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	"code.gitea.io/gitea/routers/common"
 	"code.gitea.io/gitea/services/forms"
 	wiki_service "code.gitea.io/gitea/services/wiki"
 )
@@ -558,7 +559,7 @@ func WikiRaw(ctx *context.Context) {
 	}
 
 	if entry != nil {
-		if err = ServeBlob(ctx, entry.Blob()); err != nil {
+		if err = common.ServeBlob(ctx, entry.Blob()); err != nil {
 			ctx.ServerError("ServeBlob", err)
 		}
 		return
diff --git a/routers/repo/wiki_test.go b/routers/web/repo/wiki_test.go
similarity index 100%
rename from routers/repo/wiki_test.go
rename to routers/web/repo/wiki_test.go
diff --git a/routers/swagger_json.go b/routers/web/swagger_json.go
similarity index 97%
rename from routers/swagger_json.go
rename to routers/web/swagger_json.go
index 78c7fb1e24b33..82d72698c606c 100644
--- a/routers/swagger_json.go
+++ b/routers/web/swagger_json.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package routers
+package web
 
 import (
 	"net/http"
diff --git a/routers/user/auth.go b/routers/web/user/auth.go
similarity index 100%
rename from routers/user/auth.go
rename to routers/web/user/auth.go
diff --git a/routers/user/auth_openid.go b/routers/web/user/auth_openid.go
similarity index 100%
rename from routers/user/auth_openid.go
rename to routers/web/user/auth_openid.go
diff --git a/routers/user/avatar.go b/routers/web/user/avatar.go
similarity index 100%
rename from routers/user/avatar.go
rename to routers/web/user/avatar.go
diff --git a/routers/user/home.go b/routers/web/user/home.go
similarity index 100%
rename from routers/user/home.go
rename to routers/web/user/home.go
diff --git a/routers/user/home_test.go b/routers/web/user/home_test.go
similarity index 100%
rename from routers/user/home_test.go
rename to routers/web/user/home_test.go
diff --git a/routers/user/main_test.go b/routers/web/user/main_test.go
similarity index 84%
rename from routers/user/main_test.go
rename to routers/web/user/main_test.go
index ed0724dc7733e..be17dd1f3135f 100644
--- a/routers/user/main_test.go
+++ b/routers/web/user/main_test.go
@@ -12,5 +12,5 @@ import (
 )
 
 func TestMain(m *testing.M) {
-	models.MainTest(m, filepath.Join("..", ".."))
+	models.MainTest(m, filepath.Join("..", "..", ".."))
 }
diff --git a/routers/user/notification.go b/routers/web/user/notification.go
similarity index 100%
rename from routers/user/notification.go
rename to routers/web/user/notification.go
diff --git a/routers/user/oauth.go b/routers/web/user/oauth.go
similarity index 100%
rename from routers/user/oauth.go
rename to routers/web/user/oauth.go
diff --git a/routers/user/profile.go b/routers/web/user/profile.go
similarity index 99%
rename from routers/user/profile.go
rename to routers/web/user/profile.go
index 8ff1ee24adc80..e66820e1317bc 100644
--- a/routers/user/profile.go
+++ b/routers/web/user/profile.go
@@ -17,7 +17,7 @@ import (
 	"code.gitea.io/gitea/modules/markup/markdown"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
-	"code.gitea.io/gitea/routers/org"
+	"code.gitea.io/gitea/routers/web/org"
 )
 
 // GetUserByName get user by name
diff --git a/routers/user/setting/account.go b/routers/web/user/setting/account.go
similarity index 100%
rename from routers/user/setting/account.go
rename to routers/web/user/setting/account.go
diff --git a/routers/user/setting/account_test.go b/routers/web/user/setting/account_test.go
similarity index 100%
rename from routers/user/setting/account_test.go
rename to routers/web/user/setting/account_test.go
diff --git a/routers/user/setting/adopt.go b/routers/web/user/setting/adopt.go
similarity index 100%
rename from routers/user/setting/adopt.go
rename to routers/web/user/setting/adopt.go
diff --git a/routers/user/setting/applications.go b/routers/web/user/setting/applications.go
similarity index 100%
rename from routers/user/setting/applications.go
rename to routers/web/user/setting/applications.go
diff --git a/routers/user/setting/keys.go b/routers/web/user/setting/keys.go
similarity index 100%
rename from routers/user/setting/keys.go
rename to routers/web/user/setting/keys.go
diff --git a/routers/admin/main_test.go b/routers/web/user/setting/main_test.go
similarity index 78%
rename from routers/admin/main_test.go
rename to routers/web/user/setting/main_test.go
index 9a7191d471f54..daa3f7fe5bf16 100644
--- a/routers/admin/main_test.go
+++ b/routers/web/user/setting/main_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package admin
+package setting
 
 import (
 	"path/filepath"
@@ -12,5 +12,5 @@ import (
 )
 
 func TestMain(m *testing.M) {
-	models.MainTest(m, filepath.Join("..", ".."))
+	models.MainTest(m, filepath.Join("..", "..", "..", ".."))
 }
diff --git a/routers/user/setting/oauth2.go b/routers/web/user/setting/oauth2.go
similarity index 100%
rename from routers/user/setting/oauth2.go
rename to routers/web/user/setting/oauth2.go
diff --git a/routers/user/setting/profile.go b/routers/web/user/setting/profile.go
similarity index 100%
rename from routers/user/setting/profile.go
rename to routers/web/user/setting/profile.go
diff --git a/routers/user/setting/security.go b/routers/web/user/setting/security.go
similarity index 100%
rename from routers/user/setting/security.go
rename to routers/web/user/setting/security.go
diff --git a/routers/user/setting/security_openid.go b/routers/web/user/setting/security_openid.go
similarity index 100%
rename from routers/user/setting/security_openid.go
rename to routers/web/user/setting/security_openid.go
diff --git a/routers/user/setting/security_twofa.go b/routers/web/user/setting/security_twofa.go
similarity index 100%
rename from routers/user/setting/security_twofa.go
rename to routers/web/user/setting/security_twofa.go
diff --git a/routers/user/setting/security_u2f.go b/routers/web/user/setting/security_u2f.go
similarity index 100%
rename from routers/user/setting/security_u2f.go
rename to routers/web/user/setting/security_u2f.go
diff --git a/routers/user/task.go b/routers/web/user/task.go
similarity index 100%
rename from routers/user/task.go
rename to routers/web/user/task.go
diff --git a/routers/routes/web.go b/routers/web/web.go
similarity index 92%
rename from routers/routes/web.go
rename to routers/web/web.go
index fbc41d547d163..6c0141eef30a7 100644
--- a/routers/routes/web.go
+++ b/routers/web/web.go
@@ -2,15 +2,13 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package routes
+package web
 
 import (
 	"encoding/gob"
-	"fmt"
 	"net/http"
 	"os"
 	"path"
-	"strings"
 
 	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/context"
@@ -23,17 +21,16 @@ import (
 	"code.gitea.io/gitea/modules/templates"
 	"code.gitea.io/gitea/modules/validation"
 	"code.gitea.io/gitea/modules/web"
-	"code.gitea.io/gitea/routers"
-	"code.gitea.io/gitea/routers/admin"
-	apiv1 "code.gitea.io/gitea/routers/api/v1"
 	"code.gitea.io/gitea/routers/api/v1/misc"
-	"code.gitea.io/gitea/routers/dev"
-	"code.gitea.io/gitea/routers/events"
-	"code.gitea.io/gitea/routers/org"
-	"code.gitea.io/gitea/routers/private"
-	"code.gitea.io/gitea/routers/repo"
-	"code.gitea.io/gitea/routers/user"
-	userSetting "code.gitea.io/gitea/routers/user/setting"
+	"code.gitea.io/gitea/routers/common"
+	"code.gitea.io/gitea/routers/web/admin"
+	"code.gitea.io/gitea/routers/web/dev"
+	"code.gitea.io/gitea/routers/web/events"
+	"code.gitea.io/gitea/routers/web/explore"
+	"code.gitea.io/gitea/routers/web/org"
+	"code.gitea.io/gitea/routers/web/repo"
+	"code.gitea.io/gitea/routers/web/user"
+	userSetting "code.gitea.io/gitea/routers/web/user/setting"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/lfs"
 	"code.gitea.io/gitea/services/mailer"
@@ -44,7 +41,6 @@ import (
 	"gitea.com/go-chi/captcha"
 	"gitea.com/go-chi/session"
 	"github.com/NYTimes/gziphandler"
-	"github.com/chi-middleware/proxy"
 	"github.com/go-chi/chi/middleware"
 	"github.com/go-chi/cors"
 	"github.com/prometheus/client_golang/prometheus"
@@ -56,74 +52,10 @@ const (
 	GzipMinSize = 1400
 )
 
-func commonMiddlewares() []func(http.Handler) http.Handler {
-	var handlers = []func(http.Handler) http.Handler{
-		func(next http.Handler) http.Handler {
-			return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
-				next.ServeHTTP(context.NewResponse(resp), req)
-			})
-		},
-	}
-
-	if setting.ReverseProxyLimit > 0 {
-		opt := proxy.NewForwardedHeadersOptions().
-			WithForwardLimit(setting.ReverseProxyLimit).
-			ClearTrustedProxies()
-		for _, n := range setting.ReverseProxyTrustedProxies {
-			if !strings.Contains(n, "/") {
-				opt.AddTrustedProxy(n)
-			} else {
-				opt.AddTrustedNetwork(n)
-			}
-		}
-		handlers = append(handlers, proxy.ForwardedHeaders(opt))
-	}
-
-	handlers = append(handlers, middleware.StripSlashes)
-
-	if !setting.DisableRouterLog && setting.RouterLogLevel != log.NONE {
-		if log.GetLogger("router").GetLevel() <= setting.RouterLogLevel {
-			handlers = append(handlers, LoggerHandler(setting.RouterLogLevel))
-		}
-	}
-	if setting.EnableAccessLog {
-		handlers = append(handlers, context.AccessLogger())
-	}
-
-	handlers = append(handlers, func(next http.Handler) http.Handler {
-		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
-			// Why we need this? The Recovery() will try to render a beautiful
-			// error page for user, but the process can still panic again, and other
-			// middleware like session also may panic then we have to recover twice
-			// and send a simple error page that should not panic any more.
-			defer func() {
-				if err := recover(); err != nil {
-					combinedErr := fmt.Sprintf("PANIC: %v\n%s", err, string(log.Stack(2)))
-					log.Error("%v", combinedErr)
-					if setting.IsProd() {
-						http.Error(resp, http.StatusText(500), 500)
-					} else {
-						http.Error(resp, combinedErr, 500)
-					}
-				}
-			}()
-			next.ServeHTTP(resp, req)
-		})
-	})
-	return handlers
-}
-
-var corsHandler func(http.Handler) http.Handler
-
-// NormalRoutes represents non install routes
-func NormalRoutes() *web.Route {
-	r := web.NewRoute()
-	for _, middle := range commonMiddlewares() {
-		r.Use(middle)
-	}
-
+// CorsHandler return a http handler who set CORS options if enabled by config
+func CorsHandler() func(next http.Handler) http.Handler {
 	if setting.CORSConfig.Enabled {
-		corsHandler = cors.Handler(cors.Options{
+		return cors.Handler(cors.Options{
 			//Scheme:           setting.CORSConfig.Scheme, // FIXME: the cors middleware needs scheme option
 			AllowedOrigins: setting.CORSConfig.AllowDomain,
 			//setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option
@@ -131,26 +63,21 @@ func NormalRoutes() *web.Route {
 			AllowCredentials: setting.CORSConfig.AllowCredentials,
 			MaxAge:           int(setting.CORSConfig.MaxAge.Seconds()),
 		})
-	} else {
-		corsHandler = func(next http.Handler) http.Handler {
-			return next
-		}
 	}
 
-	r.Mount("/", WebRoutes())
-	r.Mount("/api/v1", apiv1.Routes())
-	r.Mount("/api/internal", private.Routes())
-	return r
+	return func(next http.Handler) http.Handler {
+		return next
+	}
 }
 
-// WebRoutes returns all web routes
-func WebRoutes() *web.Route {
+// Routes returns all web routes
+func Routes() *web.Route {
 	routes := web.NewRoute()
 
 	routes.Use(public.AssetsHandler(&public.Options{
 		Directory:   path.Join(setting.StaticRootPath, "public"),
 		Prefix:      "/assets",
-		CorsHandler: corsHandler,
+		CorsHandler: CorsHandler(),
 	}))
 
 	routes.Use(session.Sessioner(session.Options{
@@ -216,7 +143,7 @@ func WebRoutes() *web.Route {
 		c := metrics.NewCollector()
 		prometheus.MustRegister(c)
 
-		routes.Get("/metrics", append(common, routers.Metrics)...)
+		routes.Get("/metrics", append(common, Metrics)...)
 	}
 
 	// Removed: toolbox.Toolboxer middleware will provide debug informations which seems unnecessary
@@ -297,16 +224,16 @@ func RegisterRoutes(m *web.Route) {
 	// Especially some AJAX requests, we can reduce middleware number to improve performance.
 	// Routers.
 	// for health check
-	m.Get("/", routers.Home)
+	m.Get("/", Home)
 	m.Get("/.well-known/openid-configuration", user.OIDCWellKnown)
 	m.Group("/explore", func() {
 		m.Get("", func(ctx *context.Context) {
 			ctx.Redirect(setting.AppSubURL + "/explore/repos")
 		})
-		m.Get("/repos", routers.ExploreRepos)
-		m.Get("/users", routers.ExploreUsers)
-		m.Get("/organizations", routers.ExploreOrganizations)
-		m.Get("/code", routers.ExploreCode)
+		m.Get("/repos", explore.Repos)
+		m.Get("/users", explore.Users)
+		m.Get("/organizations", explore.Organizations)
+		m.Get("/code", explore.Code)
 	}, ignExploreSignIn)
 	m.Get("/issues", reqSignIn, user.Issues)
 	m.Get("/pulls", reqSignIn, user.Pulls)
@@ -363,7 +290,7 @@ func RegisterRoutes(m *web.Route) {
 		m.Post("/authorize", bindIgnErr(forms.AuthorizationForm{}), user.AuthorizeOAuth)
 	}, ignSignInAndCsrf, reqSignIn)
 	m.Get("/login/oauth/userinfo", ignSignInAndCsrf, user.InfoOAuth)
-	m.Post("/login/oauth/access_token", corsHandler, bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth)
+	m.Post("/login/oauth/access_token", CorsHandler(), bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth)
 
 	m.Group("/user/settings", func() {
 		m.Get("", userSetting.Profile)
@@ -956,7 +883,7 @@ func RegisterRoutes(m *web.Route) {
 		}, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(models.UnitTypeCode))
 
 		m.Group("/archive", func() {
-			m.Get("/*", repo.Download)
+			m.Get("/*", common.Download)
 			m.Post("/*", repo.InitiateDownload)
 		}, repo.MustBeNotEmpty, reqRepoCodeReader)
 
@@ -1091,9 +1018,6 @@ func RegisterRoutes(m *web.Route) {
 	}, reqSignIn)
 
 	if setting.API.EnableSwagger {
-		m.Get("/swagger.v1.json", routers.SwaggerV1Json)
+		m.Get("/swagger.v1.json", SwaggerV1Json)
 	}
-
-	// Not found handler.
-	m.NotFound(web.Wrap(routers.NotFound))
 }