diff --git a/models/packages/arch/search.go b/models/packages/arch/search.go
new file mode 100644
index 0000000000000..f35c037b23a58
--- /dev/null
+++ b/models/packages/arch/search.go
@@ -0,0 +1,38 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package arch
+
+import (
+	"context"
+
+	packages_model "code.gitea.io/gitea/models/packages"
+	arch_module "code.gitea.io/gitea/modules/packages/arch"
+)
+
+// GetRepositories gets all available repositories
+func GetRepositories(ctx context.Context, ownerID int64) ([]string, error) {
+	return packages_model.GetDistinctPropertyValues(
+		ctx,
+		packages_model.TypeArch,
+		ownerID,
+		packages_model.PropertyTypeFile,
+		arch_module.PropertyRepository,
+		nil,
+	)
+}
+
+// GetArchitectures gets all available architectures for the given repository
+func GetArchitectures(ctx context.Context, ownerID int64, repository string) ([]string, error) {
+	return packages_model.GetDistinctPropertyValues(
+		ctx,
+		packages_model.TypeArch,
+		ownerID,
+		packages_model.PropertyTypeFile,
+		arch_module.PropertyArchitecture,
+		&packages_model.DistinctPropertyDependency{
+			Name:  arch_module.PropertyRepository,
+			Value: repository,
+		},
+	)
+}
diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go
index b8ef698d3822a..803b73c968995 100644
--- a/models/packages/descriptor.go
+++ b/models/packages/descriptor.go
@@ -13,6 +13,7 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/packages/alpine"
+	"code.gitea.io/gitea/modules/packages/arch"
 	"code.gitea.io/gitea/modules/packages/cargo"
 	"code.gitea.io/gitea/modules/packages/chef"
 	"code.gitea.io/gitea/modules/packages/composer"
