diff --git a/cmd/server/main.go b/cmd/server/main.go index 5fc42a0..fd8b68e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -62,6 +62,7 @@ func main() { cfg.SEO.SiteName, cfg.SEO.SiteURL, cfg.SEO.DefaultImage, + nostrClient, ) if err != nil { log.Fatalf("Failed to initialize HTML generator: %v", err) diff --git a/internal/generator/html.go b/internal/generator/html.go index 908076b..61e30dc 100644 --- a/internal/generator/html.go +++ b/internal/generator/html.go @@ -2,10 +2,12 @@ package generator import ( "bytes" + "context" "encoding/json" "fmt" "html/template" "path/filepath" + "sync" "time" "gitcitadel-online/internal/asciidoc" @@ -41,6 +43,7 @@ type HTMLGenerator struct { siteName string siteURL string defaultImage string + nostrClient *nostr.Client } // PageData represents data for a wiki page @@ -63,6 +66,7 @@ type PageData struct { Content template.HTML Summary string TableOfContents template.HTML + Profiles map[string]*nostr.Profile // Map of pubkey -> profile } // WikiPageInfo represents info about a wiki page for navigations @@ -116,7 +120,7 @@ type UserBadgeInfo struct { } // NewHTMLGenerator creates a new HTML generator -func NewHTMLGenerator(templateDir string, linkBaseURL, siteName, siteURL, defaultImage string) (*HTMLGenerator, error) { +func NewHTMLGenerator(templateDir string, linkBaseURL, siteName, siteURL, defaultImage string, nostrClient *nostr.Client) (*HTMLGenerator, error) { tmpl := template.New("base").Funcs(getTemplateFuncs()) // Load all templates @@ -148,9 +152,49 @@ func NewHTMLGenerator(templateDir string, linkBaseURL, siteName, siteURL, defaul siteName: siteName, siteURL: siteURL, defaultImage: defaultImage, + nostrClient: nostrClient, }, nil } +// fetchProfilesBatch fetches profiles for multiple pubkeys in parallel +func (g *HTMLGenerator) fetchProfilesBatch(ctx context.Context, pubkeys []string) map[string]*nostr.Profile { + if g.nostrClient == nil || len(pubkeys) == 0 { + return make(map[string]*nostr.Profile) + } + + profiles := make(map[string]*nostr.Profile) + var mu sync.Mutex + var wg sync.WaitGroup + + // Create a context with timeout for profile fetching + profileCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + for _, pubkey := range pubkeys { + if pubkey == "" { + continue + } + wg.Add(1) + go func(pk string) { + defer wg.Done() + // Convert pubkey to npub for fetching + npub, err := nostr.PubkeyToNpub(pk) + if err != nil { + return + } + profile, err := g.nostrClient.FetchProfile(profileCtx, npub) + if err == nil && profile != nil { + mu.Lock() + profiles[pk] = profile + mu.Unlock() + } + }(pubkey) + } + + wg.Wait() + return profiles +} + // ProcessAsciiDoc processes AsciiDoc content to HTML func (g *HTMLGenerator) ProcessAsciiDoc(content string) (string, error) { return g.asciidocProc.Process(content) @@ -158,6 +202,18 @@ func (g *HTMLGenerator) ProcessAsciiDoc(content string) (string, error) { // GenerateLandingPage generates the static landing page func (g *HTMLGenerator) GenerateLandingPage(wikiPages []WikiPageInfo, feedItems []FeedItemInfo) (string, error) { + // Collect pubkeys from feed items + pubkeys := make([]string, 0, len(feedItems)) + for _, item := range feedItems { + if item.Author != "" { + pubkeys = append(pubkeys, item.Author) + } + } + + // Fetch profiles for feed authors + ctx := context.Background() + profiles := g.fetchProfilesBatch(ctx, pubkeys) + data := PageData{ Title: "Home", Description: "Welcome to " + g.siteName, @@ -169,6 +225,7 @@ func (g *HTMLGenerator) GenerateLandingPage(wikiPages []WikiPageInfo, feedItems CurrentYear: time.Now().Year(), WikiPages: wikiPages, FeedItems: feedItems, + Profiles: profiles, } return g.renderTemplate("landing.html", data) @@ -202,6 +259,7 @@ func (g *HTMLGenerator) GenerateWikiPage(wiki *nostr.WikiEvent, wikiPages []Wiki FeedItems: []FeedItemInfo{}, // Empty - feed only on landing page Content: template.HTML(htmlContent), Summary: wiki.Summary, + Profiles: make(map[string]*nostr.Profile), // Empty profiles for wiki pages } // Add structured data for article @@ -219,8 +277,12 @@ func (g *HTMLGenerator) GenerateBlogPage(blogIndex *nostr.IndexEvent, blogItems canonicalURL := g.siteURL + "/blog" - // Format times for blog items + // Format times for blog items and collect pubkeys formattedBlogItems := make([]BlogItemInfo, len(blogItems)) + pubkeys := make([]string, 0, len(blogItems)+1) + if blogIndex.Author != "" { + pubkeys = append(pubkeys, blogIndex.Author) + } for i, item := range blogItems { formattedBlogItems[i] = item if item.CreatedAt > 0 { @@ -228,8 +290,15 @@ func (g *HTMLGenerator) GenerateBlogPage(blogIndex *nostr.IndexEvent, blogItems formattedBlogItems[i].Time = createdTime.Format("Jan 2, 2006") formattedBlogItems[i].TimeISO = createdTime.Format(time.RFC3339) } + if item.Author != "" { + pubkeys = append(pubkeys, item.Author) + } } + // Fetch profiles for blog authors + ctx := context.Background() + profiles := g.fetchProfilesBatch(ctx, pubkeys) + data := PageData{ Title: "Blog", Description: description, @@ -243,6 +312,7 @@ func (g *HTMLGenerator) GenerateBlogPage(blogIndex *nostr.IndexEvent, blogItems BlogSummary: blogIndex.Summary, FeedItems: []FeedItemInfo{}, // Empty - feed only on landing page Content: template.HTML(""), + Profiles: profiles, } // Add blog index metadata to template data using a map @@ -321,13 +391,21 @@ func (g *HTMLGenerator) GenerateEBooksPage(ebooks []EBookInfo, feedItems []FeedI // Format times for display formattedEBooks := make([]EBookInfo, len(ebooks)) + pubkeys := make([]string, 0, len(ebooks)) for i, ebook := range ebooks { formattedEBooks[i] = ebook createdTime := time.Unix(ebook.CreatedAt, 0) formattedEBooks[i].Time = createdTime.Format("2006-01-02 15:04:05") formattedEBooks[i].TimeISO = createdTime.Format(time.RFC3339) + if ebook.Author != "" { + pubkeys = append(pubkeys, ebook.Author) + } } + // Fetch profiles for all authors + ctx := context.Background() + profiles := g.fetchProfilesBatch(ctx, pubkeys) + data := PageData{ Title: "E-Books", Description: "Browse top-level publications (index events) from Nostr", @@ -341,6 +419,7 @@ func (g *HTMLGenerator) GenerateEBooksPage(ebooks []EBookInfo, feedItems []FeedI FeedItems: []FeedItemInfo{}, // Empty - feed only on landing page EBooks: formattedEBooks, Content: template.HTML(""), // Content comes from template + Profiles: profiles, } return g.renderTemplate("ebooks.html", data) @@ -383,8 +462,9 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event SiteName: g.siteName, SiteURL: g.siteURL, CurrentYear: time.Now().Year(), - WikiPages: []WikiPageInfo{}, // Will be populated if needed - FeedItems: []FeedItemInfo{}, // Empty - feed only on landing page + WikiPages: []WikiPageInfo{}, // Will be populated if needed + FeedItems: []FeedItemInfo{}, // Empty - feed only on landing page + Profiles: make(map[string]*nostr.Profile), // Empty profiles for contact page }, Success: success, Error: errorMsg, @@ -428,6 +508,7 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event "Error": data.Error, "EventID": data.EventID, "FormData": data.FormData, + "Profiles": make(map[string]*nostr.Profile), // Empty profiles for contact page } // Add repo announcement data for JavaScript diff --git a/static/css/main.css b/static/css/main.css index dbb1709..c332017 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -43,18 +43,19 @@ body { min-height: 100vh; display: flex; flex-direction: column; + padding-top: 80px; /* Space for fixed header */ } /* Skip link for accessibility */ .skip-link { - position: absolute; + position: fixed; top: -40px; left: 0; background: var(--accent-color); color: var(--bg-primary); padding: 8px; text-decoration: none; - z-index: 100; + z-index: 1001; /* Above fixed header */ } .skip-link:focus { @@ -65,9 +66,12 @@ body { header { background-color: var(--bg-secondary); border-bottom: 1px solid var(--border-color); - position: sticky; + position: fixed; top: 0; - z-index: 50; + left: 0; + right: 0; + width: 100%; + z-index: 1000; } .navbar { diff --git a/templates/components.html b/templates/components.html index 5ba7ba2..18b1f8e 100644 --- a/templates/components.html +++ b/templates/components.html @@ -76,10 +76,27 @@ {{end}} -{{/* Simple user badge - just takes a pubkey string */}} +{{/* Simple user badge - takes a pubkey string and looks up profile from .Profiles map */}} {{define "user-badge-simple"}} - +{{$pubkey := .}} +{{$profile := index $.Profiles $pubkey}} + + {{if and $profile $profile.Picture}} + {{if $profile.DisplayName}}{{$profile.DisplayName}}{{else if $profile.Name}}{{$profile.Name}}{{else}}User{{end}} + {{else}} - {{shortenPubkey .}} + {{end}} + + {{if and $profile $profile.DisplayName}} + {{$profile.DisplayName}} + {{else if and $profile $profile.Name}} + {{$profile.Name}} + {{else}} + {{shortenPubkey $pubkey}} + {{end}} + + {{if and $profile $profile.Name (not $profile.DisplayName)}} + @{{$profile.Name}} + {{end}} {{end}} diff --git a/templates/ebooks.html b/templates/ebooks.html index 6b50cae..4cf0706 100644 --- a/templates/ebooks.html +++ b/templates/ebooks.html @@ -29,7 +29,7 @@ - View + View {{else}}