internal: add ModuleInfo

ModuleInfo is added, which represents the module info for the new data
model. ModuleInfo is embedded in LegacyModuleInfo.

In order to support the existing overview tab functionality,
GetDirectoryNew was changed to return the module README, regardless of
whether there is a README for the directory.

We will change this logic to display the README for the directory in a
future CL.

Change-Id: I624a6d99b711870826fd7dff9100d4ad47852db2
Reviewed-on: https://team-review.git.corp.google.com/c/golang/discovery/+/766801
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/discovery.go b/internal/discovery.go
index a14d592..3284ae4 100644
--- a/internal/discovery.go
+++ b/internal/discovery.go
@@ -30,15 +30,20 @@
 	UnknownModulePath = "unknownModulePath"
 )
 
+// ModuleInfo holds metadata associated with a module.
+type ModuleInfo struct {
+	ModulePath        string
+	Version           string
+	CommitTime        time.Time
+	VersionType       version.Type
+	IsRedistributable bool
+	HasGoMod          bool // whether the module zip has a go.mod file
+	SourceInfo        *source.Info
+}
+
 // LegacyModuleInfo holds metadata associated with a module.
 type LegacyModuleInfo struct {
-	ModulePath           string
-	Version              string
-	CommitTime           time.Time
-	VersionType          version.Type
-	IsRedistributable    bool
-	HasGoMod             bool // whether the module zip has a go.mod file
-	SourceInfo           *source.Info
+	ModuleInfo
 	LegacyReadmeFilePath string
 	LegacyReadmeContents string
 }
@@ -67,7 +72,7 @@
 // The module paths "a/b" and "a/b/v2"  both have series path "a/b".
 // The module paths "gopkg.in/yaml.v1" and "gopkg.in/yaml.v2" both have series
 // path "gopkg.in/yaml".
