internal/frontend: use new model tables to build package pages

Use the results of postgres.GetDirectoryNew to build
package pages.

Change-Id: Iff812416f4fb879baa16536639a80810fc1248b2
Reviewed-on: https://team-review.git.corp.google.com/c/golang/discovery/+/747780
Reviewed-by: Julie Qiu <julieqiu@google.com>
diff --git a/internal/datasource.go b/internal/datasource.go
index 58177e9..b3acd16 100644
--- a/internal/datasource.go
+++ b/internal/datasource.go
@@ -20,6 +20,11 @@
 	// package paths satisfy this query, it should prefer the module with
 	// the longest path.
 	GetDirectory(ctx context.Context, dirPath, modulePath, version string, fields FieldSet) (_ *Directory, err error)
+
+	// GetDirectoryNew returns information about a directory, which may also be a module and/or package.
+	// The module and version must both be known.
+	GetDirectoryNew(ctx context.Context, dirPath, modulePath, version string) (_ *VersionedDirectory, err error)
+
 	// GetImportedBy returns a slice of import paths corresponding to packages
 	// that import the given package path (at any version).
 	GetImportedBy(ctx context.Context, pkgPath, version string, limit int) ([]string, error)
diff --git a/internal/discovery.go b/internal/discovery.go
index d4a5ea2..476f698 100644
--- a/internal/discovery.go
+++ b/internal/discovery.go
@@ -154,6 +154,12 @@
 	Imports       []string
 }
 
+// VersionedPackageNew is a PackageNew along with associated module information.
+type VersionedPackageNew struct {
+	PackageNew
+	ModuleInfo
+}
+
 // Documentation is the rendered documentation for a given package
 // for a specific GOOS and GOARCH.
 type Documentation struct {
diff --git a/internal/frontend/doc.go b/internal/frontend/doc.go
index 74f911f..baabeef 100644
--- a/internal/frontend/doc.go
+++ b/internal/frontend/doc.go
@@ -40,6 +40,19 @@
 	}
 }
 
+// fetchDocumentationDetails returnsNew a DocumentationDetails constructed from doc.
+func fetchDocumentationDetailsNew(doc *internal.Documentation) *DocumentationDetails {
+	docHTML := doc.HTML
+	if addDocQueryParam {
+		docHTML = hackUpDocumentation(docHTML)
+	}
+	return &DocumentationDetails{
+		GOOS:          doc.GOOS,
+		GOARCH:        doc.GOARCH,
+		Documentation: template.HTML(docHTML),
+	}
+}
+
 // packageLinkRegexp matches cross-package identifier links that have been
 // generated by the dochtml package. At the time this hack was added, these
 // links are all constructed to have either the form
diff --git a/internal/frontend/header.go b/internal/frontend/header.go
index af6ebf3..17c6de4 100644
--- a/internal/frontend/header.go
+++ b/internal/frontend/header.go
@@ -79,6 +79,42 @@
 	}, nil
 }
 
+// createPackageNew returns a *Package based on the fields of the specified
+// internal package and version info.
+//
+// latestRequested indicates whether the user requested the latest
+// version of the package. If so, the returned Package.URL will have the
+// structure /<path> instead of /<path>@<version>.
+func createPackageNew(vdir *internal.VersionedDirectory, latestRequested bool) (_ *Package, err error) {
+	defer derrors.Wrap(&err, "createPackageNew(%v, %t)", vdir, latestRequested)
+
+	if vdir == nil || vdir.Package == nil {
+		return nil, fmt.Errorf("package info must not be nil")
+	}
+
+	var modLicenses []*licenses.Metadata
+	for _, lm := range vdir.Licenses {
+		if path.Dir(lm.FilePath) == "." {
+			modLicenses = append(modLicenses, lm)
+		}
+	}
+
+	m := createModule(&vdir.ModuleInfo, modLicenses, latestRequested)
+	urlVersion := m.LinkVersion
+	if latestRequested {
+		urlVersion = internal.LatestVersion
+	}
+	return &Package{
+		Path:              vdir.Path,
+		Synopsis:          vdir.Package.Documentation.Synopsis,
+		IsRedistributable: vdir.DirectoryNew.IsRedistributable,
+		Licenses:          transformLicenseMetadata(vdir.Licenses),
+		Module:            *m,
+		URL:               constructPackageURL(vdir.Path, vdir.ModulePath, urlVersion),
+		LatestURL:         constructPackageURL(vdir.Path, vdir.ModulePath, middleware.LatestVersionPlaceholder),
+	}, nil
+}
+
 // createModule returns a *Module based on the fields of the specified
 // versionInfo.
 //
@@ -139,6 +175,21 @@
 	return base
 }
 
