feat: add page titles and meta descriptions

Amolith and Crush created

Add Title and Description fields to BaseData for proper
HTML title and meta description support. Update base.html
template to use these fields instead of deriving them from
Repo and ServerName.

Implement page-specific titles:
- About: "About | {server name}"
- Home: "{server name}"
- Overview: "{project name or repo}" with description
- Commits: "Commits in {ref} | {project or repo}"
- Commit: "{subject} | Commit {hash} | {project or repo}"
- Tags: "Tags | {project or repo}"
- Branches: "Branches | {project or repo}"
- Files: "Files | {project or repo}"
- Blob: "{filename} | {project or repo}"

Fix shortHash template function to use 7 characters
instead of 8 for consistency with git standards.

Implements: bug-972b30b
Implements: bug-7778d23
Co-Authored-By: Crush <crush@charm.land>

Change summary

pkg/web/templates/base.html |  4 ++--
pkg/web/webui.go            | 10 ++++++----
pkg/web/webui_about.go      |  1 +
pkg/web/webui_blob.go       |  7 +++++++
pkg/web/webui_branches.go   |  6 ++++++
pkg/web/webui_commit.go     | 29 +++++++++++++++++++++++++++--
pkg/web/webui_commits.go    |  6 ++++++
pkg/web/webui_home.go       |  1 +
pkg/web/webui_overview.go   | 11 +++++++++--
pkg/web/webui_tags.go       |  6 ++++++
pkg/web/webui_tree.go       |  6 ++++++
11 files changed, 77 insertions(+), 10 deletions(-)

Detailed changes

pkg/web/templates/base.html 🔗

@@ -5,8 +5,8 @@
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <meta name="color-scheme" content="light dark">
-  <title>{{if .Repo}}{{if .Repo.ProjectName}}{{.Repo.ProjectName}}{{else}}{{.Repo.Name}}{{end}}{{else}}{{.ServerName}}{{end}}</title>
-  {{if .Repo}}{{if .Repo.Description}}<meta name="description" content="{{.Repo.Description}}">{{end}}{{end}}
+  <title>{{.Title}}</title>
+  {{if .Description}}<meta name="description" content="{{.Description}}">{{end}}
   <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍦</text></svg>">
   <link rel="stylesheet" href="/static/pico-2.1.1-pink.min.css">
   <link rel="stylesheet" href="/static/overrides.css?v=1">

pkg/web/webui.go 🔗

