@ -2,10 +2,12 @@ package generator
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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
@@ -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) {
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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