@@ -150,6 +151,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
 	switch p.Type {
 	case TypeAlpine:
 		metadata = &alpine.VersionMetadata{}
+	case TypeArch:
+		metadata = &arch.VersionMetadata{}
 	case TypeCargo:
 		metadata = &cargo.Metadata{}
 	case TypeChef:
diff --git a/models/packages/package.go b/models/packages/package.go
index 65a25741509e4..417d62d199310 100644
--- a/models/packages/package.go
+++ b/models/packages/package.go
@@ -31,6 +31,7 @@ type Type string
 // List of supported packages
 const (
 	TypeAlpine    Type = "alpine"
+	TypeArch      Type = "arch"
 	TypeCargo     Type = "cargo"
 	TypeChef      Type = "chef"
 	TypeComposer  Type = "composer"
@@ -55,6 +56,7 @@ const (
 
 var TypeList = []Type{
 	TypeAlpine,
+	TypeArch,
 	TypeCargo,
 	TypeChef,
 	TypeComposer,
@@ -82,6 +84,8 @@ func (pt Type) Name() string {
 	switch pt {
 	case TypeAlpine:
 		return "Alpine"
+	case TypeArch:
+		return "Arch"
 	case TypeCargo:
 		return "Cargo"
 	case TypeChef:
@@ -131,6 +135,8 @@ func (pt Type) SVGName() string {
 	switch pt {
 	case TypeAlpine:
 		return "gitea-alpine"
+	case TypeArch:
+		return "gitea-arch"
 	case TypeCargo:
 		return "gitea-cargo"
 	case TypeChef:
diff --git a/models/packages/package_file.go b/models/packages/package_file.go
index 1bb6b57a34e8e..270cb32fdf6b5 100644
--- a/models/packages/package_file.go
+++ b/models/packages/package_file.go
@@ -221,6 +221,11 @@ func SearchFiles(ctx context.Context, opts *PackageFileSearchOptions) ([]*Packag
 	return pfs, count, err
 }
 
+// HasFiles tests if there are files of packages matching the search options
+func HasFiles(ctx context.Context, opts *PackageFileSearchOptions) (bool, error) {
+	return db.Exist[PackageFile](ctx, opts.toConds())
+}
+
 // CalculateFileSize sums up all blob sizes matching the search options.
 // It does NOT respect the deduplication of blobs.
 func CalculateFileSize(ctx context.Context, opts *PackageFileSearchOptions) (int64, error) {
diff --git a/modules/packages/arch/metadata.go b/modules/packages/arch/metadata.go
new file mode 100644
index 0000000000000..e1e79c60e0dfe
--- /dev/null
+++ b/modules/packages/arch/metadata.go
@@ -0,0 +1,249 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package arch
+
+import (
+	"archive/tar"
+	"bufio"
+	"bytes"
+	"compress/gzip"
+	"io"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/modules/validation"
+
+	"github.com/klauspost/compress/zstd"
+	"github.com/ulikunitz/xz"
+)
+
+const (
+	PropertyRepository   = "arch.repository"
+	PropertyArchitecture = "arch.architecture"
+	PropertySignature    = "arch.signature"
+	PropertyMetadata     = "arch.metadata"
+
+	SettingKeyPrivate = "arch.key.private"
+	SettingKeyPublic  = "arch.key.public"
+
+	RepositoryPackage = "_arch"
+	RepositoryVersion = "_repository"
+
+	AnyArch = "any"
+)
+
+var (
+	ErrMissingPKGINFOFile  = util.NewInvalidArgumentErrorf(".PKGINFO file is missing")
+	ErrUnsupportedFormat   = util.NewInvalidArgumentErrorf("unsupported package container format")
+	ErrInvalidName         = util.NewInvalidArgumentErrorf("package name is invalid")
+	ErrInvalidVersion      = util.NewInvalidArgumentErrorf("package version is invalid")
+	ErrInvalidArchitecture = util.NewInvalidArgumentErrorf("package architecture is invalid")
+
+	// https://man.archlinux.org/man/PKGBUILD.5
+	namePattern    = regexp.MustCompile(`\A[a-zA-Z0-9@._+-]+\z`)
+	versionPattern = regexp.MustCompile(`\A(?:[0-9]:)?[a-zA-Z0-9.+~]+(?:-[a-zA-Z0-9.+-~]+)?\z`)
+)
+
+type Package struct {
+	Name                     string
+	Version                  string
+	VersionMetadata          VersionMetadata
+	FileMetadata             FileMetadata
+	FileCompressionExtension string
+}
+
+type VersionMetadata struct {
+	Description string   `json:"description,omitempty"`
+	ProjectURL  string   `json:"project_url,omitempty"`
+	Licenses    []string `json:"licenses,omitempty"`
+}
+
+type FileMetadata struct {
+	Architecture  string   `json:"architecture"`
+	Base          string   `json:"base,omitempty"`
+	InstalledSize int64    `json:"installed_size,omitempty"`
+	BuildDate     int64    `json:"build_date,omitempty"`
+	Packager      string   `json:"packager,omitempty"`
+	Groups        []string `json:"groups,omitempty"`
+	Provides      []string `json:"provides,omitempty"`
+	Depends       []string `json:"depends,omitempty"`
+	OptDepends    []string `json:"opt_depends,omitempty"`
+	MakeDepends   []string `json:"make_depends,omitempty"`
+	CheckDepends  []string `json:"check_depends,omitempty"`
+	XData         []string `json:"xdata,omitempty"`
+	Backup        []string `json:"backup,omitempty"`
+	Files         []string `json:"files,omitempty"`
+}
+
+// ParsePackage parses an Arch package file
+func ParsePackage(r io.Reader) (*Package, error) {
+	header := make([]byte, 10)
+	n, err := util.ReadAtMost(r, header)
+	if err != nil {
+		return nil, err
+	}
+
+	r = io.MultiReader(bytes.NewReader(header[:n]), r)
+
+	var inner io.Reader
+	var compressionType string
+	if bytes.HasPrefix(header, []byte{0x28, 0xB5, 0x2F, 0xFD}) { // zst
+		zr, err := zstd.NewReader(r)
+		if err != nil {
+			return nil, err
+		}
+		defer zr.Close()
+
+		inner = zr
+		compressionType = "zst"
+	} else if bytes.HasPrefix(header, []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A}) { // xz
+		xzr, err := xz.NewReader(r)
+		if err != nil {
+			return nil, err
+		}
+
+		inner = xzr
+		compressionType = "xz"
+	} else if bytes.HasPrefix(header, []byte{0x1F, 0x8B}) { // gz
+		gzr, err := gzip.NewReader(r)
+		if err != nil {
+			return nil, err
+		}
+		defer gzr.Close()
+
+		inner = gzr
+		compressionType = "gz"
+	} else {
+		return nil, ErrUnsupportedFormat
+	}
+
+	var p *Package
+	files := make([]string, 0, 10)
+
+	tr := tar.NewReader(inner)
+	for {
+		hd, err := tr.Next()
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			return nil, err
+		}
+
+		if hd.Typeflag != tar.TypeReg {
+			continue
+		}
+
+		filename := hd.FileInfo().Name()
+		if filename == ".PKGINFO" {
+			p, err = ParsePackageInfo(tr)
+			if err != nil {
+				return nil, err
+			}
+		} else if !strings.HasPrefix(filename, ".") {
+			files = append(files, hd.Name)
+		}
+	}
+
+	if p == nil {
+		return nil, ErrMissingPKGINFOFile
+	}
+
+	p.FileMetadata.Files = files
+	p.FileCompressionExtension = compressionType
+
+	return p, nil
+}
+
+// ParsePackageInfo parses a .PKGINFO file to retrieve the metadata
+// https://man.archlinux.org/man/PKGBUILD.5
+// https://gitlab.archlinux.org/pacman/pacman/-/blob/master/lib/libalpm/be_package.c#L161
+func ParsePackageInfo(r io.Reader) (*Package, error) {
+	p := &Package{}
+
+	s := bufio.NewScanner(r)
+	for s.Scan() {
+		line := s.Text()
+
+		if strings.HasPrefix(line, "#") {
+			continue
+		}
+
+		i := strings.IndexRune(line, '=')
+		if i == -1 {
+			continue
+		}
+
+		key := strings.TrimSpace(line[:i])
+		value := strings.TrimSpace(line[i+1:])
+
+		switch key {
+		case "pkgname":
+			p.Name = value
+		case "pkgbase":
+			p.FileMetadata.Base = value
+		case "pkgver":
+			p.Version = value
+		case "pkgdesc":
+			p.VersionMetadata.Description = value
+		case "url":
+			p.VersionMetadata.ProjectURL = value
+		case "packager":
+			p.FileMetadata.Packager = value
+		case "arch":
+			p.FileMetadata.Architecture = value
+		case "license":
+			p.VersionMetadata.Licenses = append(p.VersionMetadata.Licenses, value)
+		case "provides":
+			p.FileMetadata.Provides = append(p.FileMetadata.Provides, value)
+		case "depend":
+			p.FileMetadata.Depends = append(p.FileMetadata.Depends, value)
+		case "optdepend":
+			p.FileMetadata.OptDepends = append(p.FileMetadata.OptDepends, value)
+		case "makedepend":
+			p.FileMetadata.MakeDepends = append(p.FileMetadata.MakeDepends, value)
+		case "checkdepend":
+			p.FileMetadata.CheckDepends = append(p.FileMetadata.CheckDepends, value)
+		case "backup":
+			p.FileMetadata.Backup = append(p.FileMetadata.Backup, value)
+		case "group":
+			p.FileMetadata.Groups = append(p.FileMetadata.Groups, value)
+		case "builddate":
+			date, err := strconv.ParseInt(value, 10, 64)
+			if err != nil {
+				return nil, err
+			}
+			p.FileMetadata.BuildDate = date
+		case "size":
+			size, err := strconv.ParseInt(value, 10, 64)
+			if err != nil {
+				return nil, err
+			}
+			p.FileMetadata.InstalledSize = size
+		case "xdata":
+			p.FileMetadata.XData = append(p.FileMetadata.XData, value)
+		}
+	}
+	if err := s.Err(); err != nil {
+		return nil, err
+	}
+
+	if !namePattern.MatchString(p.Name) {
+		return nil, ErrInvalidName
+	}
+	if !versionPattern.MatchString(p.Version) {
+		return nil, ErrInvalidVersion
+	}
+	if p.FileMetadata.Architecture == "" {
+		return nil, ErrInvalidArchitecture
+	}
+
+	if !validation.IsValidURL(p.VersionMetadata.ProjectURL) {
+		p.VersionMetadata.ProjectURL = ""
+	}
+
+	return p, nil
+}
diff --git a/modules/packages/arch/metadata_test.go b/modules/packages/arch/metadata_test.go
new file mode 100644
index 0000000000000..f611ef5e84586
--- /dev/null
+++ b/modules/packages/arch/metadata_test.go
@@ -0,0 +1,157 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package arch
+
+import (
+	"archive/tar"
+	"bytes"
+	"compress/gzip"
+	"io"
+	"testing"
+
+	"github.com/klauspost/compress/zstd"
+	"github.com/stretchr/testify/assert"
+	"github.com/ulikunitz/xz"
+)
+
+const (
+	packageName        = "gitea"
+	packageVersion     = "1.0.1"
+	packageDescription = "Package Description"
+	packageProjectURL  = "https://gitea.com"
+	packagePackager    = "KN4CK3R <packager@gitea.com>"
+)
+
+func createPKGINFOContent(name, version string) []byte {
+	return []byte(`pkgname = ` + name + `
+pkgbase = ` + name + `
+pkgver = ` + version + `
+pkgdesc = ` + packageDescription + `
+url = ` + packageProjectURL + `
+# comment
+group=group
+builddate = 1678834800
+size = 123456
+arch = x86_64
+license = MIT
+packager = ` + packagePackager + `
+depend = common
+xdata = value
+depend = gitea
+provides = common
+provides = gitea
+optdepend = hex
+checkdepend = common
+makedepend = cmake
+backup = usr/bin/paket1`)
+}
+
+func TestParsePackage(t *testing.T) {
+	createPackage := func(compression string, files map[string][]byte) io.Reader {
+		var buf bytes.Buffer
+		var cw io.WriteCloser
+		switch compression {
+		case "zst":
+			cw, _ = zstd.NewWriter(&buf)
+		case "xz":
+			cw, _ = xz.NewWriter(&buf)
+		case "gz":
+			cw = gzip.NewWriter(&buf)
+		}
+		tw := tar.NewWriter(cw)
+
+		for name, content := range files {
+			hdr := &tar.Header{
+				Name: name,
+				Mode: 0o600,
+				Size: int64(len(content)),
+			}
+			tw.WriteHeader(hdr)
+			tw.Write(content)
+		}
+
+		tw.Close()
+		cw.Close()
+
+		return &buf
+	}
+
+	for _, c := range []string{"gz", "xz", "zst"} {
+		t.Run(c, func(t *testing.T) {
+			t.Run("MissingPKGINFOFile", func(t *testing.T) {
+				data := createPackage(c, map[string][]byte{"dummy.txt": {}})
+
+				pp, err := ParsePackage(data)
+				assert.Nil(t, pp)
+				assert.ErrorIs(t, err, ErrMissingPKGINFOFile)
+			})
+
+			t.Run("InvalidPKGINFOFile", func(t *testing.T) {
+				data := createPackage(c, map[string][]byte{".PKGINFO": {}})
+
+				pp, err := ParsePackage(data)
+				assert.Nil(t, pp)
+				assert.ErrorIs(t, err, ErrInvalidName)
+			})
+
+			t.Run("Valid", func(t *testing.T) {
+				data := createPackage(c, map[string][]byte{
+					".PKGINFO":        createPKGINFOContent(packageName, packageVersion),
+					"/test/dummy.txt": {},
+				})
+
+				p, err := ParsePackage(data)
+				assert.NoError(t, err)
+				assert.NotNil(t, p)
+
+				assert.ElementsMatch(t, []string{"/test/dummy.txt"}, p.FileMetadata.Files)
+			})
+		})
+	}
+}
+
+func TestParsePackageInfo(t *testing.T) {
+	t.Run("InvalidName", func(t *testing.T) {
+		data := createPKGINFOContent("", packageVersion)
+
+		p, err := ParsePackageInfo(bytes.NewReader(data))
+		assert.Nil(t, p)
+		assert.ErrorIs(t, err, ErrInvalidName)
+	})
+
+	t.Run("InvalidVersion", func(t *testing.T) {
+		data := createPKGINFOContent(packageName, "")
+
+		p, err := ParsePackageInfo(bytes.NewReader(data))
+		assert.Nil(t, p)
+		assert.ErrorIs(t, err, ErrInvalidVersion)
+	})
+
+	t.Run("Valid", func(t *testing.T) {
+		data := createPKGINFOContent(packageName, packageVersion)
+
+		p, err := ParsePackageInfo(bytes.NewReader(data))
+		assert.NoError(t, err)
+		assert.NotNil(t, p)
+
+		assert.Equal(t, packageName, p.Name)
+		assert.Equal(t, packageName, p.FileMetadata.Base)
+		assert.Equal(t, packageVersion, p.Version)
+		assert.Equal(t, packageDescription, p.VersionMetadata.Description)
+		assert.Equal(t, packagePackager, p.FileMetadata.Packager)
+		assert.Equal(t, packageProjectURL, p.VersionMetadata.ProjectURL)
+		assert.ElementsMatch(t, []string{"MIT"}, p.VersionMetadata.Licenses)
+		assert.EqualValues(t, 1678834800, p.FileMetadata.BuildDate)
+		assert.EqualValues(t, 123456, p.FileMetadata.InstalledSize)
+		assert.Equal(t, "x86_64", p.FileMetadata.Architecture)
+		assert.ElementsMatch(t, []string{"value"}, p.FileMetadata.XData)
+		assert.ElementsMatch(t, []string{"group"}, p.FileMetadata.Groups)
+		assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Provides)
+		assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Depends)
+		assert.ElementsMatch(t, []string{"hex"}, p.FileMetadata.OptDepends)
+		assert.ElementsMatch(t, []string{"common"}, p.FileMetadata.CheckDepends)
+		assert.ElementsMatch(t, []string{"cmake"}, p.FileMetadata.MakeDepends)
+		assert.ElementsMatch(t, []string{"usr/bin/paket1"}, p.FileMetadata.Backup)
+	})
+}
diff --git a/modules/setting/packages.go b/modules/setting/packages.go
index bc093e7ea6c1d..3f618cfd64115 100644
--- a/modules/setting/packages.go
+++ b/modules/setting/packages.go
@@ -22,6 +22,7 @@ var (
 		LimitTotalOwnerCount int64
 		LimitTotalOwnerSize  int64
 		LimitSizeAlpine      int64
+		LimitSizeArch        int64
 		LimitSizeCargo       int64
 		LimitSizeChef        int64
 		LimitSizeComposer    int64
@@ -79,6 +80,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
 
 	Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE")
 	Packages.LimitSizeAlpine = mustBytes(sec, "LIMIT_SIZE_ALPINE")
+	Packages.LimitSizeArch = mustBytes(sec, "LIMIT_SIZE_ARCH")
 	Packages.LimitSizeCargo = mustBytes(sec, "LIMIT_SIZE_CARGO")
 	Packages.LimitSizeChef = mustBytes(sec, "LIMIT_SIZE_CHEF")
 	Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER")
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 57d2d89c5acad..395063faf8180 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3524,6 +3524,11 @@ alpine.repository = Repository Info
 alpine.repository.branches = Branches
 alpine.repository.repositories = Repositories
 alpine.repository.architectures = Architectures
+arch.registry = Add server with related repository and architecture to <code>/etc/pacman.conf</code>:
+arch.install = Sync package with pacman:
+arch.repository = Repository Info
+arch.repository.repositories = Repositories
+arch.repository.architectures = Architectures
 cargo.registry = Setup this registry in the Cargo configuration file (for example <code>~/.cargo/config.toml</code>):
 cargo.install = To install the package using Cargo, run the following command:
 chef.registry = Setup this registry in your <code>~/.chef/config.rb</code> file:
diff --git a/public/assets/img/svg/gitea-arch.svg b/public/assets/img/svg/gitea-arch.svg
new file mode 100644
index 0000000000000..943a92c579468
--- /dev/null
+++ b/public/assets/img/svg/gitea-arch.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="svg gitea-arch" width="16" height="16" aria-hidden="true"><path fill="#1793d1" d="M256 72c-14 35-23 57-39 91 10 11 22 23 41 36-21-8-35-17-45-26-21 43-53 103-117 220 50-30 90-48 127-55-2-7-3-14-3-22v-1c1-33 18-58 38-56 20 1 36 29 35 62l-2 17c36 7 75 26 125 54l-27-50c-13-10-27-23-55-38 19 5 33 11 44 17-86-159-93-180-122-250z"/></svg>
\ No newline at end of file
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index c3da5a7513bc3..4e194f65fa1b4 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -14,6 +14,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/packages/alpine"
+	"code.gitea.io/gitea/routers/api/packages/arch"
 	"code.gitea.io/gitea/routers/api/packages/cargo"
 	"code.gitea.io/gitea/routers/api/packages/chef"
 	"code.gitea.io/gitea/routers/api/packages/composer"
@@ -135,6 +136,49 @@ func CommonRoutes() *web.Router {
 				})
 			})
 		}, reqPackageAccess(perm.AccessModeRead))
+		r.Group("/arch", func() {
+			r.Methods("HEAD,GET", "/repository.key", arch.GetRepositoryKey)
+
+			r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) {
+				path := strings.Trim(ctx.PathParam("*"), "/")
+
+				if ctx.Req.Method == "PUT" {
+					reqPackageAccess(perm.AccessModeWrite)(ctx)
+					if ctx.Written() {
+						return
+					}
+					ctx.SetPathParam("repository", path)
+					arch.UploadPackageFile(ctx)
+					return
+				}
+
+				pathFields := strings.Split(path, "/")
+				pathFieldsLen := len(pathFields)
+
+				if (ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET") && pathFieldsLen >= 2 {
+					ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-2], "/"))
+					ctx.SetPathParam("architecture", pathFields[pathFieldsLen-2])
+					ctx.SetPathParam("filename", pathFields[pathFieldsLen-1])
+					arch.GetPackageOrRepositoryFile(ctx)
+					return
+				}
+
+				if ctx.Req.Method == "DELETE" && pathFieldsLen >= 3 {
+					reqPackageAccess(perm.AccessModeWrite)(ctx)
+					if ctx.Written() {
+						return
+					}
+					ctx.SetPathParam("repository", strings.Join(pathFields[:pathFieldsLen-3], "/"))
+					ctx.SetPathParam("name", pathFields[pathFieldsLen-3])
+					ctx.SetPathParam("version", pathFields[pathFieldsLen-2])
+					ctx.SetPathParam("architecture", pathFields[pathFieldsLen-1])
+					arch.DeletePackageVersion(ctx)
+					return
+				}
+
+				ctx.Status(http.StatusNotFound)
+			})
+		}, reqPackageAccess(perm.AccessModeRead))
 		r.Group("/cargo", func() {
 			r.Group("/api/v1/crates", func() {
 				r.Get("", cargo.SearchPackages)
diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go
new file mode 100644
index 0000000000000..573e93cfb016f
--- /dev/null
+++ b/routers/api/packages/arch/arch.go
@@ -0,0 +1,306 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package arch
+
+import (
+	"bytes"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"strings"
+
+	packages_model "code.gitea.io/gitea/models/packages"
+	"code.gitea.io/gitea/modules/json"
+	packages_module "code.gitea.io/gitea/modules/packages"
+	arch_module "code.gitea.io/gitea/modules/packages/arch"
+	"code.gitea.io/gitea/modules/util"
+	"code.gitea.io/gitea/routers/api/packages/helper"
+	"code.gitea.io/gitea/services/context"
+	packages_service "code.gitea.io/gitea/services/packages"
+	arch_service "code.gitea.io/gitea/services/packages/arch"
+)
+
+func apiError(ctx *context.Context, status int, obj any) {
+	helper.LogAndProcessError(ctx, status, obj, func(message string) {
+		ctx.PlainText(status, message)
+	})
+}
+
+func GetRepositoryKey(ctx *context.Context) {
+	_, pub, err := arch_service.GetOrCreateKeyPair(ctx, ctx.Package.Owner.ID)
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+
+	ctx.ServeContent(strings.NewReader(pub), &context.ServeHeaderOptions{
+		ContentType: "application/pgp-keys",
+	})
+}
+
+func UploadPackageFile(ctx *context.Context) {
+	repository := strings.TrimSpace(ctx.PathParam("repository"))
+
+	upload, needToClose, err := ctx.UploadStream()
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+	if needToClose {
+		defer upload.Close()
+	}
+
+	buf, err := packages_module.CreateHashedBufferFromReader(upload)
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+	defer buf.Close()
+
+	pck, err := arch_module.ParsePackage(buf)
+	if err != nil {
+		if errors.Is(err, util.ErrInvalidArgument) || err == io.EOF {
+			apiError(ctx, http.StatusBadRequest, err)
+		} else {
+			apiError(ctx, http.StatusInternalServerError, err)
+		}
+		return
+	}
+
+	if _, err := buf.Seek(0, io.SeekStart); err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+
+	fileMetadataRaw, err := json.Marshal(pck.FileMetadata)
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+
+	signature, err := arch_service.SignData(ctx, ctx.Package.Owner.ID, buf)
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+
+	if _, err := buf.Seek(0, io.SeekStart); err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+
+	release, err := arch_service.AquireRegistryLock(ctx, ctx.Package.Owner.ID)
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+	defer release()
+
+	// Search for duplicates with different file compression
+	has, err := packages_model.HasFiles(ctx, &packages_model.PackageFileSearchOptions{
+		OwnerID:     ctx.Package.Owner.ID,
+		PackageType: packages_model.TypeArch,
+		Query:       fmt.Sprintf("%s-%s-%s.pkg.tar.%%", pck.Name, pck.Version, pck.FileMetadata.Architecture),
+		Properties: map[string]string{
+			arch_module.PropertyRepository:   repository,
+			arch_module.PropertyArchitecture: pck.FileMetadata.Architecture,
+		},
+	})
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+	if has {
+		apiError(ctx, http.StatusConflict, packages_model.ErrDuplicatePackageFile)
+		return
+	}
+
+	_, _, err = packages_service.CreatePackageOrAddFileToExisting(
+		ctx,
+		&packages_service.PackageCreationInfo{
+			PackageInfo: packages_service.PackageInfo{
+				Owner:       ctx.Package.Owner,
+				PackageType: packages_model.TypeArch,
+				Name:        pck.Name,
+				Version:     pck.Version,
+			},
+			Creator:  ctx.Doer,
+			Metadata: pck.VersionMetadata,
+		},
+		&packages_service.PackageFileCreationInfo{
+			PackageFileInfo: packages_service.PackageFileInfo{
+				Filename:     fmt.Sprintf("%s-%s-%s.pkg.tar.%s", pck.Name, pck.Version, pck.FileMetadata.Architecture, pck.FileCompressionExtension),
+				CompositeKey: fmt.Sprintf("%s|%s", repository, pck.FileMetadata.Architecture),
+			},
+			Creator: ctx.Doer,
+			Data:    buf,
+			IsLead:  true,
+			Properties: map[string]string{
+				arch_module.PropertyRepository:   repository,
+				arch_module.PropertyArchitecture: pck.FileMetadata.Architecture,
+				arch_module.PropertyMetadata:     string(fileMetadataRaw),
+				arch_module.PropertySignature:    base64.StdEncoding.EncodeToString(signature),
+			},
+		},
+	)
+	if err != nil {
+		switch err {
+		case packages_model.ErrDuplicatePackageVersion, packages_model.ErrDuplicatePackageFile:
+			apiError(ctx, http.StatusConflict, err)
+		case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
+			apiError(ctx, http.StatusForbidden, err)
+		default:
+			apiError(ctx, http.StatusInternalServerError, err)
+		}
+		return
+	}
+
+	if err := arch_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, repository, pck.FileMetadata.Architecture); err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+
+	ctx.Status(http.StatusCreated)
+}
+
+func GetPackageOrRepositoryFile(ctx *context.Context) {
+	repository := ctx.PathParam("repository")
+	architecture := ctx.PathParam("architecture")
+	filename := ctx.PathParam("filename")
+	filenameOrig := filename
+
+	isSignature := strings.HasSuffix(filename, ".sig")
+	if isSignature {
+		filename = filename[:len(filename)-len(".sig")]
+	}
+
+	opts := &packages_model.PackageFileSearchOptions{
+		OwnerID:      ctx.Package.Owner.ID,
+		PackageType:  packages_model.TypeArch,
+		Query:        filename,
+		CompositeKey: fmt.Sprintf("%s|%s", repository, architecture),
+	}
+
+	if strings.HasSuffix(filename, ".db.tar.gz") || strings.HasSuffix(filename, ".files.tar.gz") || strings.HasSuffix(filename, ".files") || strings.HasSuffix(filename, ".db") {
+		// The requested filename is based on the user-defined repository name.
+		// Normalize everything to "packages.db".
+		opts.Query = arch_service.IndexArchiveFilename
+
+		pv, err := arch_service.GetOrCreateRepositoryVersion(ctx, ctx.Package.Owner.ID)
+		if err != nil {
+			apiError(ctx, http.StatusInternalServerError, err)
+			return
+		}
+		opts.VersionID = pv.ID
+	}
+
+	pfs, _, err := packages_model.SearchFiles(ctx, opts)
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+	if len(pfs) == 0 {
+		// Try again with architecture 'any'
+		if architecture == arch_module.AnyArch {
+			apiError(ctx, http.StatusNotFound, nil)
+			return
+		}
+
+		opts.CompositeKey = fmt.Sprintf("%s|%s", repository, arch_module.AnyArch)
+		if pfs, _, err = packages_model.SearchFiles(ctx, opts); err != nil {
+			apiError(ctx, http.StatusInternalServerError, err)
+			return
+		}
+	}
+	if len(pfs) != 1 {
+		apiError(ctx, http.StatusNotFound, nil)
+		return
+	}
+
+	if isSignature {
+		pfps, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pfs[0].ID, arch_module.PropertySignature)
+		if err != nil || len(pfps) == 0 {
+			apiError(ctx, http.StatusInternalServerError, err)
+			return
+		}
+
+		data, err := base64.StdEncoding.DecodeString(pfps[0].Value)
+		if err != nil {
+			apiError(ctx, http.StatusInternalServerError, err)
+			return
+		}
+
+		ctx.ServeContent(bytes.NewReader(data), &context.ServeHeaderOptions{
+			Filename: filenameOrig,
+		})
+		return
+	}
+
+	s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0])
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			apiError(ctx, http.StatusNotFound, err)
+		} else {
+			apiError(ctx, http.StatusInternalServerError, err)
+		}
+		return
+	}
+
+	helper.ServePackageFile(ctx, s, u, pf)
+}
+
+func DeletePackageVersion(ctx *context.Context) {
+	repository := ctx.PathParam("repository")
+	architecture := ctx.PathParam("architecture")
+	name := ctx.PathParam("name")
+	version := ctx.PathParam("version")
+
+	release, err := arch_service.AquireRegistryLock(ctx, ctx.Package.Owner.ID)
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+	defer release()
+
+	pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeArch, name, version)
+	if err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			apiError(ctx, http.StatusNotFound, err)
+		} else {
+			apiError(ctx, http.StatusInternalServerError, err)
+		}
+		return
+	}
+
+	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
+		VersionID:    pv.ID,
+		CompositeKey: fmt.Sprintf("%s|%s", repository, architecture),
+	})
+	if err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+	if len(pfs) != 1 {
+		apiError(ctx, http.StatusNotFound, nil)
+		return
+	}
+
+	if err := packages_service.RemovePackageFileAndVersionIfUnreferenced(ctx, ctx.Doer, pfs[0]); err != nil {
+		if errors.Is(err, util.ErrNotExist) {
+			apiError(ctx, http.StatusNotFound, err)
+		} else {
+			apiError(ctx, http.StatusInternalServerError, err)
+		}
+		return
+	}
+
+	if err := arch_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, repository, architecture); err != nil {
+		apiError(ctx, http.StatusInternalServerError, err)
+		return
+	}
+
+	ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index 4e39eabc0ffde..c6f85ac734eb9 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -20,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/optional"
 	alpine_module "code.gitea.io/gitea/modules/packages/alpine"