+// effectiveNameNew returns either the command name or package name.
+func effectiveNameNew(pkg *internal.PackageNew) string {
+	if pkg.Name != "main" {
+		return pkg.Name
+	}
+	var prefix string // package path without version
+	if pkg.Path[len(pkg.Path)-3:] == "/v1" {
+		prefix = pkg.Path[:len(pkg.Path)-3]
+	} else {
+		prefix, _, _ = module.SplitPathVersion(pkg.Path)
+	}
+	_, base := path.Split(prefix)
+	return base
+}
+
 // packageHTMLTitle constructs the details page title for pkg.
 // The string will appear in the <title> element (and thus
 // the browser tab).
@@ -149,6 +200,16 @@
 	return effectiveName(pkg) + " command"
 }
 
+// packageHTMLTitleNew constructs the details page title for pkg.
+// The string will appear in the <title> element (and thus
+// the browser tab).
+func packageHTMLTitleNew(pkg *internal.PackageNew) string {
+	if pkg.Name != "main" {
+		return pkg.Name + " package"
+	}
+	return effectiveNameNew(pkg) + " command"
+}
+
 // packageTitle returns the package title as it will
 // appear in the heading at the top of the page.
 func packageTitle(pkg *internal.Package) string {
@@ -158,6 +219,15 @@
 	return "command " + effectiveName(pkg)
 }
 
+// packageTitleNew returns the package title as it will
+// appear in the heading at the top of the page.
+func packageTitleNew(pkg *internal.PackageNew) string {
+	if pkg.Name != "main" {
+		return "package " + pkg.Name
+	}
+	return "command " + effectiveNameNew(pkg)
+}
+
 // breadcrumbPath builds HTML that displays pkgPath as a sequence of links
 // to its parents.
 // pkgPath is a slash-separated path, and may be a package import path or a directory.
diff --git a/internal/frontend/imports.go b/internal/frontend/imports.go
index 5946300..a04b36c 100644
--- a/internal/frontend/imports.go
+++ b/internal/frontend/imports.go
@@ -31,8 +31,8 @@
 
 // fetchImportsDetails fetches imports for the package version specified by
 // pkgPath, modulePath and version from the database and returns a ImportsDetails.
