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}}
+
+ {{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 @@