@ -2,6 +2,7 @@ package generator
@@ -2,6 +2,7 @@ package generator
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"path/filepath"
@ -14,6 +15,7 @@ import (
@@ -14,6 +15,7 @@ import (
// HTMLGenerator generates HTML pages from wiki events
type HTMLGenerator struct {
templates * template . Template
templateDir string
asciidocProc * asciidoc . Processor
linkBaseURL string
siteName string
@ -31,10 +33,13 @@ type PageData struct {
@@ -31,10 +33,13 @@ type PageData struct {
OGType string
StructuredData template . JS
SiteName string
SiteURL string
CurrentYear int
WikiPages [ ] WikiPageInfo
BlogItems [ ] BlogItemInfo
BlogSummary string
FeedItems [ ] FeedItemInfo
EBooks [ ] EBookInfo
Content template . HTML
Summary string
TableOfContents template . HTML
@ -52,6 +57,10 @@ type BlogItemInfo struct {
@@ -52,6 +57,10 @@ type BlogItemInfo struct {
Title string
Summary string
Content template . HTML
Author string
CreatedAt int64
Time string // Formatted time
TimeISO string // ISO time
}
// FeedItemInfo represents info about a feed item
@ -63,19 +72,42 @@ type FeedItemInfo struct {
@@ -63,19 +72,42 @@ type FeedItemInfo struct {
Link string
}
// EBookInfo represents info about an e-book (top-level index event)
type EBookInfo struct {
EventID string
Title string
DTag string
Author string
Summary string
Type string
CreatedAt int64
Naddr string
Time string // Formatted time
TimeISO string // ISO time
}
// NewHTMLGenerator creates a new HTML generator
func NewHTMLGenerator ( templateDir string , linkBaseURL , siteName , siteURL , defaultImage string ) ( * HTMLGenerator , error ) {
tmpl := template . New ( "base" ) . Funcs ( template . FuncMap {
"year" : func ( ) int { return time . Now ( ) . Year ( ) } ,
"json" : func ( v interface { } ) ( string , error ) {
b , err := json . Marshal ( v )
if err != nil {
return "" , err
}
return string ( b ) , nil
} ,
} )
// Load all templates
templateFiles := [ ] string {
"components.html" , // Reusable components (feed, alerts, etc.)
"base.html" ,
"landing.html" ,
"page.html" ,
"blog.html" ,
"feed_sidebar.html" ,
"wiki.html" ,
"contact.html" ,
"404.html" ,
"500.html" ,
}
@ -90,6 +122,7 @@ func NewHTMLGenerator(templateDir string, linkBaseURL, siteName, siteURL, defaul
@@ -90,6 +122,7 @@ func NewHTMLGenerator(templateDir string, linkBaseURL, siteName, siteURL, defaul
return & HTMLGenerator {
templates : tmpl ,
templateDir : templateDir ,
asciidocProc : asciidoc . NewProcessor ( linkBaseURL ) ,
linkBaseURL : linkBaseURL ,
siteName : siteName ,
@ -112,6 +145,7 @@ func (g *HTMLGenerator) GenerateLandingPage(wikiPages []WikiPageInfo, feedItems
@@ -112,6 +145,7 @@ func (g *HTMLGenerator) GenerateLandingPage(wikiPages []WikiPageInfo, feedItems
OGImage : g . siteURL + g . defaultImage ,
OGType : "website" ,
SiteName : g . siteName ,
SiteURL : g . siteURL ,
CurrentYear : time . Now ( ) . Year ( ) ,
WikiPages : wikiPages ,
FeedItems : feedItems ,
@ -142,6 +176,7 @@ func (g *HTMLGenerator) GenerateWikiPage(wiki *nostr.WikiEvent, wikiPages []Wiki
@@ -142,6 +176,7 @@ func (g *HTMLGenerator) GenerateWikiPage(wiki *nostr.WikiEvent, wikiPages []Wiki
OGImage : g . siteURL + g . defaultImage ,
OGType : "article" ,
SiteName : g . siteName ,
SiteURL : g . siteURL ,
CurrentYear : time . Now ( ) . Year ( ) ,
WikiPages : wikiPages ,
FeedItems : feedItems ,
@ -164,6 +199,17 @@ func (g *HTMLGenerator) GenerateBlogPage(blogIndex *nostr.IndexEvent, blogItems
@@ -164,6 +199,17 @@ func (g *HTMLGenerator) GenerateBlogPage(blogIndex *nostr.IndexEvent, blogItems
canonicalURL := g . siteURL + "/blog"
// Format times for blog items
formattedBlogItems := make ( [ ] BlogItemInfo , len ( blogItems ) )
for i , item := range blogItems {
formattedBlogItems [ i ] = item
if item . CreatedAt > 0 {
createdTime := time . Unix ( item . CreatedAt , 0 )
formattedBlogItems [ i ] . Time = createdTime . Format ( "Jan 2, 2006" )
formattedBlogItems [ i ] . TimeISO = createdTime . Format ( time . RFC3339 )
}
}
data := PageData {
Title : "Blog" ,
Description : description ,
@ -171,16 +217,126 @@ func (g *HTMLGenerator) GenerateBlogPage(blogIndex *nostr.IndexEvent, blogItems
@@ -171,16 +217,126 @@ func (g *HTMLGenerator) GenerateBlogPage(blogIndex *nostr.IndexEvent, blogItems
OGImage : g . siteURL + g . defaultImage ,
OGType : "website" ,
SiteName : g . siteName ,
SiteURL : g . siteURL ,
CurrentYear : time . Now ( ) . Year ( ) ,
BlogItems : blogItems ,
BlogItems : formattedBlogItems ,
BlogSummary : blogIndex . Summary ,
FeedItems : feedItems ,
Content : template . HTML ( "" ) ,
}
// Add blog index metadata to template data using a map
type BlogPageData struct {
PageData
BlogIndexTitle string
BlogIndexAuthor string
BlogIndexImage string
BlogIndexSummary string
}
blogData := BlogPageData {
PageData : data ,
BlogIndexTitle : blogIndex . Title ,
BlogIndexAuthor : blogIndex . Author ,
BlogIndexImage : blogIndex . Image ,
BlogIndexSummary : blogIndex . Summary ,
}
// Use renderTemplate but with custom data
renderTmpl := template . New ( "blog-render" ) . Funcs ( template . FuncMap {
"year" : func ( ) int { return time . Now ( ) . Year ( ) } ,
"json" : func ( v interface { } ) ( string , error ) {
b , err := json . Marshal ( v )
if err != nil {
return "" , err
}
return string ( b ) , nil
} ,
} )
files := [ ] string {
filepath . Join ( g . templateDir , "components.html" ) ,
filepath . Join ( g . templateDir , "base.html" ) ,
filepath . Join ( g . templateDir , "blog.html" ) ,
}
return g . renderTemplate ( "blog.html" , data )
_ , err := renderTmpl . ParseFiles ( files ... )
if err != nil {
return "" , fmt . Errorf ( "failed to parse blog templates: %w" , err )
}
var buf bytes . Buffer
if err := renderTmpl . ExecuteTemplate ( & buf , "base.html" , blogData ) ; err != nil {
return "" , fmt . Errorf ( "failed to execute blog template: %w" , err )
}
return buf . String ( ) , nil
}
// GenerateWikiIndexPage generates the wiki index page
func ( g * HTMLGenerator ) GenerateWikiIndexPage ( wikiIndex * nostr . IndexEvent , wikiPages [ ] WikiPageInfo , feedItems [ ] FeedItemInfo ) ( string , error ) {
description := "Wiki documentation from " + g . siteName
summary := ""
if wikiIndex != nil {
if wikiIndex . Summary != "" {
description = wikiIndex . Summary
summary = wikiIndex . Summary
}
}
canonicalURL := g . siteURL + "/wiki"
data := PageData {
Title : "Wiki" ,
Description : description ,
CanonicalURL : canonicalURL ,
OGImage : g . siteURL + g . defaultImage ,
OGType : "website" ,
SiteName : g . siteName ,
SiteURL : g . siteURL ,
CurrentYear : time . Now ( ) . Year ( ) ,
WikiPages : wikiPages ,
FeedItems : feedItems ,
Summary : summary ,
}
return g . renderTemplate ( "wiki.html" , data )
}
// GenerateEBooksPage generates the e-books listing page
func ( g * HTMLGenerator ) GenerateEBooksPage ( ebooks [ ] EBookInfo , feedItems [ ] FeedItemInfo ) ( string , error ) {
canonicalURL := g . siteURL + "/ebooks"
// Format times for display
formattedEBooks := make ( [ ] EBookInfo , 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 )
}
data := PageData {
Title : "E-Books" ,
Description : "Browse top-level publications (index events) from Nostr" ,
CanonicalURL : canonicalURL ,
OGImage : g . siteURL + g . defaultImage ,
OGType : "website" ,
SiteName : g . siteName ,
SiteURL : g . siteURL ,
CurrentYear : time . Now ( ) . Year ( ) ,
WikiPages : [ ] WikiPageInfo { } ,
FeedItems : feedItems ,
EBooks : formattedEBooks ,
Content : template . HTML ( "" ) , // Content comes from template
}
return g . renderTemplate ( "ebooks.html" , data )
}
// GenerateContactPage generates the contact form page
func ( g * HTMLGenerator ) GenerateContactPage ( success bool , errorMsg string , eventID string , formData map [ string ] string ) ( string , error ) {
func ( g * HTMLGenerator ) GenerateContactPage ( success bool , errorMsg string , eventID string , formData map [ string ] string , repoAnnouncement * nostr . RepoAnnouncement , feedItems [ ] FeedItemInfo ) ( string , error ) {
// Prepare form data with defaults
subject := ""
content := ""
@ -214,9 +370,10 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event
@@ -214,9 +370,10 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event
OGImage : g . siteURL + g . defaultImage ,
OGType : "website" ,
SiteName : g . siteName ,
SiteURL : g . siteURL ,
CurrentYear : time . Now ( ) . Year ( ) ,
WikiPages : [ ] WikiPageInfo { } , // Will be populated if needed
FeedItems : [ ] FeedItemInfo { } , // Will be populated if needed
FeedItems : feedItems ,
} ,
Success : success ,
Error : errorMsg ,
@ -228,12 +385,28 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event
@@ -228,12 +385,28 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event
} ,
}
// Render contact template with base template
// The base template uses {{block "content" .}} which will be filled by contact.html
// We need to pass the full ContactPageData so the template can access all fields
baseTmpl := g . templates . Lookup ( "base.html" )
if baseTmpl == nil {
return "" , fmt . Errorf ( "template base.html not found" )
// Parse base.html together with contact.html to ensure correct blocks are used
renderTmpl := template . New ( "render" ) . Funcs ( template . FuncMap {
"year" : func ( ) int { return time . Now ( ) . Year ( ) } ,
"json" : func ( v interface { } ) ( string , error ) {
b , err := json . Marshal ( v )
if err != nil {
return "" , err
}
return string ( b ) , nil
} ,
} )
// Parse base.html, components.html, and contact.html together
files := [ ] string {
filepath . Join ( g . templateDir , "components.html" ) , // Reusable components
filepath . Join ( g . templateDir , "base.html" ) ,
filepath . Join ( g . templateDir , "contact.html" ) ,
}
_ , err := renderTmpl . ParseFiles ( files ... )
if err != nil {
return "" , fmt . Errorf ( "failed to parse templates: %w" , err )
}
// Create a map that includes both PageData fields and contact-specific fields
@ -244,6 +417,7 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event
@@ -244,6 +417,7 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event
"OGImage" : data . OGImage ,
"OGType" : data . OGType ,
"SiteName" : data . SiteName ,
"SiteURL" : data . SiteURL ,
"CurrentYear" : data . CurrentYear ,
"WikiPages" : data . WikiPages ,
"BlogItems" : data . BlogItems ,
@ -254,17 +428,27 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event
@@ -254,17 +428,27 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event
"FormData" : data . FormData ,
}
// Execute the base template, which will use the contact.html "content" block
// Add repo announcement data for JavaScript
if repoAnnouncement != nil {
templateData [ "RepoAnnouncement" ] = map [ string ] interface { } {
"Pubkey" : repoAnnouncement . Pubkey ,
"DTag" : repoAnnouncement . DTag ,
"Relays" : repoAnnouncement . Relays ,
"Maintainers" : repoAnnouncement . Maintainers ,
}
}
// Execute base.html which will use the blocks from contact.html
var buf bytes . Buffer
if err := baseTmpl . ExecuteTemplate ( & buf , "base.html" , templateData ) ; err != nil {
if err := render Tmpl. ExecuteTemplate ( & buf , "base.html" , templateData ) ; err != nil {
return "" , fmt . Errorf ( "failed to execute base template: %w" , err )
}
return buf . String ( ) , nil
}
// GenerateErrorPage generates an error page (404 or 500)
func ( g * HTMLGenerator ) GenerateErrorPage ( statusCode int , siteName string ) ( string , error ) {
// GenerateErrorPage generates an error page (404 or 500) using base.html
func ( g * HTMLGenerator ) GenerateErrorPage ( statusCode int , feedItems [ ] FeedItemInfo ) ( string , error ) {
var title , message string
switch statusCode {
case 404 :
@ -278,36 +462,61 @@ func (g *HTMLGenerator) GenerateErrorPage(statusCode int, siteName string) (stri
@@ -278,36 +462,61 @@ func (g *HTMLGenerator) GenerateErrorPage(statusCode int, siteName string) (stri
message = "An error occurred."
}
data := map [ string ] interface { } {
"SiteName" : siteName ,
"Title" : title ,
"Message" : message ,
canonicalURL := g . siteURL
if statusCode == 404 {
canonicalURL = g . siteURL + "/404"
} else if statusCode == 500 {
canonicalURL = g . siteURL + "/500"
}
tmpl := g . templates . Lookup ( fmt . Sprintf ( "%d.html" , statusCode ) )
if tmpl == nil {
return "" , fmt . Errorf ( "template for status %d not found" , statusCode )
}
var buf bytes . Buffer
if err := tmpl . Execute ( & buf , data ) ; err != nil {
return "" , err
data := PageData {
Title : title ,
Description : message ,
CanonicalURL : canonicalURL ,
OGImage : g . siteURL + g . defaultImage ,
OGType : "website" ,
SiteName : g . siteName ,
SiteURL : g . siteURL ,
CurrentYear : time . Now ( ) . Year ( ) ,
WikiPages : [ ] WikiPageInfo { } ,
FeedItems : feedItems ,
Content : template . HTML ( "" ) , // Content will come from the template
}
return buf . String ( ) , nil
return g . renderTemplate ( fmt . Sprintf ( "%d.html" , statusCode ) , data )
}
// renderTemplate renders a template with the base template
func ( g * HTMLGenerator ) renderTemplate ( templateName string , data PageData ) ( string , error ) {
// The templateName (e.g., "landing.html") defines blocks that are used by base.html
// We need to execute base.html, which will use the blocks from templateName
baseTmpl := g . templates . Lookup ( "base.html" )
if baseTmpl == nil {
return "" , fmt . Errorf ( "template base.html not found" )
// Parse base.html together with the specific template to ensure correct blocks are used
// This avoids the issue where all templates are parsed together and the last "content" block wins
renderTmpl := template . New ( "render" ) . Funcs ( template . FuncMap {
"year" : func ( ) int { return time . Now ( ) . Year ( ) } ,
"json" : func ( v interface { } ) ( string , error ) {
b , err := json . Marshal ( v )
if err != nil {
return "" , err
}
return string ( b ) , nil
} ,
} )
// Parse base.html, components.html, and the specific template together
// This ensures the correct "content" block and reusable components are available
files := [ ] string {
filepath . Join ( g . templateDir , "components.html" ) , // Reusable components
filepath . Join ( g . templateDir , "base.html" ) ,
filepath . Join ( g . templateDir , templateName ) ,
}
_ , err := renderTmpl . ParseFiles ( files ... )
if err != nil {
return "" , fmt . Errorf ( "failed to parse templates: %w" , err )
}
// Execute base.html which will use the blocks from templateName
var buf bytes . Buffer
if err := baseTmpl . ExecuteTemplate ( & buf , "base.html" , data ) ; err != nil {
if err := render Tmpl. ExecuteTemplate ( & buf , "base.html" , data ) ; err != nil {
return "" , fmt . Errorf ( "failed to execute base template: %w" , err )
}