-func fetchImportsDetails(ctx context.Context, ds internal.DataSource, pkg *internal.VersionedPackage) (*ImportsDetails, error) {
-	dsImports, err := ds.GetImports(ctx, pkg.Path, pkg.ModulePath, pkg.Version)
+func fetchImportsDetails(ctx context.Context, ds internal.DataSource, pkgPath, modulePath, version string) (*ImportsDetails, error) {
+	dsImports, err := ds.GetImports(ctx, pkgPath, modulePath, version)
 	if err != nil {
 		return nil, err
 	}
@@ -41,7 +41,7 @@
 	for _, p := range dsImports {
 		if stdlib.Contains(p) {
 			std = append(std, p)
-		} else if strings.HasPrefix(p+"/", pkg.ModuleInfo.ModulePath+"/") {
+		} else if strings.HasPrefix(p+"/", modulePath+"/") {
 			moduleImports = append(moduleImports, p)
 		} else {
 			externalImports = append(externalImports, p)
@@ -49,7 +49,7 @@
 	}
 
 	return &ImportsDetails{
-		ModulePath:      pkg.ModuleInfo.ModulePath,
+		ModulePath:      modulePath,
 		ExternalImports: externalImports,
 		InternalImports: moduleImports,
 		StdLib:          std,
@@ -74,8 +74,8 @@
 
 // fetchImportedByDetails fetches importers for the package version specified by
 // path and version from the database and returns a ImportedByDetails.
-func fetchImportedByDetails(ctx context.Context, ds internal.DataSource, pkg *internal.VersionedPackage) (*ImportedByDetails, error) {
-	importedBy, err := ds.GetImportedBy(ctx, pkg.Path, pkg.ModulePath, importedByLimit)
+func fetchImportedByDetails(ctx context.Context, ds internal.DataSource, pkgPath, modulePath string) (*ImportedByDetails, error) {
+	importedBy, err := ds.GetImportedBy(ctx, pkgPath, modulePath, importedByLimit)
 	if err != nil {
 		return nil, err
 	}
@@ -90,7 +90,7 @@
 	}
 	sections := Sections(importedBy, nextPrefixAccount)
 	return &ImportedByDetails{
-		ModulePath:   pkg.ModuleInfo.ModulePath,
+		ModulePath:   modulePath,
 		ImportedBy:   sections,
 		Total:        len(importedBy),
 		TotalIsExact: totalIsExact,
diff --git a/internal/frontend/imports_test.go b/internal/frontend/imports_test.go
index 072a86d..f674897 100644
--- a/internal/frontend/imports_test.go
+++ b/internal/frontend/imports_test.go
@@ -56,7 +56,8 @@
 				t.Fatal(err)
 			}
 
-			got, err := fetchImportsDetails(ctx, testDB, firstVersionedPackage(module))
+			pkg := firstVersionedPackage(module)
+			got, err := fetchImportsDetails(ctx, testDB, pkg.Path, pkg.ModulePath, pkg.Version)
 			if err != nil {
 				t.Fatalf("fetchImportsDetails(ctx, db, %q, %q) = %v err = %v, want %v",
 					module.Packages[0].Path, module.Version, got, err, tc.wantDetails)
@@ -145,7 +146,7 @@
 			otherVersion := newModule(path.Dir(tc.pkg.Path), tc.pkg)
 			otherVersion.Version = "v1.0.5"
 			vp := firstVersionedPackage(otherVersion)
-			got, err := fetchImportedByDetails(ctx, testDB, vp)
+			got, err := fetchImportedByDetails(ctx, testDB, vp.Path, vp.ModulePath)
 			if err != nil {
 				t.Fatalf("fetchImportedByDetails(ctx, db, %q) = %v err = %v, want %v",
 					tc.pkg.Path, got, err, tc.wantDetails)
diff --git a/internal/frontend/license.go b/internal/frontend/license.go
index affa2c3..755d342 100644
--- a/internal/frontend/license.go
+++ b/internal/frontend/license.go
@@ -33,12 +33,12 @@
 
 // fetchPackageLicensesDetails fetches license data for the package version specified by
 // path and version from the database and returns a LicensesDetails.
-func fetchPackageLicensesDetails(ctx context.Context, ds internal.DataSource, pkg *internal.VersionedPackage) (*LicensesDetails, error) {
-	dsLicenses, err := ds.GetPackageLicenses(ctx, pkg.Path, pkg.ModulePath, pkg.ModuleInfo.Version)
+func fetchPackageLicensesDetails(ctx context.Context, ds internal.DataSource, pkgPath, modulePath, version string) (*LicensesDetails, error) {
+	dsLicenses, err := ds.GetPackageLicenses(ctx, pkgPath, modulePath, version)
 	if err != nil {
 		return nil, err
 	}
-	return &LicensesDetails{Licenses: transformLicenses(pkg.ModulePath, pkg.Version, dsLicenses)}, nil
+	return &LicensesDetails{Licenses: transformLicenses(modulePath, version, dsLicenses)}, nil
 }
 
 // transformLicenses transforms licenses.License into a License
diff --git a/internal/frontend/overview.go b/internal/frontend/overview.go
index 03008e3..b96265b 100644
--- a/internal/frontend/overview.go
+++ b/internal/frontend/overview.go
@@ -54,8 +54,8 @@
 	return overview
 }
 
-// constructPackageOverviewDetails uses data for the given package to return an OverviewDetails.
-func constructPackageOverviewDetails(pkg *internal.VersionedPackage, versionedLinks bool) *OverviewDetails {
+// fetchPackageOverviewDetails uses data for the given package to return an OverviewDetails.
+func fetchPackageOverviewDetails(pkg *internal.VersionedPackage, versionedLinks bool) *OverviewDetails {
 	od := constructOverviewDetails(&pkg.ModuleInfo, pkg.Package.IsRedistributable, versionedLinks)
 	od.PackageSourceURL = pkg.SourceInfo.DirectoryURL(packageSubdir(pkg.Path, pkg.ModulePath))
 	if !pkg.Package.IsRedistributable {
@@ -64,6 +64,16 @@
 	return od
 }
 
+// fetchPackageOverviewDetailsNew uses data for the given versioned directory to return an OverviewDetails.
+func fetchPackageOverviewDetailsNew(vdir *internal.VersionedDirectory, versionedLinks bool) *OverviewDetails {
+	od := constructOverviewDetails(&vdir.ModuleInfo, vdir.DirectoryNew.IsRedistributable, versionedLinks)
+	od.PackageSourceURL = vdir.SourceInfo.DirectoryURL(packageSubdir(vdir.Path, vdir.ModulePath))
+	if !vdir.DirectoryNew.IsRedistributable {
+		od.Redistributable = false
+	}
+	return od
+}
+
 // packageSubdir returns the subdirectory of the package relative to its module.
 func packageSubdir(pkgPath, modulePath string) string {
 	switch {
diff --git a/internal/frontend/overview_test.go b/internal/frontend/overview_test.go
index cd35eb8..1a89e1f 100644
--- a/internal/frontend/overview_test.go
+++ b/internal/frontend/overview_test.go
@@ -51,6 +51,81 @@
 	}
 }
 
+func TestConstructPackageOverviewDetailsNew(t *testing.T) {
+	for _, test := range []struct {
+		name           string
+		vdir           *internal.VersionedDirectory
+		versionedLinks bool
+		want           *OverviewDetails
+	}{
+		{
+			name: "redistributable",
+			vdir: &internal.VersionedDirectory{
+				DirectoryNew: internal.DirectoryNew{
+					Path:              "github.com/u/m/p",
+					IsRedistributable: true,
+				},
+				ModuleInfo: *sample.ModuleInfo("github.com/u/m", "v1.2.3"),
+			},
+			versionedLinks: true,
+			want: &OverviewDetails{
+				ModulePath:       "github.com/u/m",
+				ModuleURL:        "/mod/github.com/u/m@v1.2.3",
+				RepositoryURL:    "https://github.com/u/m",
+				PackageSourceURL: "https://github.com/u/m/tree/v1.2.3/p",
+				ReadMe:           template.HTML("<p>readme</p>\n"),
+				ReadMeSource:     "github.com/u/m@v1.2.3/README.md",
+				Redistributable:  true,
+			},
+		},
+		{
+			name: "unversioned",
+			vdir: &internal.VersionedDirectory{
+				DirectoryNew: internal.DirectoryNew{
+					Path:              "github.com/u/m/p",
+					IsRedistributable: true,
+				},
+				ModuleInfo: *sample.ModuleInfo("github.com/u/m", "v1.2.3"),
+			},
+			versionedLinks: false,
+			want: &OverviewDetails{
+				ModulePath:       "github.com/u/m",
+				ModuleURL:        "/mod/github.com/u/m",
+				RepositoryURL:    "https://github.com/u/m",
+				PackageSourceURL: "https://github.com/u/m/tree/v1.2.3/p",
+				ReadMe:           template.HTML("<p>readme</p>\n"),
+				ReadMeSource:     "github.com/u/m@v1.2.3/README.md",
+				Redistributable:  true,
+			},
+		},
+		{
+			name: "non-redistributable",
+			vdir: &internal.VersionedDirectory{
+				DirectoryNew: internal.DirectoryNew{
+					Path:              "github.com/u/m/p",
+					IsRedistributable: false,
+				},
+				ModuleInfo: *sample.ModuleInfo("github.com/u/m", "v1.2.3"),
+			},
+			versionedLinks: true,
+			want: &OverviewDetails{
+				ModulePath:       "github.com/u/m",
+				ModuleURL:        "/mod/github.com/u/m@v1.2.3",
+				RepositoryURL:    "https://github.com/u/m",
+				PackageSourceURL: "https://github.com/u/m/tree/v1.2.3/p",
+				ReadMe:           "",
+				ReadMeSource:     "",
+				Redistributable:  false,
+			},
+		},
+	} {
+		got := fetchPackageOverviewDetailsNew(test.vdir, test.versionedLinks)
+		if diff := cmp.Diff(test.want, got); diff != "" {
+			t.Errorf("%s: mismatch (-want +got):\n%s", test.name, diff)
+		}
+	}
+}
+
 func TestReadmeHTML(t *testing.T) {
 	testCases := []struct {
 		name string
diff --git a/internal/frontend/package.go b/internal/frontend/package.go
index 48d3e98..0fed96d 100644
--- a/internal/frontend/package.go
+++ b/internal/frontend/package.go
@@ -153,7 +153,7 @@
 	defer derrors.Wrap(&err, "servePackagePageNew(w, r, %q, %q, %q)", fullPath, inModulePath, inVersion)
 
 	ctx := r.Context()
-	modulePath, version, isPackage, err := s.ds.GetPathInfo(ctx, fullPath, inModulePath, inVersion)
+	modulePath, version, _, err := s.ds.GetPathInfo(ctx, fullPath, inModulePath, inVersion)
 	if err != nil {
 		if !errors.Is(err, derrors.NotFound) {
 			return err
@@ -195,12 +195,12 @@
 			},
 		}
 	}
-	if isPackage {
-		pkg, err := s.ds.GetPackage(ctx, fullPath, modulePath, version)
-		if err != nil {
-			return err
-		}
-		return s.servePackagePageWithPackage(ctx, w, r, pkg, inVersion)
+	vdir, err := s.ds.GetDirectoryNew(ctx, fullPath, modulePath, version)
+	if err != nil {
+		return err
+	}
+	if vdir.Package != nil {
+		return s.servePackagePageWithVersionedDirectory(ctx, w, r, vdir, inVersion)
 	}
 	dir, err := s.ds.GetDirectory(ctx, fullPath, modulePath, version, internal.AllFields)
 	if err != nil {
@@ -226,3 +226,48 @@
 	// No matches, or ambiguous.
 	return "", nil
 }
+
+func (s *Server) servePackagePageWithVersionedDirectory(ctx context.Context, w http.ResponseWriter, r *http.Request, vdir *internal.VersionedDirectory, requestedVersion string) error {
+
+	pkgHeader, err := createPackageNew(vdir, requestedVersion == internal.LatestVersion)
+	if err != nil {
+		return fmt.Errorf("creating package header for %s@%s: %v", vdir.Path, vdir.Version, err)
+	}
+
+	tab := r.FormValue("tab")
+	settings, ok := packageTabLookup[tab]
+	if !ok {
+		var tab string
+		if vdir.DirectoryNew.IsRedistributable {
+			tab = "doc"
+		} else {
+			tab = "overview"
+		}
+		http.Redirect(w, r, fmt.Sprintf(r.URL.Path+"?tab=%s", tab), http.StatusFound)
+		return nil
+	}
+	canShowDetails := vdir.DirectoryNew.IsRedistributable || settings.AlwaysShowDetails
+
+	var details interface{}
+	if canShowDetails {
+		var err error
+		details, err = fetchDetailsForVersionedDirectory(ctx, r, tab, s.ds, vdir)
+		if err != nil {
+			return fmt.Errorf("fetching page for %q: %v", tab, err)
+		}
+	}
+	page := &DetailsPage{
+		basePage: newBasePage(r, packageHTMLTitleNew(vdir.Package)),
+		Title:    packageTitleNew(vdir.Package),
+		Settings: settings,
+		Header:   pkgHeader,
+		BreadcrumbPath: breadcrumbPath(pkgHeader.Path, pkgHeader.Module.ModulePath,
+			pkgHeader.Module.LinkVersion),
+		Details:        details,
+		CanShowDetails: canShowDetails,
+		Tabs:           packageTabSettings,
+		PageType:       "pkg",
+	}
+	s.servePage(ctx, w, settings.TemplateName, page)
+	return nil
+}
diff --git a/internal/frontend/tabs.go b/internal/frontend/tabs.go
index d1f06e6..9e69d33 100644
--- a/internal/frontend/tabs.go
+++ b/internal/frontend/tabs.go
@@ -146,17 +146,39 @@
 	case "doc":
 		return fetchDocumentationDetails(pkg), nil
 	case "versions":
-		return fetchPackageVersionsDetails(ctx, ds, pkg)
+		return fetchPackageVersionsDetails(ctx, ds, pkg.Path, pkg.V1Path, pkg.ModulePath)
 	case "subdirectories":
 		return fetchDirectoryDetails(ctx, ds, pkg.Path, &pkg.ModuleInfo, pkg.Licenses, false)
 	case "imports":
-		return fetchImportsDetails(ctx, ds, pkg)
+		return fetchImportsDetails(ctx, ds, pkg.Path, pkg.ModulePath, pkg.Version)
 	case "importedby":
-		return fetchImportedByDetails(ctx, ds, pkg)
+		return fetchImportedByDetails(ctx, ds, pkg.Path, pkg.ModulePath)
 	case "licenses":
-		return fetchPackageLicensesDetails(ctx, ds, pkg)
+		return fetchPackageLicensesDetails(ctx, ds, pkg.Path, pkg.ModulePath, pkg.Version)
 	case "overview":
-		return constructPackageOverviewDetails(pkg, urlIsVersioned(r.URL)), nil
+		return fetchPackageOverviewDetails(pkg, urlIsVersioned(r.URL)), nil
+	}
+	return nil, fmt.Errorf("BUG: unable to fetch details: unknown tab %q", tab)
+}
+
+// fetchDetailsForVersionedDirectory returns tab details by delegating to the correct detail
+// handler.
+func fetchDetailsForVersionedDirectory(ctx context.Context, r *http.Request, tab string, ds internal.DataSource, vdir *internal.VersionedDirectory) (interface{}, error) {
+	switch tab {
+	case "doc":
+		return fetchDocumentationDetailsNew(vdir.Package.Documentation), nil
+	case "versions":
+		return fetchPackageVersionsDetails(ctx, ds, vdir.Path, vdir.V1Path, vdir.ModulePath)
+	case "subdirectories":
+		return fetchDirectoryDetails(ctx, ds, vdir.Path, &vdir.ModuleInfo, vdir.Licenses, false)
+	case "imports":
+		return fetchImportsDetails(ctx, ds, vdir.Path, vdir.ModulePath, vdir.Version)
+	case "importedby":
+		return fetchImportedByDetails(ctx, ds, vdir.Path, vdir.ModulePath)
+	case "licenses":
+		return fetchPackageLicensesDetails(ctx, ds, vdir.Path, vdir.ModulePath, vdir.Version)
+	case "overview":
+		return fetchPackageOverviewDetailsNew(vdir, urlIsVersioned(r.URL)), nil
 	}
 	return nil, fmt.Errorf("BUG: unable to fetch details: unknown tab %q", tab)
 }
diff --git a/internal/frontend/versions.go b/internal/frontend/versions.go
index 2ee7dfb..3ee148a 100644
--- a/internal/frontend/versions.go
+++ b/internal/frontend/versions.go
@@ -83,17 +83,16 @@
 }
 
 // fetchPackageVersionsDetails builds a version hierarchy for all module
-// versions containing a package path with v1 import path matching the v1
-// import path of pkg.
-func fetchPackageVersionsDetails(ctx context.Context, ds internal.DataSource, pkg *internal.VersionedPackage) (*VersionsDetails, error) {
-	versions, err := ds.GetTaggedVersionsForPackageSeries(ctx, pkg.Path)
+// versions containing a package path with v1 import path matching the given v1 path.
+func fetchPackageVersionsDetails(ctx context.Context, ds internal.DataSource, pkgPath, v1Path, modulePath string) (*VersionsDetails, error) {
+	versions, err := ds.GetTaggedVersionsForPackageSeries(ctx, pkgPath)
 	if err != nil {
 		return nil, err
 	}
 	// If no tagged versions for the package series are found, fetch the
 	// pseudo-versions instead.
 	if len(versions) == 0 {
-		versions, err = ds.GetPseudoVersionsForPackageSeries(ctx, pkg.Path)
+		versions, err = ds.GetPseudoVersionsForPackageSeries(ctx, pkgPath)
 		if err != nil {
 			return nil, err
 		}
@@ -103,7 +102,7 @@
 	// TODO(rfindley): remove this filtering, as it should not be necessary and
 	// is probably a relic of earlier version query implementations.
 	for _, v := range versions {
-		if seriesPath := v.SeriesPath(); strings.HasPrefix(pkg.V1Path, seriesPath) || seriesPath == stdlib.ModulePath {
+		if seriesPath := v.SeriesPath(); strings.HasPrefix(v1Path, seriesPath) || seriesPath == stdlib.ModulePath {
 			filteredVersions = append(filteredVersions, v)
 		} else {
 			log.Errorf(ctx, "got version with mismatching series: %q", seriesPath)
@@ -115,13 +114,13 @@
 		// import path of the package corresponding to this version.
 		var versionPath string
 		if mi.ModulePath == stdlib.ModulePath {
-			versionPath = pkg.Path
+			versionPath = pkgPath
 		} else {
-			versionPath = pathInVersion(pkg.V1Path, mi)
+			versionPath = pathInVersion(v1Path, mi)
 		}
 		return constructPackageURL(versionPath, mi.ModulePath, linkVersion(mi.Version, mi.ModulePath))
 	}
-	return buildVersionDetails(pkg.ModulePath, filteredVersions, linkify), nil
+	return buildVersionDetails(modulePath, filteredVersions, linkify), nil
 }
 
 // pathInVersion constructs the full import path of the package corresponding
diff --git a/internal/frontend/versions_test.go b/internal/frontend/versions_test.go
index e4c9b13..15cf250 100644
--- a/internal/frontend/versions_test.go
+++ b/internal/frontend/versions_test.go
@@ -269,7 +269,7 @@
 				}
 			}
 
-			got, err := fetchPackageVersionsDetails(ctx, testDB, tc.pkg)
+			got, err := fetchPackageVersionsDetails(ctx, testDB, tc.pkg.Path, tc.pkg.V1Path, tc.pkg.ModulePath)
 			if err != nil {
 				t.Fatalf("fetchPackageVersionsDetails(ctx, db, %v): %v", tc.pkg, err)
 			}
diff --git a/internal/postgres/directory.go b/internal/postgres/directory.go
index d186da8..56cd05c 100644
--- a/internal/postgres/directory.go
+++ b/internal/postgres/directory.go
@@ -17,14 +17,10 @@
 	"golang.org/x/pkgsite/internal/stdlib"
 )
 
-// getDirectoryNew returns a directory from the database, along with all of the
+// GetDirectoryNew returns a directory from the database, along with all of the
 // data associated with that directory, including the package, imports, readme,
 // documentation, and licenses.
-//
-// At the moment this function is only being used to test InsertModule. It only
-// supports fetching a directory when the modulePath is known. It will be
-// exported in a later CL once we integrate the new data model in the frontend.
-func (db *DB) getDirectoryNew(ctx context.Context, path, modulePath, version string) (_ *internal.VersionedDirectory, err error) {
+func (db *DB) GetDirectoryNew(ctx context.Context, path, modulePath, version string) (_ *internal.VersionedDirectory, err error) {
 	query := `
 		SELECT
 			m.module_path,
@@ -34,6 +30,8 @@
 			m.redistributable,
 			m.has_go_mod,
 			m.source_info,
+			m.readme_file_path,
+			m.readme_contents,
 			p.id,
 			p.path,
 			p.name,
@@ -67,7 +65,6 @@
 		licenseTypes, licensePaths []string
 		pathID                     int
 	)
-
 	row := db.db.QueryRow(ctx, query, path, modulePath, version)
 	if err := row.Scan(
 		&mi.ModulePath,
@@ -77,6 +74,8 @@
 		&mi.IsRedistributable,
 		&mi.HasGoMod,
 		jsonbScanner{&mi.SourceInfo},
+		&mi.ReadmeFilePath,
+		&mi.ReadmeContents,
 		&pathID,
 		&dir.Path,
 		database.NullIsEmpty(&pkg.Name),
diff --git a/internal/postgres/directory_test.go b/internal/postgres/directory_test.go
index a5b24a2..1b0e3f5 100644
--- a/internal/postgres/directory_test.go
+++ b/internal/postgres/directory_test.go
@@ -14,6 +14,7 @@
 	"github.com/google/go-cmp/cmp/cmpopts"
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/derrors"
+	"golang.org/x/pkgsite/internal/experiment"
 	"golang.org/x/pkgsite/internal/licenses"
 	"golang.org/x/pkgsite/internal/source"
 	"golang.org/x/pkgsite/internal/stdlib"
@@ -280,6 +281,168 @@
 	}
 }
 
+func TestGetDirectoryNew(t *testing.T) {
+	ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
+	defer cancel()
+	ctx = experiment.NewContext(ctx,
+		experiment.NewSet(map[string]bool{
+			internal.ExperimentInsertDirectories: true}))
+
+	defer ResetTestDB(testDB, t)
+
+	InsertSampleDirectoryTree(ctx, t, testDB)
+
+	// Add a module that has READMEs in a directory and a package.
+	m := sample.Module("a.com/m", "v1.2.3", "dir/p")
+	d := sample.DirectoryNewEmpty("a.com/m/dir")
+	d.Readme = &internal.Readme{
+		Filepath: "DIR_README.md",
+		Contents: "dir readme",
+	}
+	m.Directories = append(m.Directories, d)
+	d = sample.DirectoryNewEmpty("a.com/m/dir/p")
+	d.Readme = &internal.Readme{
+		Filepath: "PKG_README.md",
+		Contents: "pkg readme",
+	}
+	m.Directories = append(m.Directories, d)
+	if err := testDB.InsertModule(ctx, m); err != nil {
+		t.Fatal(err)
+	}
+
+	newVdir := func(path, modulePath, version string, readme *internal.Readme, pkg *internal.PackageNew) *internal.VersionedDirectory {
+		return &internal.VersionedDirectory{
+			ModuleInfo: *sample.ModuleInfo(modulePath, version),
+			DirectoryNew: internal.DirectoryNew{
+				Path:              path,
+				V1Path:            path,
+				IsRedistributable: true,
+				Licenses:          sample.LicenseMetadata,
+				Readme:            readme,
+				Package:           pkg,
+			},
+		}
+	}
+
+	newPackage := func(name, path string) *internal.PackageNew {
+		return &internal.PackageNew{
+			Name: name,
+			Path: path,
+			Documentation: &internal.Documentation{
+				Synopsis: sample.Synopsis,
+				HTML:     sample.DocumentationHTML,
+				GOOS:     sample.GOOS,
+				GOARCH:   sample.GOARCH,
+			},
+			Imports: sample.Imports,
+		}
+	}
+
+	for _, tc := range []struct {
+		name, dirPath, modulePath, version string
+		want                               *internal.VersionedDirectory
+		wantNotFoundErr                    bool
+	}{
+		{
+			name:       "module path",
+			dirPath:    "github.com/hashicorp/vault",
+			modulePath: "github.com/hashicorp/vault",
+			version:    "v1.0.3",
+			want: newVdir("github.com/hashicorp/vault", "github.com/hashicorp/vault", "v1.0.3",
+				&internal.Readme{
+					Filepath: sample.ReadmeFilePath,
+					Contents: sample.ReadmeContents,
+				}, nil),
+		},
+		{
+			name:       "package path",
+			dirPath:    "github.com/hashicorp/vault/api",
+			modulePath: "github.com/hashicorp/vault",
+			version:    "v1.0.3",
+			want: newVdir("github.com/hashicorp/vault/api", "github.com/hashicorp/vault", "v1.0.3", nil,
+				newPackage("api", "github.com/hashicorp/vault/api")),
+		},
+		{
+			name:       "directory path",
+			dirPath:    "github.com/hashicorp/vault/builtin",
+			modulePath: "github.com/hashicorp/vault",
+			version:    "v1.0.3",
+			want:       newVdir("github.com/hashicorp/vault/builtin", "github.com/hashicorp/vault", "v1.0.3", nil, nil),
+		},
+		{
+			name:       "stdlib directory",
+			dirPath:    "archive",
+			modulePath: stdlib.ModulePath,
+			version:    "v1.13.4",
+			want:       newVdir("archive", stdlib.ModulePath, "v1.13.4", nil, nil),
+		},
+		{
+			name:       "stdlib package",
+			dirPath:    "archive/zip",
+			modulePath: stdlib.ModulePath,
+			version:    "v1.13.4",
+			want:       newVdir("archive/zip", stdlib.ModulePath, "v1.13.4", nil, newPackage("zip", "archive/zip")),
+		},
+		{
+			name:            "stdlib package - incomplete last element",
+			dirPath:         "archive/zi",
+			modulePath:      stdlib.ModulePath,
+			version:         "v1.13.4",
+			wantNotFoundErr: true,
+		},
+		{
+			name:       "stdlib - internal directory",
+			dirPath:    "cmd/internal",
+			modulePath: stdlib.ModulePath,
+			version:    "v1.13.4",
+			want:       newVdir("cmd/internal", stdlib.ModulePath, "v1.13.4", nil, nil),
+		},
+		{
+			name:       "directory with readme",
+			dirPath:    "a.com/m/dir",
+			modulePath: "a.com/m",
+			version:    "v1.2.3",
+			want: newVdir("a.com/m/dir", "a.com/m", "v1.2.3", &internal.Readme{
+				Filepath: "DIR_README.md",
+				Contents: "dir readme",
+			}, nil),
+		},
+		{
+			name:       "package with readme",
+			dirPath:    "a.com/m/dir/p",
+			modulePath: "a.com/m",
+			version:    "v1.2.3",
+			want: newVdir("a.com/m/dir/p", "a.com/m", "v1.2.3",
+				&internal.Readme{
+					Filepath: "PKG_README.md",
+					Contents: "pkg readme",
+				},
+				newPackage("p", "a.com/m/dir/p")),
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			got, err := testDB.GetDirectoryNew(ctx, tc.dirPath, tc.modulePath, tc.version)
+			if tc.wantNotFoundErr {
+				if !errors.Is(err, derrors.NotFound) {
+					t.Fatalf("want %v; got = \n%+v, %v", derrors.NotFound, got, err)
+				}
+				return
+			}
+			if err != nil {
+				t.Fatal(err)
+			}
+			opts := []cmp.Option{
+				cmp.AllowUnexported(source.Info{}),
+				// The packages table only includes partial license information; it omits the Coverage field.
+				cmpopts.IgnoreFields(licenses.Metadata{}, "Coverage"),
+			}
+			if diff := cmp.Diff(tc.want, got, opts...); diff != "" {
+				t.Errorf("mismatch (-want, +got):\n%s", diff)
+			}
+		})
+	}
+}
+
 func TestGetDirectoryFieldSet(t *testing.T) {
 	ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
 	defer cancel()
diff --git a/internal/postgres/insert_module_test.go b/internal/postgres/insert_module_test.go
index 294268f..b76bc59 100644
--- a/internal/postgres/insert_module_test.go
+++ b/internal/postgres/insert_module_test.go
@@ -109,7 +109,7 @@
 			}
 
 			for _, dir := range test.module.Directories {
-				got, err := testDB.getDirectoryNew(ctx, dir.Path, test.module.ModulePath, test.module.Version)
+				got, err := testDB.GetDirectoryNew(ctx, dir.Path, test.module.ModulePath, test.module.Version)
 				if err != nil {
 					t.Fatal(err)
 				}
diff --git a/internal/proxydatasource/datasource.go b/internal/proxydatasource/datasource.go
index 3cb6ca1..77e7e1c 100644
--- a/internal/proxydatasource/datasource.go
+++ b/internal/proxydatasource/datasource.go
@@ -89,6 +89,21 @@
 	}, nil
 }
 
+// GetDirectoryNew returns information about a directory at a path.
+func (ds *DataSource) GetDirectoryNew(ctx context.Context, dirPath, modulePath, version string) (_ *internal.VersionedDirectory, err error) {
+	m, err := ds.getModule(ctx, modulePath, version)
+	if err != nil {
+		return nil, err
+	}
+	return &internal.VersionedDirectory{
+		ModuleInfo: m.ModuleInfo,
+		DirectoryNew: internal.DirectoryNew{
+			Path:   dirPath,
+			V1Path: internal.V1Path(modulePath, strings.TrimPrefix(dirPath, modulePath+"/")),
+		},
+	}, nil
+}
+
 // GetImportedBy is unimplemented.
 func (ds *DataSource) GetImportedBy(ctx context.Context, path, version string, limit int) (_ []string, err error) {
 	return nil, nil