+	arch_module "code.gitea.io/gitea/modules/packages/arch"
 	debian_module "code.gitea.io/gitea/modules/packages/debian"
 	rpm_module "code.gitea.io/gitea/modules/packages/rpm"
 	"code.gitea.io/gitea/modules/setting"
@@ -178,13 +179,13 @@ func ViewPackageVersion(ctx *context.Context) {
 	ctx.Data["IsPackagesPage"] = true
 	ctx.Data["PackageDescriptor"] = pd
 
+	registryHostURL, err := url.Parse(httplib.GuessCurrentHostURL(ctx))
+	if err != nil {
+		registryHostURL, _ = url.Parse(setting.AppURL)
+	}
+	ctx.Data["PackageRegistryHost"] = registryHostURL.Host
+
 	switch pd.Package.Type {
-	case packages_model.TypeContainer:
-		registryAppURL, err := url.Parse(httplib.GuessCurrentAppURL(ctx))
-		if err != nil {
-			registryAppURL, _ = url.Parse(setting.AppURL)
-		}
-		ctx.Data["RegistryHost"] = registryAppURL.Host
 	case packages_model.TypeAlpine:
 		branches := make(container.Set[string])
 		repositories := make(container.Set[string])
@@ -204,6 +205,23 @@ func ViewPackageVersion(ctx *context.Context) {
 		}
 
 		ctx.Data["Branches"] = util.Sorted(branches.Values())
+		ctx.Data["Repositories"] = util.Sorted(repositories.Values())
+		ctx.Data["Architectures"] = util.Sorted(architectures.Values())
+	case packages_model.TypeArch:
+		repositories := make(container.Set[string])
+		architectures := make(container.Set[string])
+
+		for _, f := range pd.Files {
+			for _, pp := range f.Properties {
+				switch pp.Name {
+				case arch_module.PropertyRepository:
+					repositories.Add(pp.Value)
+				case arch_module.PropertyArchitecture:
+					architectures.Add(pp.Value)
+				}
+			}
+		}
+
 		ctx.Data["Repositories"] = util.Sorted(repositories.Values())
 		ctx.Data["Architectures"] = util.Sorted(architectures.Values())
 	case packages_model.TypeDebian:
@@ -249,7 +267,6 @@ func ViewPackageVersion(ctx *context.Context) {
 	var (
 		total int64
 		pvs   []*packages_model.PackageVersion
-		err   error
 	)
 	switch pd.Package.Type {
 	case packages_model.TypeContainer:
diff --git a/services/forms/package_form.go b/services/forms/package_form.go
index cc940d42d34df..9b6f9071647bc 100644
--- a/services/forms/package_form.go
+++ b/services/forms/package_form.go
@@ -15,7 +15,7 @@ import (
 type PackageCleanupRuleForm struct {
 	ID            int64
 	Enabled       bool
-	Type          string `binding:"Required;In(alpine,cargo,chef,composer,conan,conda,container,cran,debian,generic,go,helm,maven,npm,nuget,pub,pypi,rpm,rubygems,swift,vagrant)"`
+	Type          string `binding:"Required;In(alpine,arch,cargo,chef,composer,conan,conda,container,cran,debian,generic,go,helm,maven,npm,nuget,pub,pypi,rpm,rubygems,swift,vagrant)"`
 	KeepCount     int    `binding:"In(0,1,5,10,25,50,100)"`
 	KeepPattern   string `binding:"RegexPattern"`
 	RemoveDays    int    `binding:"In(0,7,14,30,60,90,180)"`
diff --git a/services/packages/alpine/repository.go b/services/packages/alpine/repository.go
index 664ab3455980d..27e63919803eb 100644
--- a/services/packages/alpine/repository.go
+++ b/services/packages/alpine/repository.go
@@ -72,7 +72,7 @@ func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, err
 	return priv, pub, nil
 }
 
-// BuildAllRepositoryFiles (re)builds all repository files for every available distributions, components and architectures
+// BuildAllRepositoryFiles (re)builds all repository files for every available branches, repositories and architectures
 func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error {
 	pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
 	if err != nil {
diff --git a/services/packages/arch/repository.go b/services/packages/arch/repository.go
new file mode 100644
index 0000000000000..ab1b85ae95870
--- /dev/null
+++ b/services/packages/arch/repository.go
@@ -0,0 +1,401 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package arch
+
+import (
+	"archive/tar"
+	"bytes"
+	"compress/gzip"
+	"context"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"strings"
+
+	packages_model "code.gitea.io/gitea/models/packages"
+	arch_model "code.gitea.io/gitea/models/packages/arch"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/container"
+	"code.gitea.io/gitea/modules/globallock"
+	"code.gitea.io/gitea/modules/json"
+	packages_module "code.gitea.io/gitea/modules/packages"
+	arch_module "code.gitea.io/gitea/modules/packages/arch"
+	"code.gitea.io/gitea/modules/util"
+	packages_service "code.gitea.io/gitea/services/packages"
+
+	"github.com/keybase/go-crypto/openpgp"
+	"github.com/keybase/go-crypto/openpgp/armor"
+	"github.com/keybase/go-crypto/openpgp/packet"
+)
+
+const (
+	IndexArchiveFilename = "packages.db"
+)
+
+func AquireRegistryLock(ctx context.Context, ownerID int64) (globallock.ReleaseFunc, error) {
+	return globallock.Lock(ctx, fmt.Sprintf("packages_arch_%d", ownerID))
+}
+
+// GetOrCreateRepositoryVersion gets or creates the internal repository package
+// The Arch registry needs multiple index files which are stored in this package.
+func GetOrCreateRepositoryVersion(ctx context.Context, ownerID int64) (*packages_model.PackageVersion, error) {
+	return packages_service.GetOrCreateInternalPackageVersion(ctx, ownerID, packages_model.TypeArch, arch_module.RepositoryPackage, arch_module.RepositoryVersion)
+}
+
+// GetOrCreateKeyPair gets or creates the PGP keys used to sign repository files
+func GetOrCreateKeyPair(ctx context.Context, ownerID int64) (string, string, error) {
+	priv, err := user_model.GetSetting(ctx, ownerID, arch_module.SettingKeyPrivate)
+	if err != nil && !errors.Is(err, util.ErrNotExist) {
+		return "", "", err
+	}
+
+	pub, err := user_model.GetSetting(ctx, ownerID, arch_module.SettingKeyPublic)
+	if err != nil && !errors.Is(err, util.ErrNotExist) {
+		return "", "", err
+	}
+
+	if priv == "" || pub == "" {
+		priv, pub, err = generateKeypair()
+		if err != nil {
+			return "", "", err
+		}
+
+		if err := user_model.SetUserSetting(ctx, ownerID, arch_module.SettingKeyPrivate, priv); err != nil {
+			return "", "", err
+		}
+
+		if err := user_model.SetUserSetting(ctx, ownerID, arch_module.SettingKeyPublic, pub); err != nil {
+			return "", "", err
+		}
+	}
+
+	return priv, pub, nil
+}
+
+func generateKeypair() (string, string, error) {
+	e, err := openpgp.NewEntity("", "Arch Registry", "", nil)
+	if err != nil {
+		return "", "", err
+	}
+
+	var priv strings.Builder
+	var pub strings.Builder
+
+	w, err := armor.Encode(&priv, openpgp.PrivateKeyType, nil)
+	if err != nil {
+		return "", "", err
+	}
+	if err := e.SerializePrivate(w, nil); err != nil {
+		return "", "", err
+	}
+	w.Close()
+
+	w, err = armor.Encode(&pub, openpgp.PublicKeyType, nil)
+	if err != nil {
+		return "", "", err
+	}
+	if err := e.Serialize(w); err != nil {
+		return "", "", err
+	}
+	w.Close()
+
+	return priv.String(), pub.String(), nil
+}
+
+func SignData(ctx context.Context, ownerID int64, r io.Reader) ([]byte, error) {
+	priv, _, err := GetOrCreateKeyPair(ctx, ownerID)
+	if err != nil {
+		return nil, err
+	}
+
+	block, err := armor.Decode(strings.NewReader(priv))
+	if err != nil {
+		return nil, err
+	}
+
+	e, err := openpgp.ReadEntity(packet.NewReader(block.Body))
+	if err != nil {
+		return nil, err
+	}
+
+	buf := &bytes.Buffer{}
+	if err := openpgp.DetachSign(buf, e, r, nil); err != nil {
+		return nil, err
+	}
+
+	return buf.Bytes(), nil
+}
+
+// BuildAllRepositoryFiles (re)builds all repository files for every available repositories and architectures
+func BuildAllRepositoryFiles(ctx context.Context, ownerID int64) error {
+	pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
+	if err != nil {
+		return err
+	}
+
+	// 1. Delete all existing repository files
+	pfs, err := packages_model.GetFilesByVersionID(ctx, pv.ID)
+	if err != nil {
+		return err
+	}
+
+	for _, pf := range pfs {
+		if err := packages_service.DeletePackageFile(ctx, pf); err != nil {
+			return err
+		}
+	}
+
+	// 2. (Re)Build repository files for existing packages
+	repositories, err := arch_model.GetRepositories(ctx, ownerID)
+	if err != nil {
+		return err
+	}
+	for _, repository := range repositories {
+		architectures, err := arch_model.GetArchitectures(ctx, ownerID, repository)
+		if err != nil {
+			return err
+		}
+		for _, architecture := range architectures {
+			if err := buildPackagesIndex(ctx, ownerID, pv, repository, architecture); err != nil {
+				return fmt.Errorf("failed to build repository files [%s/%s]: %w", repository, architecture, err)
+			}
+		}
+	}
+
+	return nil
+}
+
+// BuildSpecificRepositoryFiles builds index files for the repository
+func BuildSpecificRepositoryFiles(ctx context.Context, ownerID int64, repository, architecture string) error {
+	pv, err := GetOrCreateRepositoryVersion(ctx, ownerID)
+	if err != nil {
+		return err
+	}
+
+	architectures := container.SetOf(architecture)
+	if architecture == arch_module.AnyArch {
+		// Update all other architectures too when updating the any index
+		additionalArchitectures, err := arch_model.GetArchitectures(ctx, ownerID, repository)
+		if err != nil {
+			return err
+		}
+		architectures.AddMultiple(additionalArchitectures...)
+	}
+
+	for architecture := range architectures {
+		if err := buildPackagesIndex(ctx, ownerID, pv, repository, architecture); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func searchPackageFiles(ctx context.Context, ownerID int64, repository, architecture string) ([]*packages_model.PackageFile, error) {
+	pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
+		OwnerID:     ownerID,
+		PackageType: packages_model.TypeArch,
+		Query:       "%.pkg.tar.%",
+		Properties: map[string]string{
+			arch_module.PropertyRepository:   repository,
+			arch_module.PropertyArchitecture: architecture,
+		},
+	})
+	if err != nil {
+		return nil, err
+	}
+	return pfs, nil
+}
+
+func buildPackagesIndex(ctx context.Context, ownerID int64, repoVersion *packages_model.PackageVersion, repository, architecture string) error {
+	pfs, err := searchPackageFiles(ctx, ownerID, repository, architecture)
+	if err != nil {
+		return err
+	}
+	if architecture != arch_module.AnyArch {
+		// Add all any packages too
+		anyarchFiles, err := searchPackageFiles(ctx, ownerID, repository, arch_module.AnyArch)
+		if err != nil {
+			return err
+		}
+		pfs = append(pfs, anyarchFiles...)
+	}
+
+	// Delete the package indices if there are no packages
+	if len(pfs) == 0 {
+		pf, err := packages_model.GetFileForVersionByName(ctx, repoVersion.ID, IndexArchiveFilename, fmt.Sprintf("%s|%s", repository, architecture))
+		if err != nil && !errors.Is(err, util.ErrNotExist) {
+			return err
+		} else if pf == nil {
+			return nil
+		}
+
+		return packages_service.DeletePackageFile(ctx, pf)
+	}
+
+	indexContent, _ := packages_module.NewHashedBuffer()
+	defer indexContent.Close()
+
+	gw := gzip.NewWriter(indexContent)
+	tw := tar.NewWriter(gw)
+
+	cache := make(map[int64]*packages_model.Package)
+
+	for _, pf := range pfs {
+		opts := &entryOptions{
+			File: pf,
+		}
+
+		opts.Version, err = packages_model.GetVersionByID(ctx, pf.VersionID)
+		if err != nil {
+			return err
+		}
+		if err := json.Unmarshal([]byte(opts.Version.MetadataJSON), &opts.VersionMetadata); err != nil {
+			return err
+		}
+		opts.Package = cache[opts.Version.PackageID]
+		if opts.Package == nil {
+			opts.Package, err = packages_model.GetPackageByID(ctx, opts.Version.PackageID)
+			if err != nil {
+				return err
+			}
+			cache[opts.Package.ID] = opts.Package
+		}
+		opts.Blob, err = packages_model.GetBlobByID(ctx, pf.BlobID)
+		if err != nil {
+			return err
+		}
+
+		sig, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertySignature)
+		if err != nil {
+			return err
+		}
+		if len(sig) == 0 {
+			return util.ErrNotExist
+		}
+		opts.Signature = sig[0].Value
+
+		meta, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypeFile, pf.ID, arch_module.PropertyMetadata)
+		if err != nil {
+			return err
+		}
+		if len(meta) == 0 {
+			return util.ErrNotExist
+		}
+		if err := json.Unmarshal([]byte(meta[0].Value), &opts.FileMetadata); err != nil {
+			return err
+		}
+
+		if err := writeFiles(tw, opts); err != nil {
+			return err
+		}
+		if err := writeDescription(tw, opts); err != nil {
+			return err
+		}
+	}
+
+	tw.Close()
+	gw.Close()
+
+	signature, err := SignData(ctx, ownerID, indexContent)
+	if err != nil {
+		return err
+	}
+
+	if _, err := indexContent.Seek(0, io.SeekStart); err != nil {
+		return err
+	}
+
+	_, err = packages_service.AddFileToPackageVersionInternal(
+		ctx,
+		repoVersion,
+		&packages_service.PackageFileCreationInfo{
+			PackageFileInfo: packages_service.PackageFileInfo{
+				Filename:     IndexArchiveFilename,
+				CompositeKey: fmt.Sprintf("%s|%s", repository, architecture),
+			},
+			Creator:           user_model.NewGhostUser(),
+			Data:              indexContent,
+			IsLead:            false,
+			OverwriteExisting: true,
+			Properties: map[string]string{
+				arch_module.PropertyRepository:   repository,
+				arch_module.PropertyArchitecture: architecture,
+				arch_module.PropertySignature:    base64.StdEncoding.EncodeToString(signature),
+			},
+		},
+	)
+	return err
+}
+
+type entryOptions struct {
+	Package         *packages_model.Package
+	Version         *packages_model.PackageVersion
+	VersionMetadata *arch_module.VersionMetadata
+	File            *packages_model.PackageFile
+	FileMetadata    *arch_module.FileMetadata
+	Blob            *packages_model.PackageBlob
+	Signature       string
+}
+
+type keyValue struct {
+	Key   string
+	Value string
+}
+
+func writeFiles(tw *tar.Writer, opts *entryOptions) error {
+	return writeFields(tw, fmt.Sprintf("%s-%s/files", opts.Package.Name, opts.Version.Version), []keyValue{
+		{"FILES", strings.Join(opts.FileMetadata.Files, "\n")},
+	})
+}
+
+// https://gitlab.archlinux.org/pacman/pacman/-/blob/master/lib/libalpm/be_sync.c#L562
+func writeDescription(tw *tar.Writer, opts *entryOptions) error {
+	return writeFields(tw, fmt.Sprintf("%s-%s/desc", opts.Package.Name, opts.Version.Version), []keyValue{
+		{"FILENAME", opts.File.Name},
+		{"MD5SUM", opts.Blob.HashMD5},
+		{"SHA256SUM", opts.Blob.HashSHA256},
+		{"PGPSIG", opts.Signature},
+		{"CSIZE", fmt.Sprintf("%d", opts.Blob.Size)},
+		{"ISIZE", fmt.Sprintf("%d", opts.FileMetadata.InstalledSize)},
+		{"NAME", opts.Package.Name},
+		{"BASE", opts.FileMetadata.Base},
+		{"ARCH", opts.FileMetadata.Architecture},
+		{"VERSION", opts.Version.Version},
+		{"DESC", opts.VersionMetadata.Description},
+		{"URL", opts.VersionMetadata.ProjectURL},
+		{"LICENSE", strings.Join(opts.VersionMetadata.Licenses, "\n")},
+		{"GROUPS", strings.Join(opts.FileMetadata.Groups, "\n")},
+		{"BUILDDATE", fmt.Sprintf("%d", opts.FileMetadata.BuildDate)},
+		{"PACKAGER", opts.FileMetadata.Packager},
+		{"PROVIDES", strings.Join(opts.FileMetadata.Provides, "\n")},
+		{"DEPENDS", strings.Join(opts.FileMetadata.Depends, "\n")},
+		{"OPTDEPENDS", strings.Join(opts.FileMetadata.OptDepends, "\n")},
+		{"MAKEDEPENDS", strings.Join(opts.FileMetadata.MakeDepends, "\n")},
+		{"CHECKDEPENDS", strings.Join(opts.FileMetadata.CheckDepends, "\n")},
+		{"XDATA", strings.Join(opts.FileMetadata.XData, "\n")},
+	})
+}
+
+func writeFields(tw *tar.Writer, filename string, fields []keyValue) error {
+	buf := &bytes.Buffer{}
+	for _, kv := range fields {
+		if kv.Value == "" {
+			continue
+		}
+		fmt.Fprintf(buf, "%%%s%%\n%s\n\n", kv.Key, kv.Value)
+	}
+
+	if err := tw.WriteHeader(&tar.Header{
+		Name: filename,
+		Size: int64(buf.Len()),
+		Mode: int64(os.ModePerm),
+	}); err != nil {
+		return err
+	}
+
+	_, err := io.Copy(tw, buf)
+	return err
+}
diff --git a/services/packages/cleanup/cleanup.go b/services/packages/cleanup/cleanup.go
index d7c9355da5e7d..b7ba2b6ac4afc 100644
--- a/services/packages/cleanup/cleanup.go
+++ b/services/packages/cleanup/cleanup.go
@@ -16,6 +16,7 @@ import (
 	packages_module "code.gitea.io/gitea/modules/packages"
 	packages_service "code.gitea.io/gitea/services/packages"
 	alpine_service "code.gitea.io/gitea/services/packages/alpine"
+	arch_service "code.gitea.io/gitea/services/packages/arch"
 	cargo_service "code.gitea.io/gitea/services/packages/cargo"
 	container_service "code.gitea.io/gitea/services/packages/container"
 	debian_service "code.gitea.io/gitea/services/packages/debian"
@@ -120,18 +121,29 @@ func ExecuteCleanupRules(outerCtx context.Context) error {
 		}
 
 		if anyVersionDeleted {
-			if pcr.Type == packages_model.TypeDebian {
+			switch pcr.Type {
+			case packages_model.TypeDebian:
 				if err := debian_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
 					return fmt.Errorf("CleanupRule [%d]: debian.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
 				}
-			} else if pcr.Type == packages_model.TypeAlpine {
+			case packages_model.TypeAlpine:
 				if err := alpine_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
 					return fmt.Errorf("CleanupRule [%d]: alpine.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
 				}
-			} else if pcr.Type == packages_model.TypeRpm {
+			case packages_model.TypeRpm:
 				if err := rpm_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
 					return fmt.Errorf("CleanupRule [%d]: rpm.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
 				}
+			case packages_model.TypeArch:
+				release, err := arch_service.AquireRegistryLock(ctx, pcr.OwnerID)
+				if err != nil {
+					return err
+				}
+				defer release()
+
+				if err := arch_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil {
+					return fmt.Errorf("CleanupRule [%d]: arch.BuildAllRepositoryFiles failed: %w", pcr.ID, err)
+				}
 			}
 		}
 		return nil
diff --git a/services/packages/packages.go b/services/packages/packages.go
index 95579be34be5b..55351afce2943 100644
--- a/services/packages/packages.go
+++ b/services/packages/packages.go
@@ -355,6 +355,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p
 	switch packageType {
 	case packages_model.TypeAlpine:
 		typeSpecificSize = setting.Packages.LimitSizeAlpine
+	case packages_model.TypeArch:
+		typeSpecificSize = setting.Packages.LimitSizeArch
 	case packages_model.TypeCargo:
 		typeSpecificSize = setting.Packages.LimitSizeCargo
 	case packages_model.TypeChef:
diff --git a/templates/package/content/arch.tmpl b/templates/package/content/arch.tmpl
new file mode 100644
index 0000000000000..1c568cbb78339
--- /dev/null
+++ b/templates/package/content/arch.tmpl
@@ -0,0 +1,41 @@
+{{if eq .PackageDescriptor.Package.Type "arch"}}
+	<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.installation"}}</h4>
+	<div class="ui attached segment">
+		<div class="ui form">
+			<div class="field">
+				<label>{{svg "octicon-gear"}} {{ctx.Locale.Tr "packages.arch.registry"}}</label>
+				<div class="markup"><pre class="code-block"><code>[{{.PackageDescriptor.Owner.LowerName}}.{{.PackageRegistryHost}}]
+SigLevel = Optional TrustAll
+Server = <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/arch/$repo/$arch"></origin-url></code></pre></div>
+			</div>
+			<div class="field">
+				<label>{{svg "octicon-sync"}} {{ctx.Locale.Tr "packages.arch.install"}}</label>
+				<div class="markup"><pre class="code-block"><code>pacman -Sy {{.PackageDescriptor.Package.LowerName}}</code></pre></div>
+			</div>
+			<div class="field">
+				<label>{{ctx.Locale.Tr "packages.registry.documentation" "Arch" "https://docs.gitea.com/usage/packages/arch/"}}</label>
+			</div>
+		</div>
+	</div>
+
+	<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.arch.repository"}}</h4>
+	<div class="ui attached segment">
+		<table class="ui single line very basic table">
+			<tbody>
+				<tr>
+					<td class="collapsing"><h5>{{ctx.Locale.Tr "packages.arch.repository.repositories"}}</h5></td>
+					<td>{{StringUtils.Join .Repositories ", "}}</td>
+				</tr>
+				<tr>
+					<td class="collapsing"><h5>{{ctx.Locale.Tr "packages.arch.repository.architectures"}}</h5></td>
+					<td>{{StringUtils.Join .Architectures ", "}}</td>
+				</tr>
+			</tbody>
+		</table>
+	</div>
+
+	{{if .PackageDescriptor.Metadata.Description}}
+		<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.about"}}</h4>
+		<div class="ui attached segment">{{.PackageDescriptor.Metadata.Description}}</div>
+	{{end}}
+{{end}}
diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl
index 138fedecb3fc3..aaed25bfbd61d 100644
--- a/templates/package/content/container.tmpl
+++ b/templates/package/content/container.tmpl
@@ -5,13 +5,13 @@
 			<div class="field">
 				<label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.container.pull"}}</label>
 				{{if eq .PackageDescriptor.Metadata.Type "helm"}}
-				<div class="markup"><pre class="code-block"><code>helm pull oci://{{.RegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}} --version {{.PackageDescriptor.Version.LowerVersion}}</code></pre></div>
+				<div class="markup"><pre class="code-block"><code>helm pull oci://{{.PackageRegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}} --version {{.PackageDescriptor.Version.LowerVersion}}</code></pre></div>
 				{{else}}
 					{{$separator := ":"}}
 					{{if not .PackageDescriptor.Metadata.IsTagged}}
 						{{$separator = "@"}}
 					{{end}}
-					<div class="markup"><pre class="code-block"><code>docker pull {{.RegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}}{{$separator}}{{.PackageDescriptor.Version.LowerVersion}}</code></pre></div>
+					<div class="markup"><pre class="code-block"><code>docker pull {{.PackageRegistryHost}}/{{.PackageDescriptor.Owner.LowerName}}/{{.PackageDescriptor.Package.LowerName}}{{$separator}}{{.PackageDescriptor.Version.LowerVersion}}</code></pre></div>
 				{{end}}
 			</div>
 			<div class="field">
diff --git a/templates/package/metadata/alpine.tmpl b/templates/package/metadata/alpine.tmpl
index 3e7f10f66a6d5..c9174948b1e3a 100644
--- a/templates/package/metadata/alpine.tmpl
+++ b/templates/package/metadata/alpine.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "alpine"}}
-	{{if .PackageDescriptor.Metadata.Maintainer}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Maintainer}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Maintainer}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Maintainer}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/arch.tmpl b/templates/package/metadata/arch.tmpl
new file mode 100644
index 0000000000000..2aea036ec2dd2
--- /dev/null
+++ b/templates/package/metadata/arch.tmpl
@@ -0,0 +1,4 @@
+{{if eq .PackageDescriptor.Package.Type "arch"}}
+	{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{$.locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+{{end}}
diff --git a/templates/package/metadata/cargo.tmpl b/templates/package/metadata/cargo.tmpl
index 5ad3c20a932fb..f7dd887a24d6e 100644
--- a/templates/package/metadata/cargo.tmpl
+++ b/templates/package/metadata/cargo.tmpl
@@ -1,7 +1,7 @@
 {{if eq .PackageDescriptor.Package.Type "cargo"}}
-	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/chef.tmpl b/templates/package/metadata/chef.tmpl
index 23a9ce3ec0bbe..6bf606ca487f5 100644
--- a/templates/package/metadata/chef.tmpl
+++ b/templates/package/metadata/chef.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "chef"}}
-	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/composer.tmpl b/templates/package/metadata/composer.tmpl
index 0f6ff9d6f2d75..e69e91745fc0b 100644
--- a/templates/package/metadata/composer.tmpl
+++ b/templates/package/metadata/composer.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "composer"}}
-	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.Name}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.Homepage}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.Homepage}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{range .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}</div>{{end}}
+	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.Name}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Homepage}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.Homepage}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{range .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/conan.tmpl b/templates/package/metadata/conan.tmpl
index 4e05ec2587cb3..8b15375553931 100644
--- a/templates/package/metadata/conan.tmpl
+++ b/templates/package/metadata/conan.tmpl
@@ -1,6 +1,6 @@
 {{if eq .PackageDescriptor.Package.Type "conan"}}
-	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/conda.tmpl b/templates/package/metadata/conda.tmpl
index 3628686e13acd..4add9453fa7d5 100644
--- a/templates/package/metadata/conda.tmpl
+++ b/templates/package/metadata/conda.tmpl
@@ -1,6 +1,6 @@
 {{if eq .PackageDescriptor.Package.Type "conda"}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/container.tmpl b/templates/package/metadata/container.tmpl
index f5abb7ef6e5f5..ecc17964d781a 100644
--- a/templates/package/metadata/container.tmpl
+++ b/templates/package/metadata/container.tmpl
@@ -1,9 +1,9 @@
 {{if eq .PackageDescriptor.Package.Type "container"}}
-	<div class="item" title="{{ctx.Locale.Tr "packages.container.details.type"}}">{{svg "octicon-package" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Type.Name}}</div>
-	{{if .PackageDescriptor.Metadata.Platform}}<div class="item" title="{{ctx.Locale.Tr "packages.container.details.platform"}}">{{svg "octicon-cpu" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Platform}}</div>{{end}}
-	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.Licenses}}<div class="item">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Licenses}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
+	<div class="item" title="{{ctx.Locale.Tr "packages.container.details.type"}}">{{svg "octicon-package"}} {{.PackageDescriptor.Metadata.Type.Name}}</div>
+	{{if .PackageDescriptor.Metadata.Platform}}<div class="item" title="{{ctx.Locale.Tr "packages.container.details.platform"}}">{{svg "octicon-cpu"}} {{.PackageDescriptor.Metadata.Platform}}</div>{{end}}
+	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Licenses}}<div class="item">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.Licenses}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/cran.tmpl b/templates/package/metadata/cran.tmpl
index 1d5a11e196725..3ada7ac743a06 100644
--- a/templates/package/metadata/cran.tmpl
+++ b/templates/package/metadata/cran.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "cran"}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
-	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.}}</div>{{end}}
-	{{range .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.}}</div>{{end}}
+	{{range .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/debian.tmpl b/templates/package/metadata/debian.tmpl
index 3cd845c9fe928..d35e8b00daadd 100644
--- a/templates/package/metadata/debian.tmpl
+++ b/templates/package/metadata/debian.tmpl
@@ -1,4 +1,4 @@
 {{if eq .PackageDescriptor.Package.Type "debian"}}
-	{{if .PackageDescriptor.Metadata.Maintainer}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Maintainer}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.Maintainer}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Maintainer}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/helm.tmpl b/templates/package/metadata/helm.tmpl
index 50ea484999108..b3b3f348cf1f2 100644
--- a/templates/package/metadata/helm.tmpl
+++ b/templates/package/metadata/helm.tmpl
@@ -1,4 +1,4 @@
 {{if eq .PackageDescriptor.Package.Type "helm"}}