-func (v *LegacyModuleInfo) SeriesPath() string {
+func (v *ModuleInfo) SeriesPath() string {
 	return SeriesPathForModule(v.ModulePath)
 }
 
@@ -102,7 +107,7 @@
 // information.
 type VersionedDirectory struct {
 	DirectoryNew
-	LegacyModuleInfo
+	ModuleInfo
 }
 
 // DirectoryNew is a folder in a module version, and all of the packages
diff --git a/internal/fetch/fetch.go b/internal/fetch/fetch.go
index ec5208f..e170994 100644
--- a/internal/fetch/fetch.go
+++ b/internal/fetch/fetch.go
@@ -188,15 +188,17 @@
 	}
 	return &internal.Module{
 		LegacyModuleInfo: internal.LegacyModuleInfo{
-			ModulePath:           modulePath,
-			Version:              resolvedVersion,
-			CommitTime:           commitTime,
+			ModuleInfo: internal.ModuleInfo{
+				ModulePath:        modulePath,
+				Version:           resolvedVersion,
+				CommitTime:        commitTime,
+				VersionType:       versionType,
+				IsRedistributable: d.ModuleIsRedistributable(),
+				HasGoMod:          hasGoMod,
+				SourceInfo:        sourceInfo,
+			},
 			LegacyReadmeFilePath: readmeFilePath,
 			LegacyReadmeContents: readmeContents,
-			VersionType:          versionType,
-			IsRedistributable:    d.ModuleIsRedistributable(),
-			HasGoMod:             hasGoMod,
-			SourceInfo:           sourceInfo,
 		},
 		LegacyPackages: packages,
 		Licenses:       allLicenses,
diff --git a/internal/fetch/fetchdata_test.go b/internal/fetch/fetchdata_test.go
index ad1bd40..7c03d32 100644
--- a/internal/fetch/fetchdata_test.go
+++ b/internal/fetch/fetchdata_test.go
@@ -32,10 +32,12 @@
 	fr: &FetchResult{
 		Module: &internal.Module{
 			LegacyModuleInfo: internal.LegacyModuleInfo{
-				ModulePath:           "github.com/basic",
+				ModuleInfo: internal.ModuleInfo{
+					ModulePath: "github.com/basic",
+					HasGoMod:   false,
+				},
 				LegacyReadmeFilePath: "README.md",
 				LegacyReadmeContents: "THIS IS A README",
-				HasGoMod:             false,
 			},
 			Directories: []*internal.DirectoryNew{
 				{
@@ -99,11 +101,13 @@
 	fr: &FetchResult{
 		Module: &internal.Module{
 			LegacyModuleInfo: internal.LegacyModuleInfo{
-				ModulePath:           "github.com/my/module",
-				HasGoMod:             true,
+				ModuleInfo: internal.ModuleInfo{
+					ModulePath: "github.com/my/module",
+					HasGoMod:   true,
+					SourceInfo: source.NewGitHubInfo("https://github.com/my/module", "", "v1.0.0"),
+				},
 				LegacyReadmeFilePath: "README.md",
 				LegacyReadmeContents: "README FILE FOR TESTING.",
-				SourceInfo:           source.NewGitHubInfo("https://github.com/my/module", "", "v1.0.0"),
 			},
 			Directories: []*internal.DirectoryNew{
 				{
@@ -163,8 +167,10 @@
 	fr: &FetchResult{
 		Module: &internal.Module{
 			LegacyModuleInfo: internal.LegacyModuleInfo{
-				ModulePath: "no.mod/module",
-				HasGoMod:   false,
+				ModuleInfo: internal.ModuleInfo{
+					ModulePath: "no.mod/module",
+					HasGoMod:   false,
+				},
 			},
 			Directories: []*internal.DirectoryNew{
 				{
@@ -225,7 +231,9 @@
 		Status: derrors.ToHTTPStatus(derrors.HasIncompletePackages),
 		Module: &internal.Module{
 			LegacyModuleInfo: internal.LegacyModuleInfo{
-				ModulePath: "bad.mod/module",
+				ModuleInfo: internal.ModuleInfo{
+					ModulePath: "bad.mod/module",
+				},
 			},
 			Directories: []*internal.DirectoryNew{
 				{
@@ -287,8 +295,10 @@
 		Status: derrors.ToHTTPStatus(derrors.HasIncompletePackages),
 		Module: &internal.Module{
 			LegacyModuleInfo: internal.LegacyModuleInfo{
-				ModulePath: "build.constraints/module",
-				HasGoMod:   false,
+				ModuleInfo: internal.ModuleInfo{
+					ModulePath: "build.constraints/module",
+					HasGoMod:   false,
+				},
 			},
 			Directories: []*internal.DirectoryNew{
 				{
@@ -372,10 +382,12 @@
 	fr: &FetchResult{
 		Module: &internal.Module{
 			LegacyModuleInfo: internal.LegacyModuleInfo{
-				ModulePath:           "nonredistributable.mod/module",
+				ModuleInfo: internal.ModuleInfo{
+					ModulePath: "nonredistributable.mod/module",
+					HasGoMod:   true,
+				},
 				LegacyReadmeFilePath: "README.md",
 				LegacyReadmeContents: "README FILE FOR TESTING.",
-				HasGoMod:             true,
 			},
 			Directories: []*internal.DirectoryNew{
 				{
@@ -441,7 +453,9 @@
 		Status: derrors.ToHTTPStatus(derrors.HasIncompletePackages),
 		Module: &internal.Module{
 			LegacyModuleInfo: internal.LegacyModuleInfo{
-				ModulePath: "bad.import.path.com",
+				ModuleInfo: internal.ModuleInfo{
+					ModulePath: "bad.import.path.com",
+				},
 			},
 			Directories: []*internal.DirectoryNew{
 				{
@@ -506,8 +520,10 @@
 		GoModPath: "doc.test",
 		Module: &internal.Module{
 			LegacyModuleInfo: internal.LegacyModuleInfo{
-				ModulePath: "doc.test",
-				HasGoMod:   false,
+				ModuleInfo: internal.ModuleInfo{
+					ModulePath: "doc.test",
+					HasGoMod:   false,
+				},
 			},
 			Directories: []*internal.DirectoryNew{
 				{
@@ -548,10 +564,12 @@
 	fr: &FetchResult{
 		Module: &internal.Module{
 			LegacyModuleInfo: internal.LegacyModuleInfo{
-				ModulePath:           "github.com/my/module/js",
+				ModuleInfo: internal.ModuleInfo{
+					ModulePath: "github.com/my/module/js",
+					SourceInfo: source.NewGitHubInfo("https://github.com/my/module", "js", "js/v1.0.0"),
+				},
 				LegacyReadmeFilePath: "README.md",
 				LegacyReadmeContents: "THIS IS A README",
-				SourceInfo:           source.NewGitHubInfo("https://github.com/my/module", "js", "js/v1.0.0"),
 			},
 			Directories: []*internal.DirectoryNew{
 				{
@@ -598,13 +616,15 @@
 	fr: &FetchResult{
 		Module: &internal.Module{
 			LegacyModuleInfo: internal.LegacyModuleInfo{
-				ModulePath:           "std",
-				Version:              "v1.12.5",
-				CommitTime:           stdlib.TestCommitTime,
+				ModuleInfo: internal.ModuleInfo{
+					ModulePath: "std",
+					Version:    "v1.12.5",
+					CommitTime: stdlib.TestCommitTime,
+					HasGoMod:   true,
+					SourceInfo: source.NewGitHubInfo("https://github.com/golang/go", "src", "go1.12.5"),
+				},
 				LegacyReadmeFilePath: "README.md",
 				LegacyReadmeContents: "# The Go Programming Language\n",
-				HasGoMod:             true,
-				SourceInfo:           source.NewGitHubInfo("https://github.com/golang/go", "src", "go1.12.5"),
 			},
 			Directories: []*internal.DirectoryNew{
 				{
@@ -753,8 +773,10 @@
 			GoModPath: path,
 			Module: &internal.Module{
 				LegacyModuleInfo: internal.LegacyModuleInfo{
-					ModulePath: path,
-					HasGoMod:   false,
+					ModuleInfo: internal.ModuleInfo{
+						ModulePath: path,
+						HasGoMod:   false,
+					},
 				},
 				Directories: []*internal.DirectoryNew{
 					{
diff --git a/internal/frontend/details.go b/internal/frontend/details.go
index b140c8a..4c1acba 100644
--- a/internal/frontend/details.go
+++ b/internal/frontend/details.go
@@ -193,7 +193,10 @@
 // pathNotFoundErrorNew returns an error page that provides the user with an
 // option to fetch a path.
 func pathNotFoundErrorNew(fullPath, version string) error {
-	path := fmt.Sprintf("%s@%s", fullPath, version)
+	path := fullPath
+	if version != internal.LatestVersion {
+		path = fmt.Sprintf("%s@%s", fullPath, version)
+	}
 	return &serverError{
 		status: http.StatusNotFound,
 		epage: &errorPage{
diff --git a/internal/frontend/directory.go b/internal/frontend/directory.go
index 397cf8e..f9ec29e 100644
--- a/internal/frontend/directory.go
+++ b/internal/frontend/directory.go
@@ -83,7 +83,7 @@
 // the module path. However, on the package and directory view's
 // "Subdirectories" tab, we do not want to include packages whose import paths
 // are the same as the dirPath.
-func fetchDirectoryDetails(ctx context.Context, ds internal.DataSource, dirPath string, mi *internal.LegacyModuleInfo,
+func fetchDirectoryDetails(ctx context.Context, ds internal.DataSource, dirPath string, mi *internal.ModuleInfo,
 	licmetas []*licenses.Metadata, includeDirPath bool) (_ *Directory, err error) {
 	defer derrors.Wrap(&err, "s.ds.fetchDirectoryDetails(%q, %q, %q, %v)", dirPath, mi.ModulePath, mi.Version, licmetas)
 
@@ -97,7 +97,7 @@
 			return nil, err
 		}
 		return createDirectory(&internal.LegacyDirectory{
-			LegacyModuleInfo: *mi,
+			LegacyModuleInfo: internal.LegacyModuleInfo{ModuleInfo: *mi},
 			Path:             dirPath,
 			Packages:         pkgs,
 		}, licmetas, includeDirPath)
@@ -106,7 +106,7 @@
 	dbDir, err := ds.GetDirectory(ctx, dirPath, mi.ModulePath, mi.Version, internal.AllFields)
 	if errors.Is(err, derrors.NotFound) {
 		return createDirectory(&internal.LegacyDirectory{
-			LegacyModuleInfo: *mi,
+			LegacyModuleInfo: internal.LegacyModuleInfo{ModuleInfo: *mi},
 			Path:             dirPath,
 			Packages:         nil,
 		}, licmetas, includeDirPath)
@@ -134,7 +134,7 @@
 		if !includeDirPath && pkg.Path == dbDir.Path {
 			continue
 		}
-		newPkg, err := createPackage(pkg, &dbDir.LegacyModuleInfo, false)
+		newPkg, err := createPackage(pkg, &dbDir.ModuleInfo, false)
 		if err != nil {
 			return nil, err
 		}
@@ -147,7 +147,7 @@
 		}
 		packages = append(packages, newPkg)
 	}
-	mod := createModule(&dbDir.LegacyModuleInfo, licmetas, false)
+	mod := createModule(&dbDir.ModuleInfo, licmetas, false)
 	sort.Slice(packages, func(i, j int) bool { return packages[i].Path < packages[j].Path })
 
 	return &Directory{
diff --git a/internal/frontend/directory_test.go b/internal/frontend/directory_test.go
index c18d7fe..58f355f 100644
--- a/internal/frontend/directory_test.go
+++ b/internal/frontend/directory_test.go
@@ -29,7 +29,7 @@
 	checkDirectory := func(got *Directory, dirPath, modulePath, version string, suffixes []string) {
 		t.Helper()
 
-		mi := sample.LegacyModuleInfo(modulePath, version)
+		mi := sample.ModuleInfo(modulePath, version)
 		var wantPkgs []*Package
 		for _, suffix := range suffixes {
 			sp := sample.LegacyPackage(modulePath, suffix)
diff --git a/internal/frontend/header.go b/internal/frontend/header.go
index 7a71141..e31f7d4 100644
--- a/internal/frontend/header.go
+++ b/internal/frontend/header.go
@@ -49,7 +49,7 @@
 // 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 createPackage(pkg *internal.LegacyPackage, mi *internal.LegacyModuleInfo, latestRequested bool) (_ *Package, err error) {
+func createPackage(pkg *internal.LegacyPackage, mi *internal.ModuleInfo, latestRequested bool) (_ *Package, err error) {
 	defer derrors.Wrap(&err, "createPackage(%v, %v)", pkg, mi)
 
 	if pkg == nil || mi == nil {
@@ -99,7 +99,7 @@
 		}
 	}
 
-	m := createModule(&vdir.LegacyModuleInfo, modLicenses, latestRequested)
+	m := createModule(&vdir.ModuleInfo, modLicenses, latestRequested)
 	urlVersion := m.LinkVersion
 	if latestRequested {
 		urlVersion = internal.LatestVersion
@@ -121,7 +121,7 @@
 // latestRequested indicates whether the user requested the latest
 // version of the package. If so, the returned Module.URL will have the
 // structure /<path> instead of /<path>@<version>.
-func createModule(mi *internal.LegacyModuleInfo, licmetas []*licenses.Metadata, latestRequested bool) *Module {
+func createModule(mi *internal.ModuleInfo, licmetas []*licenses.Metadata, latestRequested bool) *Module {
 	urlVersion := linkVersion(mi.Version, mi.ModulePath)
 	if latestRequested {
 		urlVersion = internal.LatestVersion
diff --git a/internal/frontend/header_test.go b/internal/frontend/header_test.go
index 2eb42bf..7f06ac2 100644
--- a/internal/frontend/header_test.go
+++ b/internal/frontend/header_test.go
@@ -138,7 +138,7 @@
 		},
 	} {
 		t.Run(tc.label, func(t *testing.T) {
-			got, err := createPackage(&tc.pkg.LegacyPackage, &tc.pkg.LegacyModuleInfo, false)
+			got, err := createPackage(&tc.pkg.LegacyPackage, &tc.pkg.ModuleInfo, false)
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/internal/frontend/module.go b/internal/frontend/module.go
index e2f9d15..71eb236 100644
--- a/internal/frontend/module.go
+++ b/internal/frontend/module.go
@@ -77,7 +77,7 @@
 		return err
 	}
 
-	modHeader := createModule(mi, licensesToMetadatas(licenses), requestedVersion == internal.LatestVersion)
+	modHeader := createModule(&mi.ModuleInfo, licensesToMetadatas(licenses), requestedVersion == internal.LatestVersion)
 	tab := r.FormValue("tab")
 	settings, ok := moduleTabLookup[tab]
 	if !ok {
diff --git a/internal/frontend/overview.go b/internal/frontend/overview.go
index 4a05917..7029c0f 100644
--- a/internal/frontend/overview.go
+++ b/internal/frontend/overview.go
@@ -34,7 +34,7 @@
 
 // versionedLinks says whether the constructed URLs should have versions.
 // constructOverviewDetails uses the given version to construct an OverviewDetails.
-func constructOverviewDetails(mi *internal.LegacyModuleInfo, isRedistributable bool, versionedLinks bool) *OverviewDetails {
+func constructOverviewDetails(mi *internal.ModuleInfo, readme *internal.Readme, isRedistributable bool, versionedLinks bool) *OverviewDetails {
 	var lv string
 	if versionedLinks {
 		lv = linkVersion(mi.Version, mi.ModulePath)
@@ -47,16 +47,17 @@
 		RepositoryURL:   mi.SourceInfo.RepoURL(),
 		Redistributable: isRedistributable,
 	}
-	if overview.Redistributable {
-		overview.ReadMeSource = fileSource(mi.ModulePath, mi.Version, mi.LegacyReadmeFilePath)
-		overview.ReadMe = readmeHTML(mi)
+	if overview.Redistributable && readme != nil {
+		overview.ReadMeSource = fileSource(mi.ModulePath, mi.Version, readme.Filepath)
+		overview.ReadMe = readmeHTML(mi, readme)
 	}
 	return overview
 }
 
 // fetchPackageOverviewDetails uses data for the given package to return an OverviewDetails.
 func fetchPackageOverviewDetails(pkg *internal.LegacyVersionedPackage, versionedLinks bool) *OverviewDetails {
-	od := constructOverviewDetails(&pkg.LegacyModuleInfo, pkg.LegacyPackage.IsRedistributable, versionedLinks)
+	od := constructOverviewDetails(&pkg.ModuleInfo, &internal.Readme{Filepath: pkg.LegacyReadmeFilePath, Contents: pkg.LegacyReadmeContents},
+		pkg.LegacyPackage.IsRedistributable, versionedLinks)
 	od.PackageSourceURL = pkg.SourceInfo.DirectoryURL(packageSubdir(pkg.Path, pkg.ModulePath))
 	if !pkg.LegacyPackage.IsRedistributable {
 		od.Redistributable = false
@@ -66,12 +67,24 @@
 
 // fetchPackageOverviewDetailsNew uses data for the given versioned directory to return an OverviewDetails.
 func fetchPackageOverviewDetailsNew(vdir *internal.VersionedDirectory, versionedLinks bool) *OverviewDetails {
-	od := constructOverviewDetails(&vdir.LegacyModuleInfo, vdir.DirectoryNew.IsRedistributable, versionedLinks)
-	od.PackageSourceURL = vdir.SourceInfo.DirectoryURL(packageSubdir(vdir.Path, vdir.ModulePath))
-	if !vdir.DirectoryNew.IsRedistributable {
-		od.Redistributable = false
+	var lv string
+	if versionedLinks {
+		lv = linkVersion(vdir.Version, vdir.ModulePath)
+	} else {
+		lv = internal.LatestVersion
 	}
-	return od
+	overview := &OverviewDetails{
+		ModulePath:       vdir.ModulePath,
+		ModuleURL:        constructModuleURL(vdir.ModulePath, lv),
+		RepositoryURL:    vdir.SourceInfo.RepoURL(),
+		Redistributable:  vdir.DirectoryNew.IsRedistributable,
+		PackageSourceURL: vdir.SourceInfo.DirectoryURL(packageSubdir(vdir.Path, vdir.ModulePath)),
+	}
+	if overview.Redistributable && vdir.Readme != nil {
+		overview.ReadMeSource = fileSource(vdir.ModulePath, vdir.Version, vdir.Readme.Filepath)
+		overview.ReadMe = readmeHTML(&vdir.ModuleInfo, vdir.Readme)
+	}
+	return overview
 }
 
 // packageSubdir returns the subdirectory of the package relative to its module.
@@ -89,12 +102,12 @@
 // readmeHTML sanitizes readmeContents based on bluemondy.UGCPolicy and returns
 // a template.HTML. If readmeFilePath indicates that this is a markdown file,
 // it will also render the markdown contents using blackfriday.
-func readmeHTML(mi *internal.LegacyModuleInfo) template.HTML {
-	if len(mi.LegacyReadmeContents) == 0 {
+func readmeHTML(mi *internal.ModuleInfo, readme *internal.Readme) template.HTML {
+	if readme == nil {
 		return ""
 	}
-	if !isMarkdown(mi.LegacyReadmeFilePath) {
-		return template.HTML(fmt.Sprintf(`<pre class="readme">%s</pre>`, html.EscapeString(string(mi.LegacyReadmeContents))))
+	if !isMarkdown(readme.Filepath) {
+		return template.HTML(fmt.Sprintf(`<pre class="readme">%s</pre>`, html.EscapeString(string(readme.Contents))))
 	}
 
 	// bluemonday.UGCPolicy allows a broad selection of HTML elements and
@@ -114,10 +127,10 @@
 	// Render HTML similar to blackfriday.Run(), but here we implement a custom
 	// Walk function in order to modify image paths in the rendered HTML.
 	b := &bytes.Buffer{}
-	rootNode := parser.Parse([]byte(mi.LegacyReadmeContents))
+	rootNode := parser.Parse([]byte(readme.Contents))
 	rootNode.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
 		if node.Type == blackfriday.Image || node.Type == blackfriday.Link {
-			translateRelativeLink(node, mi)
+			translateRelativeLink(node, mi, readme)
 		}
 		return renderer.RenderNode(b, node, entering)
 	})
@@ -138,18 +151,20 @@
 // image files inside the repository. As the discovery site doesn't host the
 // full repository content, in order for the image to render, we need to
 // convert the relative path to an absolute URL to a hosted image.
-func translateRelativeLink(node *blackfriday.Node, mi *internal.LegacyModuleInfo) {
+func translateRelativeLink(node *blackfriday.Node, mi *internal.ModuleInfo, readme *internal.Readme) {
 	destURL, err := url.Parse(string(node.LinkData.Destination))
 	if err != nil || destURL.IsAbs() {
 		return
 	}
-
 	if destURL.Path == "" {
 		// This is a fragment; leave it.
 		return
 	}
+	if mi == nil {
+		return
+	}
 	// Paths are relative to the README location.
-	destPath := path.Join(path.Dir(mi.LegacyReadmeFilePath), path.Clean(destURL.Path))
+	destPath := path.Join(path.Dir(readme.Filepath), path.Clean(destURL.Path))
 	var newURL string
 	if node.Type == blackfriday.Image {
 		newURL = mi.SourceInfo.RawURL(destPath)
diff --git a/internal/frontend/overview_test.go b/internal/frontend/overview_test.go
index a5dc3aa..12cdfd9 100644
--- a/internal/frontend/overview_test.go
+++ b/internal/frontend/overview_test.go
@@ -45,7 +45,8 @@
 		t.Fatal(err)
 	}
 
-	got := constructOverviewDetails(&tc.module.LegacyModuleInfo, true, true)
+	readme := &internal.Readme{Filepath: tc.module.LegacyReadmeFilePath, Contents: tc.module.LegacyReadmeContents}
+	got := constructOverviewDetails(&tc.module.ModuleInfo, readme, true, true)
 	if diff := cmp.Diff(tc.wantDetails, got); diff != "" {
 		t.Errorf("constructOverviewDetails(%q, %q) mismatch (-want +got):\n%s", tc.module.LegacyPackages[0].Path, tc.module.Version, diff)
 	}
@@ -64,8 +65,12 @@
 				DirectoryNew: internal.DirectoryNew{
 					Path:              "github.com/u/m/p",
 					IsRedistributable: true,
+					Readme: &internal.Readme{
+						Filepath: "README.md",
+						Contents: "readme",
+					},
 				},
-				LegacyModuleInfo: *sample.LegacyModuleInfo("github.com/u/m", "v1.2.3"),
+				ModuleInfo: *sample.ModuleInfo("github.com/u/m", "v1.2.3"),
 			},
 			versionedLinks: true,
 			want: &OverviewDetails{
@@ -84,8 +89,12 @@
 				DirectoryNew: internal.DirectoryNew{
 					Path:              "github.com/u/m/p",
 					IsRedistributable: true,
+					Readme: &internal.Readme{
+						Filepath: "README.md",
+						Contents: "readme",
+					},
 				},
-				LegacyModuleInfo: *sample.LegacyModuleInfo("github.com/u/m", "v1.2.3"),
+				ModuleInfo: *sample.ModuleInfo("github.com/u/m", "v1.2.3"),
 			},
 			versionedLinks: false,
 			want: &OverviewDetails{
@@ -105,7 +114,7 @@
 					Path:              "github.com/u/m/p",
 					IsRedistributable: false,
 				},
-				LegacyModuleInfo: *sample.LegacyModuleInfo("github.com/u/m", "v1.2.3"),
+				ModuleInfo: *sample.ModuleInfo("github.com/u/m", "v1.2.3"),
 			},
 			versionedLinks: true,
 			want: &OverviewDetails{
@@ -119,24 +128,27 @@
 			},
 		},
 	} {
-		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)
-		}
+		t.Run(test.name, func(t *testing.T) {
+			got := fetchPackageOverviewDetailsNew(test.vdir, test.versionedLinks)
+			if diff := cmp.Diff(test.want, got); diff != "" {
+				t.Errorf("mismatch (-want +got):\n%s", diff)
+			}
+		})
 	}
 }
 
 func TestReadmeHTML(t *testing.T) {
-	testCases := []struct {
-		name string
-		mi   *internal.LegacyModuleInfo
-		want template.HTML
+	for _, tc := range []struct {
+		name   string
+		mi     *internal.ModuleInfo
+		readme *internal.Readme
+		want   template.HTML
 	}{
 		{
 			name: "valid markdown readme",
-			mi: &internal.LegacyModuleInfo{
-				LegacyReadmeFilePath: "README.md",
-				LegacyReadmeContents: "This package collects pithy sayings.\n\n" +
+			readme: &internal.Readme{
+				Filepath: "README.md",
+				Contents: "This package collects pithy sayings.\n\n" +
 					"It's part of a demonstration of\n" +
 					"[package versioning in Go](https://research.swtch.com/vgo1).",
 			},
@@ -146,9 +158,9 @@
 		},
 		{
 			name: "valid markdown readme with alternative case and extension",
-			mi: &internal.LegacyModuleInfo{
-				LegacyReadmeFilePath: "README.MARKDOWN",
-				LegacyReadmeContents: "This package collects pithy sayings.\n\n" +
+			readme: &internal.Readme{
+				Filepath: "README.MARKDOWN",
+				Contents: "This package collects pithy sayings.\n\n" +
 					"It's part of a demonstration of\n" +
 					"[package versioning in Go](https://research.swtch.com/vgo1).",
 			},
@@ -158,9 +170,9 @@
 		},
 		{
 			name: "not markdown readme",
-			mi: &internal.LegacyModuleInfo{
-				LegacyReadmeFilePath: "README.rst",
-				LegacyReadmeContents: "This package collects pithy sayings.\n\n" +
+			readme: &internal.Readme{
+				Filepath: "README.rst",
+				Contents: "This package collects pithy sayings.\n\n" +
 					"It's part of a demonstration of\n" +
 					"[package versioning in Go](https://research.swtch.com/vgo1).",
 			},
@@ -168,80 +180,89 @@
 		},
 		{
 			name: "empty readme",
-			mi:   &internal.LegacyModuleInfo{},
+			mi:   &internal.ModuleInfo{},
 			want: "",
 		},
 		{
 			name: "sanitized readme",
-			mi: &internal.LegacyModuleInfo{
-				LegacyReadmeFilePath: "README",
-				LegacyReadmeContents: `<a onblur="alert(secret)" href="http://www.google.com">Google</a>`,
+			readme: &internal.Readme{
+				Filepath: "README",
+				Contents: `<a onblur="alert(secret)" href="http://www.google.com">Google</a>`,
 			},
 			want: template.HTML(`<pre class="readme">&lt;a onblur=&#34;alert(secret)&#34; href=&#34;http://www.google.com&#34;&gt;Google&lt;/a&gt;</pre>`),
 		},
 		{
 			name: "relative image markdown is made absolute for GitHub",
-			mi: &internal.LegacyModuleInfo{
-				LegacyReadmeFilePath: "README.md",
-				LegacyReadmeContents: "![Go logo](doc/logo.png)",
-				SourceInfo:           source.NewGitHubInfo("http://github.com/golang/go", "", "master"),
+			mi: &internal.ModuleInfo{
+				SourceInfo: source.NewGitHubInfo("http://github.com/golang/go", "", "master"),
+			},
+			readme: &internal.Readme{
+				Filepath: "README.md",
+				Contents: "![Go logo](doc/logo.png)",
 			},
 			want: template.HTML("<p><img src=\"https://raw.githubusercontent.com/golang/go/master/doc/logo.png\" alt=\"Go logo\"/></p>\n"),
 		},
 		{
 			name: "relative image markdown is made absolute for GitLab",
-			mi: &internal.LegacyModuleInfo{
-				LegacyReadmeFilePath: "README.md",
-				LegacyReadmeContents: "![Gitaly benchmark timings.](doc/img/rugged-new-timings.png)",
-				SourceInfo:           source.NewGitLabInfo("http://gitlab.com/gitlab-org/gitaly", "", "v1.0.0"),
+			mi: &internal.ModuleInfo{
+				SourceInfo: source.NewGitLabInfo("http://gitlab.com/gitlab-org/gitaly", "", "v1.0.0"),
+			},
+			readme: &internal.Readme{
+				Filepath: "README.md",
+				Contents: "![Gitaly benchmark timings.](doc/img/rugged-new-timings.png)",
 			},
 			want: template.HTML("<p><img src=\"http://gitlab.com/gitlab-org/gitaly/raw/v1.0.0/doc/img/rugged-new-timings.png\" alt=\"Gitaly benchmark timings.\"/></p>\n"),
 		},
 		{
 			name: "relative image markdown is left alone for unknown origins",
-			mi: &internal.LegacyModuleInfo{
-				LegacyReadmeFilePath: "README.md",
-				LegacyReadmeContents: "![Go logo](doc/logo.png)",
+			readme: &internal.Readme{
+				Filepath: "README.md",
+				Contents: "![Go logo](doc/logo.png)",
 			},
 			want: template.HTML("<p><img src=\"doc/logo.png\" alt=\"Go logo\"/></p>\n"),
 		},
 		{
 			name: "module versions are referenced in relative images",
-			mi: &internal.LegacyModuleInfo{
-				LegacyReadmeFilePath: "README.md",
-				LegacyReadmeContents: "![Hugo logo](doc/logo.png)",
-				Version:              "v0.56.3",
-				VersionType:          version.TypeRelease,
-				SourceInfo:           source.NewGitHubInfo("http://github.com/gohugoio/hugo", "", "v0.56.3"),
+			mi: &internal.ModuleInfo{
+				Version:     "v0.56.3",
+				VersionType: version.TypeRelease,
+				SourceInfo:  source.NewGitHubInfo("http://github.com/gohugoio/hugo", "", "v0.56.3"),
+			},
+			readme: &internal.Readme{
+				Filepath: "README.md",
+				Contents: "![Hugo logo](doc/logo.png)",
 			},
 			want: template.HTML("<p><img src=\"https://raw.githubusercontent.com/gohugoio/hugo/v0.56.3/doc/logo.png\" alt=\"Hugo logo\"/></p>\n"),
 		},
 		{
 			name: "image URLs relative to README directory",
-			mi: &internal.LegacyModuleInfo{
-				LegacyReadmeFilePath: "dir/sub/README.md",
-				LegacyReadmeContents: "![alt](img/thing.png)",
-				Version:              "v1.2.3",
-				VersionType:          version.TypeRelease,
-				SourceInfo:           source.NewGitHubInfo("https://github.com/some/repo", "", "v1.2.3"),
+			mi: &internal.ModuleInfo{
+				Version:     "v1.2.3",
+				VersionType: version.TypeRelease,
+				SourceInfo:  source.NewGitHubInfo("https://github.com/some/repo", "", "v1.2.3"),
+			},
+			readme: &internal.Readme{
+				Filepath: "dir/sub/README.md",
+				Contents: "![alt](img/thing.png)",
 			},
 			want: template.HTML(`<p><img src="https://raw.githubusercontent.com/some/repo/v1.2.3/dir/sub/img/thing.png" alt="alt"/></p>` + "\n"),
 		},
 		{
 			name: "non-image links relative to README directory",
-			mi: &internal.LegacyModuleInfo{
-				LegacyReadmeFilePath: "dir/sub/README.md",
-				LegacyReadmeContents: "[something](doc/thing.md)",
-				Version:              "v1.2.3",
-				VersionType:          version.TypeRelease,
-				SourceInfo:           source.NewGitHubInfo("https://github.com/some/repo", "", "v1.2.3"),
+			mi: &internal.ModuleInfo{
+				Version:     "v1.2.3",
+				VersionType: version.TypeRelease,
+				SourceInfo:  source.NewGitHubInfo("https://github.com/some/repo", "", "v1.2.3"),
+			},
+			readme: &internal.Readme{
+				Filepath: "dir/sub/README.md",
+				Contents: "[something](doc/thing.md)",
 			},
 			want: template.HTML(`<p><a href="https://github.com/some/repo/blob/v1.2.3/dir/sub/doc/thing.md" rel="nofollow">something</a></p>` + "\n"),
 		},
-	}
-	for _, tc := range testCases {
+	} {
 		t.Run(tc.name, func(t *testing.T) {
-			got := readmeHTML(tc.mi)
+			got := readmeHTML(tc.mi, tc.readme)
 			if diff := cmp.Diff(tc.want, got); diff != "" {
 				t.Errorf("readmeHTML(%v) mismatch (-want +got):\n%s", tc.mi, diff)
 			}
diff --git a/internal/frontend/package.go b/internal/frontend/package.go
index 1cc20b6..75813ec 100644
--- a/internal/frontend/package.go
+++ b/internal/frontend/package.go
@@ -106,7 +106,7 @@
 			derrors.Wrap(&err, "servePackagePageWithPackage(w, r, %q, %q, %q)", pkg.Path, pkg.ModulePath, requestedVersion)
 		}
 	}()
-	pkgHeader, err := createPackage(&pkg.LegacyPackage, &pkg.LegacyModuleInfo, requestedVersion == internal.LatestVersion)
+	pkgHeader, err := createPackage(&pkg.LegacyPackage, &pkg.ModuleInfo, requestedVersion == internal.LatestVersion)
 	if err != nil {
 		return fmt.Errorf("creating package header for %s@%s: %v", pkg.Path, pkg.Version, err)
 	}
@@ -220,8 +220,8 @@
 	return "", nil
 }
 
-func (s *Server) servePackagePageWithVersionedDirectory(ctx context.Context, w http.ResponseWriter, r *http.Request, vdir *internal.VersionedDirectory, requestedVersion string) error {
-
+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)
diff --git a/internal/frontend/search_test.go b/internal/frontend/search_test.go
index fb1f449..6e1d59d 100644
--- a/internal/frontend/search_test.go
+++ b/internal/frontend/search_test.go
@@ -25,12 +25,14 @@
 		now       = sample.NowTruncated()
 		moduleFoo = &internal.Module{
 			LegacyModuleInfo: internal.LegacyModuleInfo{
-				ModulePath:           "github.com/mod/foo",
-				Version:              "v1.0.0",
+				ModuleInfo: internal.ModuleInfo{
+					ModulePath:        "github.com/mod/foo",
+					Version:           "v1.0.0",
+					CommitTime:        now,
+					VersionType:       version.TypeRelease,
+					IsRedistributable: true,
+				},
 				LegacyReadmeContents: "readme",
-				CommitTime:           now,
-				VersionType:          version.TypeRelease,
-				IsRedistributable:    true,
 			},
 			LegacyPackages: []*internal.LegacyPackage{
 				{
@@ -44,12 +46,14 @@
 		}
 		moduleBar = &internal.Module{
 			LegacyModuleInfo: internal.LegacyModuleInfo{
-				ModulePath:           "github.com/mod/bar",
-				Version:              "v1.0.0",
+				ModuleInfo: internal.ModuleInfo{
+					ModulePath:        "github.com/mod/bar",
+					Version:           "v1.0.0",
+					CommitTime:        now,
+					VersionType:       version.TypeRelease,
+					IsRedistributable: true,
+				},
 				LegacyReadmeContents: "readme",
-				CommitTime:           now,
-				VersionType:          version.TypeRelease,
-				IsRedistributable:    true,
 			},
 			LegacyPackages: []*internal.LegacyPackage{
 				{
diff --git a/internal/frontend/server_test.go b/internal/frontend/server_test.go
index ed0e30a..ee43693 100644
--- a/internal/frontend/server_test.go
+++ b/internal/frontend/server_test.go
@@ -18,6 +18,7 @@
 	"golang.org/x/net/html"
 	"golang.org/x/pkgsite/internal"
 	"golang.org/x/pkgsite/internal/experiment"
+	"golang.org/x/pkgsite/internal/log"
 	"golang.org/x/pkgsite/internal/middleware"
 	"golang.org/x/pkgsite/internal/postgres"
 	"golang.org/x/pkgsite/internal/proxy"
@@ -151,6 +152,7 @@
 			for _, p := range ps {
 				sample.AddPackage(m, p)
 			}
+			log.Info(ctx, m.ModulePath)
 			if err := testDB.InsertModule(ctx, m); err != nil {
 				t.Fatal(err)
 			}
@@ -185,11 +187,10 @@
 	defer cancel()
 	defer postgres.ResetTestDB(testDB, t)
 
-	// Experiments need to be set in the context, for DB work, and as
-	// a middleware, for request handling.
+	// Experiments need to be set in the context, for DB work, and as a
+	// middleware, for request handling.
 	ctx = experimentContext(ctx, experimentNames...)
 	insertTestModules(ctx, t, testModules)
-
 	_, handler, _ := newTestServer(t, nil, experimentNames...)
 
 	var (
@@ -340,7 +341,6 @@
 		versioned   = true
 		unversioned = false
 	)
-
 	for _, tc := range []struct {
 		// name of the test
 		name string
@@ -417,7 +417,7 @@
 			want:           pagecheck.PackageHeader(pkgNonRedist, unversioned),
 		},
 		{
-			name:           "package@version default",
+			name:           "package at version default",
 			urlPath:        fmt.Sprintf("/%s@%s/%s?tab=doc", sample.ModulePath, sample.VersionString, sample.Suffix),
 			wantStatusCode: http.StatusOK,
 			want: in("",
@@ -425,14 +425,14 @@
 				in(".Documentation", text(`This is the documentation HTML`))),
 		},
 		{
-			name: "package@version default specific version nonredistributable",
+			name: "package at version default specific version nonredistributable",
 			// For a non-redistributable package, the name@version route goes to the overview tab.
 			urlPath:        "/github.com/non_redistributable@v1.0.0/bar?tab=overview",
 			wantStatusCode: http.StatusOK,
 			want:           pagecheck.PackageHeader(pkgNonRedist, versioned),
 		},
 		{
-			name:           "package@version doc tab",
+			name:           "package at version doc tab",
 			urlPath:        fmt.Sprintf("/%s@%s/%s?tab=doc", sample.ModulePath, "v0.9.0", sample.Suffix),
 			wantStatusCode: http.StatusOK,
 			want: in("",
@@ -440,14 +440,14 @@
 				in(".Documentation", text(`This is the documentation HTML`))),
 		},
 		{
-			name:           "package@version doc with links",
+			name:           "package at version doc with links",
 			urlPath:        "/github.com/valid_module_name/foo/directory/hello?tab=doc",
 			wantStatusCode: http.StatusOK,
 			want: in(".Documentation",
 				in("a", href("/pkg/io#Writer"), text("io.Writer"))),
 		},
 		{
-			name:             "package@version doc with hacked up links",
+			name:             "package at version doc with hacked up links",
 			urlPath:          "/github.com/valid_module_name/foo/directory/hello?tab=doc",
 			addDocQueryParam: true,
 			wantStatusCode:   http.StatusOK,
@@ -455,7 +455,7 @@
 				in("a", href("/io?tab=doc#Writer"), text("io.Writer"))),
 		},
 		{
-			name: "package@version doc tab nonredistributable",
+			name: "package at version doc tab nonredistributable",
 			// For a non-redistributable package, the doc tab will not show the doc.
 			urlPath:        "/github.com/non_redistributable@v1.0.0/bar?tab=doc",
 			wantStatusCode: http.StatusOK,
@@ -464,7 +464,7 @@
 				in(".DetailsContent", text(`not displayed due to license restrictions`))),
 		},
 		{
-			name:           "package@version readme tab",
+			name:           "package at version readme tab redistributable",
 			urlPath:        fmt.Sprintf("/%s@%s/%s?tab=overview", sample.ModulePath, sample.VersionString, sample.Suffix),
 			wantStatusCode: http.StatusOK,
 			want: in("",
@@ -479,7 +479,7 @@
 				})),
 		},
 		{
-			name: "package@version readme tab nonredistributable",
+			name: "package at version readme tab nonredistributable",
 			// For a non-redistributable package, the readme tab will not show the readme.
 			urlPath:        "/github.com/non_redistributable@v1.0.0/bar?tab=overview",
 			wantStatusCode: http.StatusOK,
@@ -488,7 +488,7 @@
 				in(".DetailsContent", text(`not displayed due to license restrictions`))),
 		},
 		{
-			name:           "package@version subdirectories tab",
+			name:           "package at version subdirectories tab",
 			urlPath:        fmt.Sprintf("/%s@%s/%s?tab=subdirectories", sample.ModulePath, sample.VersionString, sample.Suffix),
 			wantStatusCode: http.StatusOK,
 			want: in("",
@@ -499,7 +499,7 @@
 						text("directory/hello")))),
 		},
 		{
-			name:           "package@version versions tab",
+			name:           "package at version versions tab",
 			urlPath:        fmt.Sprintf("/%s@%s/%s?tab=versions", sample.ModulePath, sample.VersionString, sample.Suffix),
 			wantStatusCode: http.StatusOK,
 			want: in("",
@@ -512,7 +512,7 @@
 						text("v1.0.0")))),
 		},
 		{
-			name:           "package@version imports tab",
+			name:           "package at version imports tab",
 			urlPath:        fmt.Sprintf("/%s@%s/%s?tab=imports", sample.ModulePath, sample.VersionString, sample.Suffix),
 			wantStatusCode: http.StatusOK,
 			want: in("",
@@ -524,7 +524,7 @@
 					in("li:nth-child(2) a", href("/path/to/bar"), text("path/to/bar")))),
 		},
 		{
-			name:           "package@version imported by tab",
+			name:           "package at version imported by tab",
 			urlPath:        fmt.Sprintf("/%s@%s/%s?tab=importedby", sample.ModulePath, sample.VersionString, sample.Suffix),
 			wantStatusCode: http.StatusOK,
 			want: in("",
@@ -532,7 +532,7 @@
 				in(".EmptyContent-message", text(`No known importers for this package`))),
 		},
 		{
-			name:           "package@version imported by tab second page",
+			name:           "package at version imported by tab second page",
 			urlPath:        fmt.Sprintf("/%s@%s/%s?tab=importedby&page=2", sample.ModulePath, sample.VersionString, sample.Suffix),
 			wantStatusCode: http.StatusOK,
 			want: in("",
@@ -540,7 +540,7 @@
 				in(".EmptyContent-message", text(`No known importers for this package`))),
 		},
 		{
-			name:           "package@version licenses tab",
+			name:           "package at version licenses tab",
 			urlPath:        fmt.Sprintf("/%s@%s/%s?tab=licenses", sample.ModulePath, sample.VersionString, sample.Suffix),
 			wantStatusCode: http.StatusOK,
 			want: in("",
@@ -548,14 +548,14 @@
 				pagecheck.LicenseDetails("MIT", "Lorem Ipsum", "github.com/valid_module_name@v1.0.0/LICENSE")),
 		},
 		{
-			name:           "package@version overview tab, pseudoversion",
+			name:           "package at version overview tab, pseudoversion",
 			urlPath:        fmt.Sprintf("/%s@%s/%s?tab=overview", sample.ModulePath, pseudoVersion, sample.Suffix),
 			wantStatusCode: http.StatusOK,
 			want: in("",
 				pagecheck.PackageHeader(pkgPseudo, versioned)),
 		},
 		{
-			name:           "package@version overview tab, +incompatible",
+			name:           "package at version overview tab, +incompatible",
 			urlPath:        "/github.com/incompatible@v1.0.0+incompatible/dir/inc?tab=overview",
 			wantStatusCode: http.StatusOK,
 			want: in("",
@@ -816,10 +816,10 @@
 
 			if tc.want != nil {
 				if err := tc.want(doc); err != nil {
-					t.Error(err)
 					if testing.Verbose() {
 						html.Render(os.Stdout, doc)
 					}
+					t.Error(err)
 				}
 			}
 		})
@@ -835,20 +835,22 @@
 	if err := testDB.InsertModule(ctx, sampleModule); err != nil {
 		t.Fatal(err)
 	}
-
 	_, handler, _ := newTestServer(t, nil)
+
 	for _, test := range []struct {
-		path     string
-		wantCode int
+		name, path string
+		wantCode   int
 	}{
-		{"/invalid-page", http.StatusNotFound},
-		{"/gocloud.dev/@latest/blob", http.StatusBadRequest},
+		{"not found", "/invalid-page", http.StatusNotFound},
+		{"bad request", "/gocloud.dev/@latest/blob", http.StatusBadRequest},
 	} {
-		w := httptest.NewRecorder()
-		handler.ServeHTTP(w, httptest.NewRequest("GET", test.path, nil))
-		if w.Code != test.wantCode {
-			t.Errorf("%q: got status code = %d, want %d", test.path, w.Code, test.wantCode)
-		}
+		t.Run(test.name, func(t *testing.T) {
+			w := httptest.NewRecorder()
+			handler.ServeHTTP(w, httptest.NewRequest("GET", test.path, nil))
+			if w.Code != test.wantCode {
+				t.Errorf("%q: got status code = %d, want %d", test.path, w.Code, test.wantCode)
+			}
+		})
 	}
 }
 
@@ -914,9 +916,11 @@
 		{"/", mustRequest("http://localhost/foo?tab=imports"), "imports"},
 	}
 	for _, test := range tests {
-		if got := TagRoute(test.route, test.req); got != test.want {
-			t.Errorf("TagRoute(%q, %v) = %q, want %q", test.route, test.req, got, test.want)
-		}
+		t.Run(test.want, func(t *testing.T) {
+			if got := TagRoute(test.route, test.req); got != test.want {
+				t.Errorf("TagRoute(%q, %v) = %q, want %q", test.route, test.req, got, test.want)
+			}
+		})
 	}
 }
 
@@ -928,9 +932,9 @@
 	return experiment.NewContext(ctx, experiment.NewSet(expmap))
 }
 
-func newTestServer(t *testing.T, modules []*proxy.TestModule, experimentNames ...string) (*Server, http.Handler, func()) {
+func newTestServer(t *testing.T, proxyModules []*proxy.TestModule, experimentNames ...string) (*Server, http.Handler, func()) {
 	t.Helper()
-	proxyClient, teardown := proxy.SetupTestProxy(t, modules)
+	proxyClient, teardown := proxy.SetupTestProxy(t, proxyModules)
 	sourceClient := source.NewClient(sourceTimeout)
 	ctx := context.Background()
 
diff --git a/internal/frontend/tabs.go b/internal/frontend/tabs.go
index 1d1ded5..30faa36 100644
--- a/internal/frontend/tabs.go
+++ b/internal/frontend/tabs.go
@@ -148,7 +148,7 @@
 	case "versions":
 		return fetchPackageVersionsDetails(ctx, ds, pkg.Path, pkg.V1Path, pkg.ModulePath)
 	case "subdirectories":
-		return fetchDirectoryDetails(ctx, ds, pkg.Path, &pkg.LegacyModuleInfo, pkg.Licenses, false)
+		return fetchDirectoryDetails(ctx, ds, pkg.Path, &pkg.ModuleInfo, pkg.Licenses, false)
 	case "imports":
 		return fetchImportsDetails(ctx, ds, pkg.Path, pkg.ModulePath, pkg.Version)
 	case "importedby":
@@ -163,14 +163,15 @@
 
 // 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) {
+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.LegacyModuleInfo, vdir.Licenses, false)
+		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":
@@ -192,14 +193,15 @@
 func fetchDetailsForModule(ctx context.Context, r *http.Request, tab string, ds internal.DataSource, mi *internal.LegacyModuleInfo, licenses []*licenses.License) (interface{}, error) {
 	switch tab {
 	case "packages":
-		return fetchDirectoryDetails(ctx, ds, mi.ModulePath, mi, licensesToMetadatas(licenses), true)
+		return fetchDirectoryDetails(ctx, ds, mi.ModulePath, &mi.ModuleInfo, licensesToMetadatas(licenses), true)
 	case "licenses":
 		return &LicensesDetails{Licenses: transformLicenses(mi.ModulePath, mi.Version, licenses)}, nil
 	case "versions":
 		return fetchModuleVersionsDetails(ctx, ds, mi)
 	case "overview":
 		// TODO(b/138448402): implement remaining module views.
-		return constructOverviewDetails(mi, mi.IsRedistributable, urlIsVersioned(r.URL)), nil
+		readme := &internal.Readme{Filepath: mi.LegacyReadmeFilePath, Contents: mi.LegacyReadmeContents}
+		return constructOverviewDetails(&mi.ModuleInfo, readme, mi.IsRedistributable, urlIsVersioned(r.URL)), nil
 	}
 	return nil, fmt.Errorf("BUG: unable to fetch details: unknown tab %q", tab)
 }
@@ -209,7 +211,8 @@
 func constructDetailsForDirectory(r *http.Request, tab string, dir *internal.LegacyDirectory, licenses []*licenses.License) (interface{}, error) {
 	switch tab {
 	case "overview":
-		return constructOverviewDetails(&dir.LegacyModuleInfo, dir.LegacyModuleInfo.IsRedistributable, urlIsVersioned(r.URL)), nil
+		readme := &internal.Readme{Filepath: dir.LegacyReadmeFilePath, Contents: dir.LegacyReadmeContents}
+		return constructOverviewDetails(&dir.ModuleInfo, readme, dir.LegacyModuleInfo.IsRedistributable, urlIsVersioned(r.URL)), nil
 	case "subdirectories":
 		// Ideally we would just use fetchDirectoryDetails here so that it
 		// follows the same code path as fetchDetailsForModule and
diff --git a/internal/postgres/details.go b/internal/postgres/details.go
index c075582..f9b7908 100644
--- a/internal/postgres/details.go
+++ b/internal/postgres/details.go
@@ -470,11 +470,11 @@
 		}
 		return nil, fmt.Errorf("row.Scan(): %v", err)
 	}
-	setHasGoMod(&mi, hasGoMod)
+	setHasGoMod(&mi.ModuleInfo, hasGoMod)
 	return &mi, nil
 }
 
-func setHasGoMod(mi *internal.LegacyModuleInfo, nb sql.NullBool) {
+func setHasGoMod(mi *internal.ModuleInfo, nb sql.NullBool) {
 	// The safe default value for HasGoMod is true, because search will penalize modules that don't have one.
 	// This is temporary: when has_go_mod is fully populated, we'll make it NOT NULL.
 	mi.HasGoMod = true
diff --git a/internal/postgres/details_test.go b/internal/postgres/details_test.go
index e2be719..1b14baf 100644
--- a/internal/postgres/details_test.go
+++ b/internal/postgres/details_test.go
@@ -203,24 +203,32 @@
 			modules:    testModules,
 			wantTaggedVersions: []*internal.LegacyModuleInfo{
 				{
-					ModulePath: modulePath2,
-					Version:    "v2.1.0",
-					CommitTime: sample.CommitTime,
+					ModuleInfo: internal.ModuleInfo{
+						ModulePath: modulePath2,
+						Version:    "v2.1.0",
+						CommitTime: sample.CommitTime,
+					},
 				},
 				{
-					ModulePath: modulePath2,
-					Version:    "v2.0.1-beta",
-					CommitTime: sample.CommitTime,
+					ModuleInfo: internal.ModuleInfo{
+						ModulePath: modulePath2,
+						Version:    "v2.0.1-beta",
+						CommitTime: sample.CommitTime,
+					},
 				},
 				{
-					ModulePath: modulePath1,
-					Version:    "v1.0.0",
-					CommitTime: sample.CommitTime,
+					ModuleInfo: internal.ModuleInfo{
+						ModulePath: modulePath1,
+						Version:    "v1.0.0",
+						CommitTime: sample.CommitTime,
+					},
 				},
 				{
-					ModulePath: modulePath1,
-					Version:    "v1.0.0-alpha.1",
-					CommitTime: sample.CommitTime,
+					ModuleInfo: internal.ModuleInfo{
+						ModulePath: modulePath1,
+						Version:    "v1.0.0-alpha.1",
+						CommitTime: sample.CommitTime,
+					},
 				},
 			},
 		},
@@ -254,9 +262,11 @@
 				// if there are more than 10 in the database
 				if i < 10 {
 					wantPseudoVersions = append(wantPseudoVersions, &internal.LegacyModuleInfo{
-						ModulePath: modulePath1,
-						Version:    fmt.Sprintf("v0.0.0-201806111833%02d-d8887717615a", tc.numPseudo-i),
-						CommitTime: sample.CommitTime,
+						ModuleInfo: internal.ModuleInfo{
+							ModulePath: modulePath1,
+							Version:    fmt.Sprintf("v0.0.0-201806111833%02d-d8887717615a", tc.numPseudo-i),
+							CommitTime: sample.CommitTime,
+						},
 					})
 				}
 			}
diff --git a/internal/postgres/directory.go b/internal/postgres/directory.go
index fbec3af..c788869 100644
--- a/internal/postgres/directory.go
+++ b/internal/postgres/directory.go
@@ -30,8 +30,6 @@
 			m.redistributable,
 			m.has_go_mod,
 			m.source_info,
-			m.readme_file_path,
-			m.readme_contents,
 			p.id,
 			p.path,
 			p.name,
@@ -39,8 +37,6 @@
 			p.redistributable,
 			p.license_types,
 			p.license_paths,
-			r.file_path,
-			r.contents,
 			d.goos,
 			d.goarch,
 			d.synopsis,
@@ -48,8 +44,6 @@
 		FROM modules m
 		INNER JOIN paths p
 		ON p.module_id = m.id
-		LEFT JOIN readmes r
-		ON r.path_id = p.id
 		LEFT JOIN documentation d
 		ON d.path_id = p.id
 		WHERE
@@ -57,9 +51,8 @@
 			AND m.module_path = $2
 			AND m.version = $3;`
 	var (
-		mi                         internal.LegacyModuleInfo
+		mi                         internal.ModuleInfo
 		dir                        internal.DirectoryNew
-		readme                     internal.Readme
 		doc                        internal.Documentation
 		pkg                        internal.PackageNew
 		licenseTypes, licensePaths []string
@@ -74,8 +67,6 @@
 		&mi.IsRedistributable,
 		&mi.HasGoMod,
 		jsonbScanner{&mi.SourceInfo},
-		&mi.LegacyReadmeFilePath,
-		&mi.LegacyReadmeContents,
 		&pathID,
 		&dir.Path,
 		database.NullIsEmpty(&pkg.Name),
@@ -83,8 +74,6 @@
 		&dir.IsRedistributable,
 		pq.Array(&licenseTypes),
 		pq.Array(&licensePaths),
-		database.NullIsEmpty(&readme.Filepath),
-		database.NullIsEmpty(&readme.Contents),
 		database.NullIsEmpty(&doc.GOOS),
 		database.NullIsEmpty(&doc.GOARCH),
 		database.NullIsEmpty(&doc.Synopsis),
@@ -120,13 +109,31 @@
 			return nil, err
 		}
 	}
+
+	// TODO(golang/go#38513): remove and query the readmes table directly once
+	// we start displaying READMEs for directories instead of the top-level
+	// module.
+	var readme internal.Readme
+	row = db.db.QueryRow(ctx, `
+		SELECT file_path, contents
+		FROM modules m
+		INNER JOIN paths p
+		ON p.module_id = m.id
+		INNER JOIN readmes r
+		ON p.id = r.path_id
+		WHERE
+		    module_path=$1
+			AND m.version=$2
+			AND m.module_path=p.path`, modulePath, version)
+	if err := row.Scan(&readme.Filepath, &readme.Contents); err != nil && err != sql.ErrNoRows {
+		return nil, err
+	}
 	if readme.Filepath != "" {
 		dir.Readme = &readme
 	}
-
 	return &internal.VersionedDirectory{
-		LegacyModuleInfo: mi,
-		DirectoryNew:     dir,
+		ModuleInfo:   mi,
+		DirectoryNew: dir,
 	}, nil
 }
 
@@ -224,7 +231,7 @@
 		if err := rows.Scan(scanArgs...); err != nil {
 			return fmt.Errorf("row.Scan(): %v", err)
 		}
-		setHasGoMod(&mi, hasGoMod)
+		setHasGoMod(&mi.ModuleInfo, hasGoMod)
 		lics, err := zipLicenseMetadata(licenseTypes, licensePaths)
 		if err != nil {
 			return err
diff --git a/internal/postgres/directory_test.go b/internal/postgres/directory_test.go
index 68895a0..77d7532 100644
--- a/internal/postgres/directory_test.go
+++ b/internal/postgres/directory_test.go
@@ -289,7 +289,6 @@
 			internal.ExperimentInsertDirectories: true}))
 
 	defer ResetTestDB(testDB, t)
-
 	InsertSampleDirectoryTree(ctx, t, testDB)
 
 	// Add a module that has READMEs in a directory and a package.
@@ -310,7 +309,7 @@
 
 	newVdir := func(path, modulePath, version string, readme *internal.Readme, pkg *internal.PackageNew) *internal.VersionedDirectory {
 		return &internal.VersionedDirectory{
-			LegacyModuleInfo: *sample.LegacyModuleInfo(modulePath, version),
+			ModuleInfo: *sample.ModuleInfo(modulePath, version),
 			DirectoryNew: internal.DirectoryNew{
 				Path:              path,
 				V1Path:            path,
@@ -434,6 +433,12 @@
 				// The packages table only includes partial license information; it omits the Coverage field.
 				cmpopts.IgnoreFields(licenses.Metadata{}, "Coverage"),
 			}
+			// TODO(golang/go#38513): remove once we start displaying
+			// READMEs for directories instead of the top-level module.
+			tc.want.Readme = &internal.Readme{
+				Filepath: sample.ReadmeFilePath,
+				Contents: sample.ReadmeContents,
+			}
 			if diff := cmp.Diff(tc.want, got, opts...); diff != "" {
 				t.Errorf("mismatch (-want, +got):\n%s", diff)
 			}
@@ -466,9 +471,6 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	if g, w := got.LegacyReadmeContents, internal.StringFieldMissing; g != w {
-		t.Errorf("LegacyReadmeContents = %q, want %q", g, w)
-	}
 	if g, w := got.Packages[0].DocumentationHTML, internal.StringFieldMissing; g != w {
 		t.Errorf("DocumentationHTML = %q, want %q", g, w)
 	}
diff --git a/internal/postgres/insert_module_test.go b/internal/postgres/insert_module_test.go
index fd61402..fb3e809 100644
--- a/internal/postgres/insert_module_test.go
+++ b/internal/postgres/insert_module_test.go
@@ -115,9 +115,15 @@
 		if err != nil {
 			t.Fatal(err)
 		}
+		// TODO(golang/go#38513): remove once we start displaying
+		// READMEs for directories instead of the top-level module.
+		dir.Readme = &internal.Readme{
+			Filepath: sample.ReadmeFilePath,
+			Contents: sample.ReadmeContents,
+		}
 		wantd := internal.VersionedDirectory{
-			DirectoryNew:     *dir,
-			LegacyModuleInfo: want.LegacyModuleInfo,
+			DirectoryNew: *dir,
+			ModuleInfo:   want.ModuleInfo,
 		}
 		opts := cmp.Options{
 			cmpopts.IgnoreFields(internal.LegacyModuleInfo{}, "LegacyReadmeFilePath"),
@@ -155,7 +161,9 @@
 	// Change the module, and re-insert.
 	m.IsRedistributable = !m.IsRedistributable
 	m.Licenses[0].Contents = append(m.Licenses[0].Contents, " and more"...)
-	m.Directories[0].Readme.Contents += " and more"
+	// TODO(golang/go#38513): uncomment line below once we start displaying
+	// READMEs for directories instead of the top-level module.
+	// m.Directories[0].Readme.Contents += " and more"
 	m.LegacyPackages[0].Synopsis = "New synopsis"
 	if err := testDB.InsertModule(ctx, m); err != nil {
 		t.Fatal(err)
diff --git a/internal/postgres/package.go b/internal/postgres/package.go
index e342dd4..803b45d 100644
--- a/internal/postgres/package.go
+++ b/internal/postgres/package.go
@@ -149,7 +149,7 @@
 		}
 		return nil, fmt.Errorf("row.Scan(): %v", err)
 	}
-	setHasGoMod(&pkg.LegacyModuleInfo, hasGoMod)
+	setHasGoMod(&pkg.ModuleInfo, hasGoMod)
 	lics, err := zipLicenseMetadata(licenseTypes, licensePaths)
 	if err != nil {
 		return nil, err
diff --git a/internal/postgres/path_test.go b/internal/postgres/path_test.go
index 49759a5..2cbd2de 100644
--- a/internal/postgres/path_test.go
+++ b/internal/postgres/path_test.go
@@ -45,10 +45,12 @@
 		pkgPath := path.Join(testModule.module, testModule.packageSuffix)
 		m := &internal.Module{
 			LegacyModuleInfo: internal.LegacyModuleInfo{
-				ModulePath:  testModule.module,
-				Version:     testModule.version,
-				VersionType: vtype,
-				CommitTime:  time.Now(),
+				ModuleInfo: internal.ModuleInfo{
+					ModulePath:  testModule.module,
+					Version:     testModule.version,
+					VersionType: vtype,
+					CommitTime:  time.Now(),
+				},
 			},
 			LegacyPackages: []*internal.LegacyPackage{{
 				Name: pkgName,
diff --git a/internal/proxydatasource/datasource.go b/internal/proxydatasource/datasource.go
index d863d9c..670e5ea 100644
--- a/internal/proxydatasource/datasource.go
+++ b/internal/proxydatasource/datasource.go
@@ -83,8 +83,8 @@
 		return nil, err
 	}
 	return &internal.LegacyDirectory{
+		LegacyModuleInfo: internal.LegacyModuleInfo{ModuleInfo: v.ModuleInfo},
 		Path:             dirPath,
-		LegacyModuleInfo: v.LegacyModuleInfo,
 		Packages:         v.LegacyPackages,
 	}, nil
 }
@@ -96,7 +96,7 @@
 		return nil, err
 	}
 	return &internal.VersionedDirectory{
-		LegacyModuleInfo: m.LegacyModuleInfo,
+		ModuleInfo: m.ModuleInfo,
 		DirectoryNew: internal.DirectoryNew{
 			Path:   dirPath,
 			V1Path: internal.V1Path(modulePath, strings.TrimPrefix(dirPath, modulePath+"/")),
@@ -358,8 +358,10 @@
 			// for this version's /info endpoint to get commit time, but that is
 			// deferred as a potential future enhancement.
 			vis = append(vis, &internal.LegacyModuleInfo{
-				ModulePath: modulePath,
-				Version:    vers,
+				ModuleInfo: internal.ModuleInfo{
+					ModulePath: modulePath,
+					Version:    vers,
+				},
 			})
 		}
 	}
diff --git a/internal/proxydatasource/datasource_test.go b/internal/proxydatasource/datasource_test.go
index 822ebd4..ed8b854 100644
--- a/internal/proxydatasource/datasource_test.go
+++ b/internal/proxydatasource/datasource_test.go
@@ -60,7 +60,7 @@
 		GOOS:              "linux",
 		GOARCH:            "amd64",
 	}
-	wantModuleInfo = internal.LegacyModuleInfo{
+	wantModuleInfo = internal.ModuleInfo{
 		ModulePath:        "foo.com/bar",
 		Version:           "v1.2.0",
 		CommitTime:        time.Date(2019, 1, 30, 0, 0, 0, 0, time.UTC),
@@ -69,7 +69,7 @@
 		HasGoMod:          true,
 	}
 	wantVersionedPackage = &internal.LegacyVersionedPackage{
-		LegacyModuleInfo: wantModuleInfo,
+		LegacyModuleInfo: internal.LegacyModuleInfo{ModuleInfo: wantModuleInfo},
 		LegacyPackage:    wantPackage,
 	}
 	cmpOpts = append([]cmp.Option{
@@ -82,8 +82,8 @@
 	ctx, ds, teardown := setup(t)
 	defer teardown()
 	want := &internal.LegacyDirectory{
+		LegacyModuleInfo: internal.LegacyModuleInfo{ModuleInfo: wantModuleInfo},
 		Path:             "foo.com/bar",
-		LegacyModuleInfo: wantModuleInfo,
 		Packages:         []*internal.LegacyPackage{&wantPackage},
 	}
 	got, err := ds.GetDirectory(ctx, "foo.com/bar", internal.UnknownModulePath, "v1.2.0", internal.AllFields)
@@ -127,7 +127,7 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	if diff := cmp.Diff(&wantModuleInfo, got, cmpOpts...); diff != "" {
+	if diff := cmp.Diff(wantModuleInfo, got.ModuleInfo, cmpOpts...); diff != "" {
 		t.Errorf("GetLatestModuleInfo diff (-want +got):\n%s", diff)
 	}
 }
@@ -192,7 +192,10 @@
 	}
 	v110 := wantModuleInfo
 	v110.Version = "v1.1.0"
-	want := []*internal.LegacyModuleInfo{&wantModuleInfo, &v110}
+	want := []*internal.LegacyModuleInfo{
+		{ModuleInfo: wantModuleInfo},
+		{ModuleInfo: v110},
+	}
 	ignore := cmpopts.IgnoreFields(internal.LegacyModuleInfo{}, "CommitTime", "VersionType", "IsRedistributable", "HasGoMod")
 	if diff := cmp.Diff(want, got, ignore); diff != "" {
 		t.Errorf("GetTaggedVersionsForPackageSeries diff (-want +got):\n%s", diff)
@@ -213,7 +216,10 @@
 	}
 	v110 := wantModuleInfo
 	v110.Version = "v1.1.0"
-	want := []*internal.LegacyModuleInfo{&wantModuleInfo, &v110}
+	want := []*internal.LegacyModuleInfo{
+		{ModuleInfo: wantModuleInfo},
+		{ModuleInfo: v110},
+	}
 	ignore := cmpopts.IgnoreFields(internal.LegacyModuleInfo{}, "CommitTime", "VersionType", "IsRedistributable", "HasGoMod")
 	if diff := cmp.Diff(want, got, ignore); diff != "" {
 		t.Errorf("GetTaggedVersionsForPackageSeries diff (-want +got):\n%s", diff)
@@ -227,7 +233,7 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	if diff := cmp.Diff(&wantModuleInfo, got, cmpOpts...); diff != "" {
+	if diff := cmp.Diff(wantModuleInfo, got.ModuleInfo, cmpOpts...); diff != "" {
 		t.Errorf("GetModuleInfo diff (-want +got):\n%s", diff)
 	}
 }
diff --git a/internal/testing/sample/sample.go b/internal/testing/sample/sample.go
index eb5bde7..2a32775 100644
--- a/internal/testing/sample/sample.go
+++ b/internal/testing/sample/sample.go
@@ -112,19 +112,31 @@
 	}
 	mi := ModuleInfoReleaseType(modulePath, versionString)
 	mi.VersionType = vtype
+	return &internal.LegacyModuleInfo{
+		ModuleInfo:           *mi,
+		LegacyReadmeFilePath: ReadmeFilePath,
+		LegacyReadmeContents: ReadmeContents,
+	}
+}
+
+func ModuleInfo(modulePath, versionString string) *internal.ModuleInfo {
+	vtype, err := version.ParseType(versionString)
+	if err != nil {
+		panic(err)
+	}
+	mi := ModuleInfoReleaseType(modulePath, versionString)
+	mi.VersionType = vtype
 	return mi
 }
 
 // We shouldn't need this, but some code (notably frontend/directory_test.go) creates
 // ModuleInfos with "latest" for version, which should not be valid.
-func ModuleInfoReleaseType(modulePath, versionString string) *internal.LegacyModuleInfo {
-	return &internal.LegacyModuleInfo{
-		ModulePath:           modulePath,
-		Version:              versionString,
-		LegacyReadmeFilePath: ReadmeFilePath,
-		LegacyReadmeContents: ReadmeContents,
-		CommitTime:           CommitTime,
-		VersionType:          version.TypeRelease,
+func ModuleInfoReleaseType(modulePath, versionString string) *internal.ModuleInfo {
+	return &internal.ModuleInfo{
+		ModulePath:  modulePath,
+		Version:     versionString,
+		CommitTime:  CommitTime,
+		VersionType: version.TypeRelease,
 		// Assume the module path is a GitHub-like repo name.
 		SourceInfo:        source.NewGitHubInfo("https://"+modulePath, "", versionString),
 		IsRedistributable: true,
diff --git a/internal/worker/fetch_test.go b/internal/worker/fetch_test.go
index 1802767..7c96b30 100644
--- a/internal/worker/fetch_test.go
+++ b/internal/worker/fetch_test.go
@@ -630,13 +630,15 @@
 	}
 	want := &internal.LegacyVersionedPackage{
 		LegacyModuleInfo: internal.LegacyModuleInfo{
-			ModulePath:           modulePath,
-			Version:              version,
-			CommitTime:           time.Date(2019, 1, 30, 0, 0, 0, 0, time.UTC),
-			VersionType:          "release",
-			IsRedistributable:    true,
-			HasGoMod:             false,
-			SourceInfo:           source.NewGitHubInfo("https://github.com/my/module", "", "v1.0.0"),
+			ModuleInfo: internal.ModuleInfo{
+				ModulePath:        modulePath,
+				Version:           version,
+				CommitTime:        time.Date(2019, 1, 30, 0, 0, 0, 0, time.UTC),
+				VersionType:       "release",
+				IsRedistributable: true,
+				HasGoMod:          false,
+				SourceInfo:        source.NewGitHubInfo("https://github.com/my/module", "", "v1.0.0"),
+			},
 			LegacyReadmeFilePath: "README.md",
 			LegacyReadmeContents: "This is a readme",
 		},
@@ -769,15 +771,17 @@
 
 	myModuleV100 := &internal.LegacyVersionedPackage{
 		LegacyModuleInfo: internal.LegacyModuleInfo{
-			ModulePath:           "github.com/my/module",
-			Version:              "v1.0.0",
-			CommitTime:           testProxyCommitTime,
+			ModuleInfo: internal.ModuleInfo{
+				ModulePath:        "github.com/my/module",
+				Version:           "v1.0.0",
+				CommitTime:        testProxyCommitTime,
+				SourceInfo:        source.NewGitHubInfo("https://github.com/my/module", "", "v1.0.0"),
+				VersionType:       "release",
+				IsRedistributable: true,
+				HasGoMod:          true,
+			},
 			LegacyReadmeFilePath: "README.md",
 			LegacyReadmeContents: "README FILE FOR TESTING.",
-			SourceInfo:           source.NewGitHubInfo("https://github.com/my/module", "", "v1.0.0"),
-			VersionType:          "release",
-			IsRedistributable:    true,
-			HasGoMod:             true,
 		},
 		LegacyPackage: internal.LegacyPackage{
 			Path:              "github.com/my/module/bar",
@@ -823,15 +827,17 @@
 			pkg:        "nonredistributable.mod/module/bar/baz",
 			want: &internal.LegacyVersionedPackage{
 				LegacyModuleInfo: internal.LegacyModuleInfo{
-					ModulePath:           "nonredistributable.mod/module",
-					Version:              "v1.0.0",
-					CommitTime:           testProxyCommitTime,
+					ModuleInfo: internal.ModuleInfo{
+						ModulePath:        "nonredistributable.mod/module",
+						Version:           "v1.0.0",
+						CommitTime:        testProxyCommitTime,
+						VersionType:       "release",
+						SourceInfo:        nil,
+						IsRedistributable: true,
+						HasGoMod:          true,
+					},
 					LegacyReadmeFilePath: "README.md",
 					LegacyReadmeContents: "README FILE FOR TESTING.",
-					VersionType:          "release",
-					SourceInfo:           nil,
-					IsRedistributable:    true,
-					HasGoMod:             true,
 				},
 				LegacyPackage: internal.LegacyPackage{
 					Path:              "nonredistributable.mod/module/bar/baz",
@@ -855,15 +861,17 @@
 			pkg:        "nonredistributable.mod/module/foo",
 			want: &internal.LegacyVersionedPackage{
 				LegacyModuleInfo: internal.LegacyModuleInfo{
-					ModulePath:           "nonredistributable.mod/module",
-					Version:              "v1.0.0",
-					CommitTime:           testProxyCommitTime,
+					ModuleInfo: internal.ModuleInfo{
+						ModulePath:        "nonredistributable.mod/module",
+						Version:           "v1.0.0",
+						CommitTime:        testProxyCommitTime,
+						VersionType:       "release",
+						SourceInfo:        nil,
+						IsRedistributable: true,
+						HasGoMod:          true,
+					},
 					LegacyReadmeFilePath: "README.md",
 					LegacyReadmeContents: "README FILE FOR TESTING.",
-					VersionType:          "release",
-					SourceInfo:           nil,
-					IsRedistributable:    true,
-					HasGoMod:             true,
 				},
 				LegacyPackage: internal.LegacyPackage{
 					Path:     "nonredistributable.mod/module/foo",
@@ -885,15 +893,17 @@
 			pkg:        "context",
 			want: &internal.LegacyVersionedPackage{
 				LegacyModuleInfo: internal.LegacyModuleInfo{
-					ModulePath:           "std",
-					Version:              "v1.12.5",
-					CommitTime:           stdlib.TestCommitTime,
-					VersionType:          "release",
+					ModuleInfo: internal.ModuleInfo{
+						ModulePath:        "std",
+						Version:           "v1.12.5",
+						CommitTime:        stdlib.TestCommitTime,
+						VersionType:       "release",
+						SourceInfo:        source.NewGitHubInfo(goRepositoryURLPrefix+"/go", "src", "go1.12.5"),
+						IsRedistributable: true,
+						HasGoMod:          true,
+					},
 					LegacyReadmeFilePath: "README.md",
 					LegacyReadmeContents: "# The Go Programming Language\n",
-					SourceInfo:           source.NewGitHubInfo(goRepositoryURLPrefix+"/go", "src", "go1.12.5"),
-					IsRedistributable:    true,
-					HasGoMod:             true,
 				},
 				LegacyPackage: internal.LegacyPackage{
 					Path:              "context",
@@ -918,15 +928,18 @@
 			pkg:        "builtin",
 			want: &internal.LegacyVersionedPackage{
 				LegacyModuleInfo: internal.LegacyModuleInfo{
-					ModulePath:           "std",
-					Version:              "v1.12.5",
-					CommitTime:           stdlib.TestCommitTime,
-					VersionType:          "release",
+					ModuleInfo: internal.ModuleInfo{
+						ModulePath:        "std",
+						Version:           "v1.12.5",
+						CommitTime:        stdlib.TestCommitTime,
+						VersionType:       "release",
+						SourceInfo:        source.NewGitHubInfo(goRepositoryURLPrefix+"/go", "src", "go1.12.5"),
+						IsRedistributable: true,
+						HasGoMod:          true,
+					},
+
 					LegacyReadmeFilePath: "README.md",
 					LegacyReadmeContents: "# The Go Programming Language\n",
-					SourceInfo:           source.NewGitHubInfo(goRepositoryURLPrefix+"/go", "src", "go1.12.5"),
-					IsRedistributable:    true,
-					HasGoMod:             true,
 				},
 				LegacyPackage: internal.LegacyPackage{
 					Path:              "builtin",
@@ -951,15 +964,17 @@
 			pkg:        "encoding/json",
 			want: &internal.LegacyVersionedPackage{
 				LegacyModuleInfo: internal.LegacyModuleInfo{
-					ModulePath:           "std",
-					Version:              "v1.12.5",
-					CommitTime:           stdlib.TestCommitTime,
-					VersionType:          "release",
+					ModuleInfo: internal.ModuleInfo{
+						ModulePath:        "std",
+						Version:           "v1.12.5",
+						CommitTime:        stdlib.TestCommitTime,
+						VersionType:       "release",
+						SourceInfo:        source.NewGitHubInfo(goRepositoryURLPrefix+"/go", "src", "go1.12.5"),
+						IsRedistributable: true,
+						HasGoMod:          true,
+					},
 					LegacyReadmeFilePath: "README.md",
 					LegacyReadmeContents: "# The Go Programming Language\n",
-					SourceInfo:           source.NewGitHubInfo(goRepositoryURLPrefix+"/go", "src", "go1.12.5"),
-					IsRedistributable:    true,
-					HasGoMod:             true,
 				},
 				LegacyPackage: internal.LegacyPackage{
 					Path:              "encoding/json",
@@ -997,13 +1012,15 @@
 			pkg:        "build.constraints/module/cpu",
 			want: &internal.LegacyVersionedPackage{
 				LegacyModuleInfo: internal.LegacyModuleInfo{
-					ModulePath:        "build.constraints/module",
-					Version:           "v1.0.0",
-					CommitTime:        testProxyCommitTime,
-					VersionType:       "release",
-					SourceInfo:        nil,
-					IsRedistributable: true,
-					HasGoMod:          false,
+					ModuleInfo: internal.ModuleInfo{
+						ModulePath:        "build.constraints/module",
+						Version:           "v1.0.0",
+						CommitTime:        testProxyCommitTime,
+						VersionType:       "release",
+						SourceInfo:        nil,
+						IsRedistributable: true,
+						HasGoMod:          false,
+					},
 				},
 				LegacyPackage: internal.LegacyPackage{
 					Path:              "build.constraints/module/cpu",