diff --git a/routers/api/packages/pypi/pypi.go b/routers/api/packages/pypi/pypi.go
index 5ea86071a9..19aa54628d 100644
--- a/routers/api/packages/pypi/pypi.go
+++ b/routers/api/packages/pypi/pypi.go
@@ -10,6 +10,7 @@ import (
 	"regexp"
 	"sort"
 	"strings"
+	"unicode"
 
 	packages_model "code.gitea.io/gitea/models/packages"
 	packages_module "code.gitea.io/gitea/modules/packages"
@@ -139,9 +140,30 @@ func UploadPackageFile(ctx *context.Context) {
 		return
 	}
 
-	projectURL := ctx.Req.FormValue("home_page")
-	if !validation.IsValidURL(projectURL) {
-		projectURL = ""
+	// Ensure ctx.Req.Form exists.
+	_ = ctx.Req.ParseForm()
+
+	var homepageURL string
+	projectURLs := ctx.Req.Form["project_urls"]
+	for _, purl := range projectURLs {
+		label, url, found := strings.Cut(purl, ",")
+		if !found {
+			continue
+		}
+		if normalizeLabel(label) != "homepage" {
+			continue
+		}
+		homepageURL = strings.TrimSpace(url)
+		break
+	}
+
+	if len(homepageURL) == 0 {
+		// TODO: Home-page is a deprecated metadata field. Remove this branch once it's no longer apart of the spec.
+		homepageURL = ctx.Req.FormValue("home_page")
+	}
+
+	if !validation.IsValidURL(homepageURL) {
+		homepageURL = ""
 	}
 
 	_, _, err = packages_service.CreatePackageOrAddFileToExisting(
@@ -160,7 +182,7 @@ func UploadPackageFile(ctx *context.Context) {
 				Description:     ctx.Req.FormValue("description"),
 				LongDescription: ctx.Req.FormValue("long_description"),
 				Summary:         ctx.Req.FormValue("summary"),
-				ProjectURL:      projectURL,
+				ProjectURL:      homepageURL,
 				License:         ctx.Req.FormValue("license"),
 				RequiresPython:  ctx.Req.FormValue("requires_python"),
 			},
@@ -189,6 +211,23 @@ func UploadPackageFile(ctx *context.Context) {
 	ctx.Status(http.StatusCreated)
 }
 
+// Normalizes a Project-URL label.
+// See https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization.
+func normalizeLabel(label string) string {
+	var builder strings.Builder
+
+	// "A label is normalized by deleting all ASCII punctuation and whitespace, and then converting the result
+	// to lowercase."
+	for _, r := range label {
+		if unicode.IsPunct(r) || unicode.IsSpace(r) {
+			continue
+		}
+		builder.WriteRune(unicode.ToLower(r))
+	}
+
+	return builder.String()
+}
+
 func isValidNameAndVersion(packageName, packageVersion string) bool {
 	return nameMatcher.MatchString(packageName) && versionMatcher.MatchString(packageVersion)
 }
diff --git a/routers/api/packages/pypi/pypi_test.go b/routers/api/packages/pypi/pypi_test.go
index 3023692177..786105693f 100644
--- a/routers/api/packages/pypi/pypi_test.go
+++ b/routers/api/packages/pypi/pypi_test.go
@@ -36,3 +36,13 @@ func TestIsValidNameAndVersion(t *testing.T) {
 	assert.False(t, isValidNameAndVersion("test-name", "1.0.1aa"))
 	assert.False(t, isValidNameAndVersion("test-name", "1.0.0-alpha.beta"))
 }
+
+func TestNormalizeLabel(t *testing.T) {
+	// Cases fetched from https://packaging.python.org/en/latest/specifications/well-known-project-urls/#label-normalization.
+	assert.Equal(t, "homepage", normalizeLabel("Homepage"))
+	assert.Equal(t, "homepage", normalizeLabel("Home-page"))
+	assert.Equal(t, "homepage", normalizeLabel("Home page"))
+	assert.Equal(t, "changelog", normalizeLabel("Change_Log"))
+	assert.Equal(t, "whatsnew", normalizeLabel("What's New?"))
+	assert.Equal(t, "github", normalizeLabel("github"))
+}
diff --git a/tests/integration/api_packages_pypi_test.go b/tests/integration/api_packages_pypi_test.go
index e973f6a52a..2dabb5005b 100644
--- a/tests/integration/api_packages_pypi_test.go
+++ b/tests/integration/api_packages_pypi_test.go
@@ -32,15 +32,16 @@ func TestPackagePyPI(t *testing.T) {
 	packageVersion := "1!1.0.1+r1234"
 	packageAuthor := "KN4CK3R"
 	packageDescription := "Test Description"
+	projectURL := "https://example.com"
 
 	content := "test"
 	hashSHA256 := "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
 
 	root := fmt.Sprintf("/api/packages/%s/pypi", user.Name)
 
-	uploadFile := func(t *testing.T, filename, content string, expectedStatus int) {
-		body := &bytes.Buffer{}
-		writer := multipart.NewWriter(body)
+	createBasicMultipartFile := func(filename, packageName, content string) (body *bytes.Buffer, writer *multipart.Writer, closer func() error) {
+		body = &bytes.Buffer{}
+		writer = multipart.NewWriter(body)
 		part, _ := writer.CreateFormFile("content", filename)
 		_, _ = io.Copy(part, strings.NewReader(content))
 
@@ -52,14 +53,27 @@ func TestPackagePyPI(t *testing.T) {
 		writer.WriteField("sha256_digest", hashSHA256)
 		writer.WriteField("requires_python", "3.6")
 
-		_ = writer.Close()
+		return body, writer, writer.Close
+	}
 
+	uploadHelper := func(t *testing.T, body *bytes.Buffer, contentType string, expectedStatus int) {
 		req := NewRequestWithBody(t, "POST", root, body).
-			SetHeader("Content-Type", writer.FormDataContentType()).
+			SetHeader("Content-Type", contentType).
 			AddBasicAuth(user.Name)
 		MakeRequest(t, req, expectedStatus)
 	}
 
+	uploadFile := func(t *testing.T, filename, content string, expectedStatus int) {
+		body, writer, closeFunc := createBasicMultipartFile(filename, packageName, content)
+
+		writer.WriteField("project_urls", "DOCUMENTATION , https://readthedocs.org")
+		writer.WriteField("project_urls", fmt.Sprintf("Home-page, %s", projectURL))
+
+		_ = closeFunc()
+
+		uploadHelper(t, body, writer.FormDataContentType(), expectedStatus)
+	}
+
 	t.Run("Upload", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 
@@ -74,6 +88,7 @@ func TestPackagePyPI(t *testing.T) {
 		assert.NoError(t, err)
 		assert.Nil(t, pd.SemVer)
 		assert.IsType(t, &pypi.Metadata{}, pd.Metadata)
+		assert.Equal(t, projectURL, pd.Metadata.(*pypi.Metadata).ProjectURL)
 		assert.Equal(t, packageName, pd.Package.Name)
 		assert.Equal(t, packageVersion, pd.Version.Version)
 
@@ -133,6 +148,48 @@ func TestPackagePyPI(t *testing.T) {
 		uploadFile(t, "test.tar.gz", content, http.StatusConflict)
 	})
 
+	t.Run("UploadUsingDeprecatedHomepageMetadata", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		pkgName := "homepage-package"
+		body, writer, closeFunc := createBasicMultipartFile("test.whl", pkgName, content)
+
+		writer.WriteField("home_page", projectURL)
+
+		_ = closeFunc()
+
+		uploadHelper(t, body, writer.FormDataContentType(), http.StatusCreated)
+
+		pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, pkgName)
+		assert.NoError(t, err)
+		assert.Len(t, pvs, 1)
+
+		pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+		assert.NoError(t, err)
+		assert.IsType(t, &pypi.Metadata{}, pd.Metadata)
+		assert.Equal(t, projectURL, pd.Metadata.(*pypi.Metadata).ProjectURL)
+	})
+
+	t.Run("UploadWithoutAnyHomepageURLMetadata", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		pkgName := "no-project-url-or-homepage-package"
+		body, writer, closeFunc := createBasicMultipartFile("test.whl", pkgName, content)
+
+		_ = closeFunc()
+
+		uploadHelper(t, body, writer.FormDataContentType(), http.StatusCreated)
+
+		pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, pkgName)
+		assert.NoError(t, err)
+		assert.Len(t, pvs, 1)
+
+		pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+		assert.NoError(t, err)
+		assert.IsType(t, &pypi.Metadata{}, pd.Metadata)
+		assert.Empty(t, pd.Metadata.(*pypi.Metadata).ProjectURL)
+	})
+
 	t.Run("Download", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 
@@ -147,7 +204,7 @@ func TestPackagePyPI(t *testing.T) {
 		downloadFile("test.whl")
 		downloadFile("test.tar.gz")
 
-		pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypePyPI)
+		pvs, err := packages.GetVersionsByPackageName(db.DefaultContext, user.ID, packages.TypePyPI, packageName)
 		assert.NoError(t, err)
 		assert.Len(t, pvs, 1)
 		assert.Equal(t, int64(2), pvs[0].DownloadCount)