-	{{range .PackageDescriptor.Metadata.Maintainers}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.Name}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.Home}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.Home}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{range .PackageDescriptor.Metadata.Maintainers}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.Name}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Home}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.Home}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/maven.tmpl b/templates/package/metadata/maven.tmpl
index 36412723d24f8..33662be7caf4e 100644
--- a/templates/package/metadata/maven.tmpl
+++ b/templates/package/metadata/maven.tmpl
@@ -1,8 +1,8 @@
 {{if and (eq .PackageDescriptor.Package.Type "maven") (not .PackageDescriptor.Metadata)}}
-	<div class="item">{{svg "octicon-note" 16 "tw-mr-2"}} {{ctx.Locale.Tr "packages.no_metadata"}}</div>
+	<div class="item">{{svg "octicon-note"}} {{ctx.Locale.Tr "packages.no_metadata"}}</div>
 {{end}}
 {{if and (eq .PackageDescriptor.Package.Type "maven") .PackageDescriptor.Metadata}}
-	{{if .PackageDescriptor.Metadata.Name}}<div class="item">{{svg "octicon-note" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Name}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Name}}<div class="item">{{svg "octicon-note"}} {{.PackageDescriptor.Metadata.Name}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/npm.tmpl b/templates/package/metadata/npm.tmpl
index df37504e374d3..ff245f2b03ef2 100644
--- a/templates/package/metadata/npm.tmpl
+++ b/templates/package/metadata/npm.tmpl
@@ -1,8 +1,8 @@
 {{if eq .PackageDescriptor.Package.Type "npm"}}
-	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 	{{range .PackageDescriptor.VersionProperties}}
-		{{if eq .Name "npm.tag"}}<div class="item" title="{{ctx.Locale.Tr "packages.npm.details.tag"}}">{{svg "octicon-versions" 16 "tw-mr-2"}} {{.Value}}</div>{{end}}
+		{{if eq .Name "npm.tag"}}<div class="item" title="{{ctx.Locale.Tr "packages.npm.details.tag"}}">{{svg "octicon-versions"}} {{.Value}}</div>{{end}}
 	{{end}}
 {{end}}
diff --git a/templates/package/metadata/nuget.tmpl b/templates/package/metadata/nuget.tmpl
index 5534577bd26ef..2d18528f857e4 100644
--- a/templates/package/metadata/nuget.tmpl
+++ b/templates/package/metadata/nuget.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "nuget"}}
-	{{if .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Authors}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Authors}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/pub.tmpl b/templates/package/metadata/pub.tmpl
index 16f7cec370407..e54207c4c60e6 100644
--- a/templates/package/metadata/pub.tmpl
+++ b/templates/package/metadata/pub.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "pub"}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.documentation_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/pypi.tmpl b/templates/package/metadata/pypi.tmpl
index 3d9b213907f81..9dfac07cbfe6c 100644
--- a/templates/package/metadata/pypi.tmpl
+++ b/templates/package/metadata/pypi.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "pypi"}}
-	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/rpm.tmpl b/templates/package/metadata/rpm.tmpl
index eda8a489f3cdf..65093933a92b2 100644
--- a/templates/package/metadata/rpm.tmpl
+++ b/templates/package/metadata/rpm.tmpl
@@ -1,4 +1,4 @@
 {{if eq .PackageDescriptor.Package.Type "rpm"}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/rubygems.tmpl b/templates/package/metadata/rubygems.tmpl
index 9b11287691c34..04fc3695abe08 100644
--- a/templates/package/metadata/rubygems.tmpl
+++ b/templates/package/metadata/rubygems.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "rubygems"}}
-	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>	{{end}}
-	{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "tw-mr-2"}} {{.}}</div>{{end}}
+	{{range .PackageDescriptor.Metadata.Authors}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>	{{end}}
+	{{range .PackageDescriptor.Metadata.Licenses}}<div class="item" title="{{ctx.Locale.Tr "packages.details.license"}}">{{svg "octicon-law"}} {{.}}</div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/swift.tmpl b/templates/package/metadata/swift.tmpl
index fdffb6dedec3f..fe28759de3f5b 100644
--- a/templates/package/metadata/swift.tmpl
+++ b/templates/package/metadata/swift.tmpl
@@ -1,4 +1,4 @@
 {{if eq .PackageDescriptor.Package.Type "swift"}}
-	{{if .PackageDescriptor.Metadata.Author.String}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.Author.String}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/metadata/vagrant.tmpl b/templates/package/metadata/vagrant.tmpl
index 4628a2dcbb0f6..795ab33da9350 100644
--- a/templates/package/metadata/vagrant.tmpl
+++ b/templates/package/metadata/vagrant.tmpl
@@ -1,5 +1,5 @@
 {{if eq .PackageDescriptor.Package.Type "vagrant"}}
-	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "tw-mr-2"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
-	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
-	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{ctx.Locale.Tr "packages.details.author"}}">{{svg "octicon-person"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
+	{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.project_site"}}</a></div>{{end}}
+	{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{ctx.Locale.Tr "packages.details.repository_site"}}</a></div>{{end}}
 {{end}}
diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl
index 0f06d7afbd390..9e92207466d96 100644
--- a/templates/package/view.tmpl
+++ b/templates/package/view.tmpl
@@ -1,12 +1,10 @@
 {{template "base/head" .}}
-<div role="main" aria-label="{{.Title}}" class="page-content repository view issue packages">
+<div role="main" aria-label="{{.Title}}" class="page-content repository packages">
 	{{template "shared/user/org_profile_avatar" .}}
 	<div class="ui container">
 		{{template "user/overview/header" .}}
 		<div class="issue-title-header">
-			<div class="issue-title">
-				<h1>{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</h1>
-			</div>
+			<h1>{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</h1>
 			<div>
 				{{$timeStr := DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}}
 				{{if .HasRepositoryAccess}}
@@ -19,6 +17,7 @@
 		<div class="issue-content">
 			<div class="issue-content-left">
 				{{template "package/content/alpine" .}}
+				{{template "package/content/arch" .}}
 				{{template "package/content/cargo" .}}
 				{{template "package/content/chef" .}}
 				{{template "package/content/composer" .}}
@@ -42,14 +41,15 @@
 			</div>
 			<div class="issue-content-right ui segment">
 				<strong>{{ctx.Locale.Tr "packages.details"}}</strong>
-				<div class="ui relaxed list">
-					<div class="item">{{svg .PackageDescriptor.Package.Type.SVGName 16 "tw-mr-2"}} {{.PackageDescriptor.Package.Type.Name}}</div>
+				<div class="ui relaxed list flex-items-block">
+					<div class="item">{{svg .PackageDescriptor.Package.Type.SVGName}} {{.PackageDescriptor.Package.Type.Name}}</div>
 					{{if .HasRepositoryAccess}}
-					<div class="item">{{svg "octicon-repo" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Repository.Link}}">{{.PackageDescriptor.Repository.FullName}}</a></div>
+					<div class="item">{{svg "octicon-repo"}} <a href="{{.PackageDescriptor.Repository.Link}}">{{.PackageDescriptor.Repository.FullName}}</a></div>
 					{{end}}
-					<div class="item">{{svg "octicon-calendar" 16 "tw-mr-2"}} {{DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}}</div>
-					<div class="item">{{svg "octicon-download" 16 "tw-mr-2"}} {{.PackageDescriptor.Version.DownloadCount}}</div>
+					<div class="item">{{svg "octicon-calendar"}} {{DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}}</div>
+					<div class="item">{{svg "octicon-download"}} {{.PackageDescriptor.Version.DownloadCount}}</div>
 					{{template "package/metadata/alpine" .}}
+					{{template "package/metadata/arch" .}}
 					{{template "package/metadata/cargo" .}}
 					{{template "package/metadata/chef" .}}
 					{{template "package/metadata/composer" .}}
@@ -70,7 +70,7 @@
 					{{template "package/metadata/swift" .}}
 					{{template "package/metadata/vagrant" .}}
 					{{if not (and (eq .PackageDescriptor.Package.Type "container") .PackageDescriptor.Metadata.Manifests)}}
-					<div class="item">{{svg "octicon-database" 16 "tw-mr-2"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}</div>
+					<div class="item">{{svg "octicon-database"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}</div>
 					{{end}}
 				</div>
 				{{if not (eq .PackageDescriptor.Package.Type "container")}}
@@ -98,12 +98,12 @@
 				</div>
 				{{if or .CanWritePackages .HasRepositoryAccess}}
 					<div class="divider"></div>
-					<div class="ui relaxed list">
+					<div class="ui relaxed list flex-items-block">
 						{{if .HasRepositoryAccess}}
-						<div class="item">{{svg "octicon-issue-opened" 16 "tw-mr-2"}} <a href="{{.PackageDescriptor.Repository.Link}}/issues">{{ctx.Locale.Tr "repo.issues"}}</a></div>
+						<div class="item">{{svg "octicon-issue-opened"}} <a href="{{.PackageDescriptor.Repository.Link}}/issues">{{ctx.Locale.Tr "repo.issues"}}</a></div>
 						{{end}}
 						{{if .CanWritePackages}}
-						<div class="item">{{svg "octicon-tools" 16 "tw-mr-2"}} <a href="{{.Link}}/settings">{{ctx.Locale.Tr "repo.settings"}}</a></div>
+						<div class="item">{{svg "octicon-tools"}} <a href="{{.Link}}/settings">{{ctx.Locale.Tr "repo.settings"}}</a></div>
 						{{end}}
 					</div>
 				{{end}}
diff --git a/tests/integration/api_packages_arch_test.go b/tests/integration/api_packages_arch_test.go
new file mode 100644
index 0000000000000..9c7a9dd19dec1
--- /dev/null
+++ b/tests/integration/api_packages_arch_test.go
@@ -0,0 +1,302 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+	"archive/tar"
+	"bytes"
+	"compress/gzip"
+	"fmt"
+	"io"
+	"net/http"
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	"code.gitea.io/gitea/models/packages"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	arch_module "code.gitea.io/gitea/modules/packages/arch"
+	arch_service "code.gitea.io/gitea/services/packages/arch"
+	"code.gitea.io/gitea/tests"
+
+	"github.com/klauspost/compress/zstd"
+	"github.com/stretchr/testify/assert"
+	"github.com/ulikunitz/xz"
+)
+
+func TestPackageArch(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+	packageName := "gitea-test"
+	packageVersion := "1.4.1-r3"
+
+	createPackage := func(compression, name, version, architecture string) []byte {
+		var buf bytes.Buffer
+		var cw io.WriteCloser
+		switch compression {
+		case "zst":
+			cw, _ = zstd.NewWriter(&buf)
+		case "xz":
+			cw, _ = xz.NewWriter(&buf)
+		case "gz":
+			cw = gzip.NewWriter(&buf)
+		}
+		tw := tar.NewWriter(cw)
+
+		info := []byte(`pkgname = ` + name + `
+pkgbase = ` + name + `
+pkgver = ` + version + `
+pkgdesc = Description
+# comment
+builddate = 1678834800
+size = 8
+arch = ` + architecture + `
+license = MIT`)
+
+		hdr := &tar.Header{
+			Name: ".PKGINFO",
+			Mode: 0o600,
+			Size: int64(len(info)),
+		}
+		tw.WriteHeader(hdr)
+		tw.Write(info)
+
+		for _, file := range []string{"etc/dummy", "opt/file/bin"} {
+			hdr := &tar.Header{
+				Name: file,
+				Mode: 0o600,
+				Size: 4,
+			}
+			tw.WriteHeader(hdr)
+			tw.Write([]byte("test"))
+		}
+
+		tw.Close()
+		cw.Close()
+
+		return buf.Bytes()
+	}
+
+	compressions := []string{"gz", "xz", "zst"}
+	repositories := []string{"main", "testing", "with/slash", ""}
+
+	rootURL := fmt.Sprintf("/api/packages/%s/arch", user.Name)
+
+	t.Run("RepositoryKey", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		req := NewRequest(t, "GET", rootURL+"/repository.key")
+		resp := MakeRequest(t, req, http.StatusOK)
+
+		assert.Equal(t, "application/pgp-keys", resp.Header().Get("Content-Type"))
+		assert.Contains(t, resp.Body.String(), "-----BEGIN PGP PUBLIC KEY BLOCK-----")
+	})
+
+	contentAarch64Gz := createPackage("gz", packageName, packageVersion, "aarch64")
+	for _, compression := range compressions {
+		contentAarch64 := createPackage(compression, packageName, packageVersion, "aarch64")
+		contentAny := createPackage(compression, packageName+"_"+arch_module.AnyArch, packageVersion, arch_module.AnyArch)
+
+		for _, repository := range repositories {
+			t.Run(fmt.Sprintf("[%s,%s]", repository, compression), func(t *testing.T) {
+				t.Run("Upload", func(t *testing.T) {
+					defer tests.PrintCurrentTest(t)()
+
+					uploadURL := fmt.Sprintf("%s/%s", rootURL, repository)
+
+					req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{}))
+					MakeRequest(t, req, http.StatusUnauthorized)
+
+					req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})).
+						AddBasicAuth(user.Name)
+					MakeRequest(t, req, http.StatusBadRequest)
+
+					req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64)).
+						AddBasicAuth(user.Name)
+					MakeRequest(t, req, http.StatusCreated)
+
+					pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeArch)
+					assert.NoError(t, err)
+					assert.Len(t, pvs, 1)
+
+					pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+					assert.NoError(t, err)
+					assert.Nil(t, pd.SemVer)
+					assert.IsType(t, &arch_module.VersionMetadata{}, pd.Metadata)
+					assert.Equal(t, packageName, pd.Package.Name)
+					assert.Equal(t, packageVersion, pd.Version.Version)
+
+					pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+					assert.NoError(t, err)
+					assert.NotEmpty(t, pfs)
+					assert.Condition(t, func() bool {
+						seen := false
+						expectedFilename := fmt.Sprintf("%s-%s-aarch64.pkg.tar.%s", packageName, packageVersion, compression)
+						expectedCompositeKey := fmt.Sprintf("%s|aarch64", repository)
+						for _, pf := range pfs {
+							if pf.Name == expectedFilename && pf.CompositeKey == expectedCompositeKey {
+								if seen {
+									return false
+								}
+								seen = true
+
+								assert.True(t, pf.IsLead)
+
+								pfps, err := packages.GetProperties(db.DefaultContext, packages.PropertyTypeFile, pf.ID)
+								assert.NoError(t, err)
+
+								for _, pfp := range pfps {
+									switch pfp.Name {
+									case arch_module.PropertyRepository:
+										assert.Equal(t, repository, pfp.Value)
+									case arch_module.PropertyArchitecture:
+										assert.Equal(t, "aarch64", pfp.Value)
+									}
+								}
+							}
+						}
+						return seen
+					})
+
+					req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64)).
+						AddBasicAuth(user.Name)
+					MakeRequest(t, req, http.StatusConflict)
+
+					// Add same package with different compression leads to conflict
+					req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(contentAarch64Gz)).
+						AddBasicAuth(user.Name)
+					MakeRequest(t, req, http.StatusConflict)
+				})
+
+				readIndexContent := func(r io.Reader) (map[string]string, error) {
+					gzr, err := gzip.NewReader(r)
+					if err != nil {
+						return nil, err
+					}
+
+					content := make(map[string]string)
+
+					tr := tar.NewReader(gzr)
+					for {
+						hd, err := tr.Next()
+						if err == io.EOF {
+							break
+						}
+						if err != nil {
+							return nil, err
+						}
+
+						buf, err := io.ReadAll(tr)
+						if err != nil {
+							return nil, err
+						}
+
+						content[hd.Name] = string(buf)
+					}
+
+					return content, nil
+				}
+
+				t.Run("Index", func(t *testing.T) {
+					defer tests.PrintCurrentTest(t)()
+
+					req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename))
+					resp := MakeRequest(t, req, http.StatusOK)
+
+					content, err := readIndexContent(resp.Body)
+					assert.NoError(t, err)
+
+					desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)]
+					assert.True(t, has)
+					assert.Contains(t, desc, "%FILENAME%\n"+fmt.Sprintf("%s-%s-aarch64.pkg.tar.%s", packageName, packageVersion, compression)+"\n\n")
+					assert.Contains(t, desc, "%NAME%\n"+packageName+"\n\n")
+					assert.Contains(t, desc, "%VERSION%\n"+packageVersion+"\n\n")
+					assert.Contains(t, desc, "%ARCH%\naarch64\n")
+					assert.NotContains(t, desc, "%ARCH%\n"+arch_module.AnyArch+"\n")
+					assert.Contains(t, desc, "%LICENSE%\nMIT\n")
+
+					files, has := content[fmt.Sprintf("%s-%s/files", packageName, packageVersion)]
+					assert.True(t, has)
+					assert.Contains(t, files, "%FILES%\netc/dummy\nopt/file/bin\n\n")
+
+					for _, indexFile := range []string{
+						arch_service.IndexArchiveFilename,
+						arch_service.IndexArchiveFilename + ".tar.gz",
+						"index.db",
+						"index.db.tar.gz",
+						"index.files",
+						"index.files.tar.gz",
+					} {
+						req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, indexFile))
+						MakeRequest(t, req, http.StatusOK)
+
+						req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s.sig", rootURL, repository, indexFile))
+						MakeRequest(t, req, http.StatusOK)
+					}
+				})
+
+				t.Run("Download", func(t *testing.T) {
+					defer tests.PrintCurrentTest(t)()
+
+					req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pkg.tar.%s", rootURL, repository, packageName, packageVersion, compression))
+					MakeRequest(t, req, http.StatusOK)
+
+					req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s-%s-aarch64.pkg.tar.%s.sig", rootURL, repository, packageName, packageVersion, compression))
+					MakeRequest(t, req, http.StatusOK)
+				})
+
+				t.Run("Any", func(t *testing.T) {
+					defer tests.PrintCurrentTest(t)()
+
+					req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/%s", rootURL, repository), bytes.NewReader(contentAny)).
+						AddBasicAuth(user.Name)
+					MakeRequest(t, req, http.StatusCreated)
+
+					req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename))
+					resp := MakeRequest(t, req, http.StatusOK)
+
+					content, err := readIndexContent(resp.Body)
+					assert.NoError(t, err)
+
+					desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)]
+					assert.True(t, has)
+					assert.Contains(t, desc, "%NAME%\n"+packageName+"\n\n")
+					assert.Contains(t, desc, "%ARCH%\naarch64\n")
+
+					desc, has = content[fmt.Sprintf("%s-%s/desc", packageName+"_"+arch_module.AnyArch, packageVersion)]
+					assert.True(t, has)
+					assert.Contains(t, desc, "%NAME%\n"+packageName+"_any\n\n")
+					assert.Contains(t, desc, "%ARCH%\n"+arch_module.AnyArch+"\n")
+
+					// "any" architecture package should be available with every architecture requested
+					for _, arch := range []string{arch_module.AnyArch, "aarch64", "myarch"} {
+						req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s-%s-any.pkg.tar.%s", rootURL, repository, arch, packageName+"_"+arch_module.AnyArch, packageVersion, compression))
+						MakeRequest(t, req, http.StatusOK)
+					}
+
+					req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/%s/any", rootURL, repository, packageName+"_"+arch_module.AnyArch, packageVersion)).
+						AddBasicAuth(user.Name)
+					MakeRequest(t, req, http.StatusNoContent)
+				})
+
+				t.Run("Delete", func(t *testing.T) {
+					defer tests.PrintCurrentTest(t)()
+
+					req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/%s/aarch64", rootURL, repository, packageName, packageVersion))
+					MakeRequest(t, req, http.StatusUnauthorized)
+
+					req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s/%s/aarch64", rootURL, repository, packageName, packageVersion)).
+						AddBasicAuth(user.Name)
+					MakeRequest(t, req, http.StatusNoContent)
+
+					// Deleting the last file of an architecture should remove that index
+					req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename))
+					MakeRequest(t, req, http.StatusNotFound)
+				})
+			})
+		}
+	}
+}
diff --git a/web_src/css/base.css b/web_src/css/base.css
index babbf4c89dcab..8f5ef51c4aa8b 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -1386,7 +1386,7 @@ table th[data-sortt-desc] .svg {
 .flex-text-block {
   display: flex;
   align-items: center;
-  gap: .25rem;
+  gap: .5rem;
   min-width: 0;
 }
 
diff --git a/web_src/svg/gitea-arch.svg b/web_src/svg/gitea-arch.svg
new file mode 100644
index 0000000000000..ba8254d8049e7
--- /dev/null
+++ b/web_src/svg/gitea-arch.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#1793d1" d="M256 72c-14 35-23 57-39 91 10 11 22 23 41 36-21-8-35-17-45-26-21 43-53 103-117 220 50-30 90-48 127-55-2-7-3-14-3-22v-1c1-33 18-58 38-56 20 1 36 29 35 62l-2 17c36 7 75 26 125 54l-27-50c-13-10-27-23-55-38 19 5 33 11 44 17-86-159-93-180-122-250z"/></svg>
\ No newline at end of file