@@ -39,8 +39,10 @@ const (
 
 // BaseData contains common fields for all web UI pages.
 type BaseData struct {
-	ServerName string
-	ActiveTab  string
+	ServerName  string
+	ActiveTab   string
+	Title       string
+	Description string
 }
 
 // RepoBaseData contains common fields for repository-specific pages.
@@ -103,8 +105,8 @@ var templateFuncs = template.FuncMap{
 		default:
 			hashStr = fmt.Sprintf("%v", hash)
 		}
-		if len(hashStr) > 8 {
-			return hashStr[:8]
+		if len(hashStr) > 7 {
+			return hashStr[:7]
 		}
 		return hashStr
 	},

pkg/web/webui_about.go 🔗

@@ -37,6 +37,7 @@ func about(w http.ResponseWriter, r *http.Request) {
 		BaseData: BaseData{
 			ServerName: cfg.Name,
 			ActiveTab:  "about",
+			Title:      "About | " + cfg.Name,
 		},
 		ReadmeHTML: readmeHTML,
 	}

pkg/web/webui_blob.go 🔗

@@ -99,11 +99,18 @@ func repoBlob(w http.ResponseWriter, r *http.Request) {
 		renderedHTML = highlightCode(path, content)
 	}
 
+	repoDisplayName := repo.ProjectName()
+	if repoDisplayName == "" {
+		repoDisplayName = repo.Name()
+	}
+	fileName := filepath.Base(path)
+
 	data := BlobData{
 		RepoBaseData: RepoBaseData{
 			BaseData: BaseData{
 				ServerName: cfg.Name,
 				ActiveTab:  "tree",
+				Title:      fileName + " | " + repoDisplayName,
 			},
 			Repo:          repo,
 			DefaultBranch: defaultBranch,

pkg/web/webui_branches.go 🔗

@@ -88,11 +88,17 @@ func repoBranches(w http.ResponseWriter, r *http.Request) {
 		})
 	}
 
+	repoDisplayName := repo.ProjectName()
+	if repoDisplayName == "" {
+		repoDisplayName = repo.Name()
+	}
+
 	data := BranchesData{
 		RepoBaseData: RepoBaseData{
 			BaseData: BaseData{
 				ServerName: cfg.Name,
 				ActiveTab:  "branches",
+				Title:      "Branches | " + repoDisplayName,
 			},
 			Repo:          repo,
 			DefaultBranch: defaultBranch,

pkg/web/webui_commit.go 🔗

@@ -2,6 +2,7 @@ package web
 
 import (
 	"net/http"
+	"strings"
 
 	"github.com/charmbracelet/log/v2"
 	"github.com/charmbracelet/soft-serve/git"
@@ -57,11 +58,35 @@ func repoCommit(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
+	commitSubject := commit.Message
+	commitBody := ""
+	if lines := strings.Split(commit.Message, "\n"); len(lines) > 0 {
+		commitSubject = lines[0]
+		if len(lines) > 1 {
+			commitBody = strings.TrimSpace(strings.Join(lines[1:], "\n"))
+			if len(commitBody) > 200 {
+				commitBody = commitBody[:200] + "..."
+			}
+		}
+	}
+
+	repoDisplayName := repo.ProjectName()
+	if repoDisplayName == "" {
+		repoDisplayName = repo.Name()
+	}
+
+	shortHash := hash
+	if len(hash) > 7 {
+		shortHash = hash[:7]
+	}
+
 	data := CommitData{
 		RepoBaseData: RepoBaseData{
 			BaseData: BaseData{
-				ServerName: cfg.Name,
-				ActiveTab:  "commits",
+				ServerName:  cfg.Name,
+				ActiveTab:   "commits",
+				Title:       commitSubject + " | Commit " + shortHash + " | " + repoDisplayName,
+				Description: commitBody,
 			},
 			Repo:          repo,
 			DefaultBranch: defaultBranch,

pkg/web/webui_commits.go 🔗

@@ -82,11 +82,17 @@ func repoCommits(w http.ResponseWriter, r *http.Request) {
 
 	defaultBranch := getDefaultBranch(gr)
 
+	repoDisplayName := repo.ProjectName()
+	if repoDisplayName == "" {
+		repoDisplayName = repo.Name()
+	}
+
 	data := CommitsData{
 		RepoBaseData: RepoBaseData{
 			BaseData: BaseData{
 				ServerName: cfg.Name,
 				ActiveTab:  "commits",
+				Title:      "Commits in " + ref + " | " + repoDisplayName,
 			},
 			Repo:          repo,
 			DefaultBranch: defaultBranch,

pkg/web/webui_home.go 🔗

@@ -154,6 +154,7 @@ func home(w http.ResponseWriter, r *http.Request) {
 		BaseData: BaseData{
 			ServerName: cfg.Name,
 			ActiveTab:  "repositories",
+			Title:      cfg.Name,
 		},
 		PaginationData: PaginationData{
 			Page:        page,

pkg/web/webui_overview.go 🔗

@@ -50,11 +50,18 @@ func repoOverview(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
+	repoDisplayName := repo.ProjectName()
+	if repoDisplayName == "" {
+		repoDisplayName = repo.Name()
+	}
+
 	data := OverviewData{
 		RepoBaseData: RepoBaseData{
 			BaseData: BaseData{
-				ServerName: cfg.Name,
-				ActiveTab:  "overview",
+				ServerName:  cfg.Name,
+				ActiveTab:   "overview",
+				Title:       repoDisplayName,
+				Description: repo.Description(),
 			},
 			Repo:          repo,
 			DefaultBranch: defaultBranch,

pkg/web/webui_tags.go 🔗

@@ -108,11 +108,17 @@ func repoTags(w http.ResponseWriter, r *http.Request) {
 		})
 	}
 
+	repoDisplayName := repo.ProjectName()
+	if repoDisplayName == "" {
+		repoDisplayName = repo.Name()
+	}
+
 	data := TagsData{
 		RepoBaseData: RepoBaseData{
 			BaseData: BaseData{
 				ServerName: cfg.Name,
 				ActiveTab:  "tags",
+				Title:      "Tags | " + repoDisplayName,
 			},
 			Repo:          repo,
 			DefaultBranch: defaultBranch,

pkg/web/webui_tree.go 🔗

@@ -72,11 +72,17 @@ func repoTree(w http.ResponseWriter, r *http.Request) {
 
 	defaultBranch := getDefaultBranch(gr)
 
+	repoDisplayName := repo.ProjectName()
+	if repoDisplayName == "" {
+		repoDisplayName = repo.Name()
+	}
+
 	data := TreeData{
 		RepoBaseData: RepoBaseData{
 			BaseData: BaseData{
 				ServerName: cfg.Name,
 				ActiveTab:  "tree",
+				Title:      "Files | " + repoDisplayName,
 			},
 			Repo:          repo,
 			DefaultBranch: defaultBranch,