Detailed changes
@@ -28,18 +28,25 @@ func about(w http.ResponseWriter, r *http.Request) {
return
}
- readmeHTML, err := getServerReadme(ctx, be)
+ readmeRaw, readmeHTML, err := getServerReadme(ctx, be)
if err != nil {
logger.Debug("failed to get server README", "err", err)
renderInternalServerError(w, r)
return
}
+ // Generate description from README or fallback
+ description := extractPlainTextFromMarkdown(readmeRaw, 200)
+ if description == "" {
+ description = "About " + cfg.Name
+ }
+
data := AboutData{
BaseData: BaseData{
- ServerName: cfg.Name,
- ActiveTab: "about",
- Title: "About | " + cfg.Name,
+ ServerName: cfg.Name,
+ ActiveTab: "about",
+ Title: "About | " + cfg.Name,
+ Description: description,
},
ReadmeHTML: readmeHTML,
}
@@ -105,12 +105,15 @@ func repoBlob(w http.ResponseWriter, r *http.Request) {
}
fileName := filepath.Base(path)
+ description := getRepoDescriptionOrFallback(repo, "View "+fileName+" in "+repoDisplayName)
+
data := BlobData{
RepoBaseData: RepoBaseData{
BaseData: BaseData{
- ServerName: cfg.Name,
- ActiveTab: "tree",
- Title: fileName + " | " + repoDisplayName,
+ ServerName: cfg.Name,
+ ActiveTab: "tree",
+ Title: fileName + " | " + repoDisplayName,
+ Description: description,
},
Repo: repo,
DefaultBranch: defaultBranch,
@@ -93,12 +93,15 @@ func repoBranches(w http.ResponseWriter, r *http.Request) {
repoDisplayName = repo.Name()
}
+ description := getRepoDescriptionOrFallback(repo, "Branches in "+repoDisplayName)
+
data := BranchesData{
RepoBaseData: RepoBaseData{
BaseData: BaseData{
- ServerName: cfg.Name,
- ActiveTab: "branches",
- Title: "Branches | " + repoDisplayName,
+ ServerName: cfg.Name,
+ ActiveTab: "branches",
+ Title: "Branches | " + repoDisplayName,
+ Description: description,
},
Repo: repo,
DefaultBranch: defaultBranch,
@@ -87,12 +87,15 @@ func repoCommits(w http.ResponseWriter, r *http.Request) {
repoDisplayName = repo.Name()
}
+ description := getRepoDescriptionOrFallback(repo, "Commit log for "+ref+" in "+repoDisplayName)
+
data := CommitsData{
RepoBaseData: RepoBaseData{
BaseData: BaseData{
- ServerName: cfg.Name,
- ActiveTab: "commits",
- Title: "Commits in " + ref + " | " + repoDisplayName,
+ ServerName: cfg.Name,
+ ActiveTab: "commits",
+ Title: "Commits in " + ref + " | " + repoDisplayName,
+ Description: description,
},
Repo: repo,
DefaultBranch: defaultBranch,
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"html/template"
+ "strings"
"github.com/charmbracelet/soft-serve/git"
"github.com/charmbracelet/soft-serve/pkg/backend"
@@ -42,25 +43,30 @@ func renderMarkdown(content []byte) (template.HTML, error) {
}
// getServerReadme loads and renders the README from the .soft-serve repository.
-func getServerReadme(ctx context.Context, be *backend.Backend) (template.HTML, error) {
+// Returns both the raw markdown content and the rendered HTML.
+func getServerReadme(ctx context.Context, be *backend.Backend) (raw string, html template.HTML, err error) {
repos, err := be.Repositories(ctx)
if err != nil {
- return "", err
+ return "", "", err
}
for _, r := range repos {
if r.Name() == ".soft-serve" {
readme, _, err := backend.Readme(r, nil)
if err != nil {
- return "", err
+ return "", "", err
}
if readme != "" {
- return renderMarkdown([]byte(readme))
+ html, err := renderMarkdown([]byte(readme))
+ if err != nil {
+ return "", "", err
+ }
+ return readme, html, nil
}
}
}
- return "", nil
+ return "", "", nil
}
// openRepository opens a git repository.
@@ -76,3 +82,83 @@ func getDefaultBranch(gr *git.Repository) string {
}
return head.Name().Short()
}
+
+// truncateText truncates text to maxLength characters, respecting word boundaries.
+// If truncated, appends "...". Returns empty string if text is empty or maxLength <= 0.
+func truncateText(text string, maxLength int) string {
+ text = strings.TrimSpace(text)
+ if text == "" || maxLength <= 0 {
+ return ""
+ }
+
+ if len(text) <= maxLength {
+ return text
+ }
+
+ // Find last space before maxLength
+ truncated := text[:maxLength]
+ if lastSpace := strings.LastIndex(truncated, " "); lastSpace > 0 {
+ truncated = truncated[:lastSpace]
+ }
+
+ return truncated + "..."
+}
+
+// extractPlainTextFromMarkdown converts markdown to plain text and truncates to maxLength.
+// Strips markdown formatting to produce a clean description suitable for meta tags.
+func extractPlainTextFromMarkdown(markdown string, maxLength int) string {
+ markdown = strings.TrimSpace(markdown)
+ if markdown == "" {
+ return ""
+ }
+
+ // Simple markdown stripping - remove common formatting
+ text := markdown
+
+ // Remove headers
+ text = strings.ReplaceAll(text, "# ", "")
+ text = strings.ReplaceAll(text, "## ", "")
+ text = strings.ReplaceAll(text, "### ", "")
+ text = strings.ReplaceAll(text, "#### ", "")
+ text = strings.ReplaceAll(text, "##### ", "")
+ text = strings.ReplaceAll(text, "###### ", "")
+
+ // Remove bold/italic markers
+ text = strings.ReplaceAll(text, "**", "")
+ text = strings.ReplaceAll(text, "__", "")
+ text = strings.ReplaceAll(text, "*", "")
+ text = strings.ReplaceAll(text, "_", "")
+
+ // Remove code blocks and inline code
+ text = strings.ReplaceAll(text, "`", "")
+
+ // Remove links but keep text: [text](url) -> text
+ for strings.Contains(text, "[") && strings.Contains(text, "](") {
+ start := strings.Index(text, "[")
+ mid := strings.Index(text[start:], "](")
+ if mid == -1 {
+ break
+ }
+ end := strings.Index(text[start+mid+2:], ")")
+ if end == -1 {
+ break
+ }
+ linkText := text[start+1 : start+mid]
+ text = text[:start] + linkText + text[start+mid+2+end+1:]
+ }
+
+ // Replace multiple spaces/newlines with single space
+ text = strings.Join(strings.Fields(text), " ")
+
+ return truncateText(text, maxLength)
+}
+
+// getRepoDescriptionOrFallback returns the repository description if available,
+// otherwise returns the fallback text. Result is truncated to 200 characters.
+func getRepoDescriptionOrFallback(repo proto.Repository, fallback string) string {
+ desc := strings.TrimSpace(repo.Description())
+ if desc == "" {
+ desc = fallback
+ }
+ return truncateText(desc, 200)
+}
@@ -59,10 +59,11 @@ func home(w http.ResponseWriter, r *http.Request) {
}
var readmeHTML template.HTML
+ var readmeRaw string
homeRepos := make([]HomeRepository, 0)
items := make([]repoItem, 0)
- readmeHTML, err = getServerReadme(ctx, be)
+ readmeRaw, readmeHTML, err = getServerReadme(ctx, be)
if err != nil {
logger.Debug("failed to get server README", "err", err)
}
@@ -149,11 +150,18 @@ func home(w http.ResponseWriter, r *http.Request) {
})
}
+ // Generate description from README or fallback
+ description := extractPlainTextFromMarkdown(readmeRaw, 200)
+ if description == "" {
+ description = "Git repositories on " + cfg.Name
+ }
+
data := HomeData{
BaseData: BaseData{
- ServerName: cfg.Name,
- ActiveTab: "repositories",
- Title: cfg.Name,
+ ServerName: cfg.Name,
+ ActiveTab: "repositories",
+ Title: cfg.Name,
+ Description: description,
},
PaginationData: PaginationData{
Page: page,
@@ -113,12 +113,15 @@ func repoTags(w http.ResponseWriter, r *http.Request) {
repoDisplayName = repo.Name()
}
+ description := getRepoDescriptionOrFallback(repo, "Tags in "+repoDisplayName)
+
data := TagsData{
RepoBaseData: RepoBaseData{
BaseData: BaseData{
- ServerName: cfg.Name,
- ActiveTab: "tags",
- Title: "Tags | " + repoDisplayName,
+ ServerName: cfg.Name,
+ ActiveTab: "tags",
+ Title: "Tags | " + repoDisplayName,
+ Description: description,
},
Repo: repo,
DefaultBranch: defaultBranch,
@@ -89,12 +89,27 @@ func repoTree(w http.ResponseWriter, r *http.Request) {
}
title += " | " + repoDisplayName
+ // Build fallback description matching title information
+ descFallback := "Browse "
+ if path != "." {
+ descFallback += path + " at "
+ }
+ if isCommitHash && len(ref) > 7 {
+ descFallback += ref[:7]
+ } else {
+ descFallback += ref
+ }
+ descFallback += " in " + repoDisplayName
+
+ description := getRepoDescriptionOrFallback(repo, descFallback)
+
data := TreeData{
RepoBaseData: RepoBaseData{
BaseData: BaseData{
- ServerName: cfg.Name,
- ActiveTab: "tree",
- Title: title,
+ ServerName: cfg.Name,
+ ActiveTab: "tree",
+ Title: title,
+ Description: description,
},
Repo: repo,
DefaultBranch: defaultBranch,