package generator import ( "bytes" "fmt" "html/template" "path/filepath" "time" "gitcitadel-online/internal/asciidoc" "gitcitadel-online/internal/nostr" ) // HTMLGenerator generates HTML pages from wiki events type HTMLGenerator struct { templates *template.Template asciidocProc *asciidoc.Processor linkBaseURL string siteName string siteURL string defaultImage string } // PageData represents data for a wiki page type PageData struct { Title string Description string Keywords string CanonicalURL string OGImage string OGType string StructuredData template.JS SiteName string CurrentYear int WikiPages []WikiPageInfo BlogItems []BlogItemInfo FeedItems []FeedItemInfo Content template.HTML Summary string TableOfContents template.HTML } // WikiPageInfo represents info about a wiki page for navigations type WikiPageInfo struct { DTag string Title string } // BlogItemInfo represents info about a blog item type BlogItemInfo struct { DTag string Title string Summary string Content template.HTML } // FeedItemInfo represents info about a feed item type FeedItemInfo struct { Author string Content string Time string TimeISO string Link string } // 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() }, }) // Load all templates templateFiles := []string{ "base.html", "landing.html", "page.html", "blog.html", "feed_sidebar.html", "404.html", "500.html", } for _, file := range templateFiles { path := filepath.Join(templateDir, file) _, err := tmpl.ParseFiles(path) if err != nil { return nil, err } } return &HTMLGenerator{ templates: tmpl, asciidocProc: asciidoc.NewProcessor(linkBaseURL), linkBaseURL: linkBaseURL, siteName: siteName, siteURL: siteURL, defaultImage: defaultImage, }, nil } // ProcessAsciiDoc processes AsciiDoc content to HTML func (g *HTMLGenerator) ProcessAsciiDoc(content string) (string, error) { return g.asciidocProc.Process(content) } // GenerateLandingPage generates the static landing page func (g *HTMLGenerator) GenerateLandingPage(wikiPages []WikiPageInfo, feedItems []FeedItemInfo) (string, error) { data := PageData{ Title: "Home", Description: "Welcome to " + g.siteName, CanonicalURL: g.siteURL + "/", OGImage: g.siteURL + g.defaultImage, OGType: "website", SiteName: g.siteName, CurrentYear: time.Now().Year(), WikiPages: wikiPages, FeedItems: feedItems, } return g.renderTemplate("landing.html", data) } // GenerateWikiPage generates a wiki article page func (g *HTMLGenerator) GenerateWikiPage(wiki *nostr.WikiEvent, wikiPages []WikiPageInfo, feedItems []FeedItemInfo) (string, error) { // Process AsciiDoc content htmlContent, err := g.asciidocProc.Process(wiki.Content) if err != nil { return "", err } description := wiki.Summary if description == "" { description = wiki.Title } canonicalURL := g.siteURL + "/wiki/" + wiki.DTag data := PageData{ Title: wiki.Title, Description: description, CanonicalURL: canonicalURL, OGImage: g.siteURL + g.defaultImage, OGType: "article", SiteName: g.siteName, CurrentYear: time.Now().Year(), WikiPages: wikiPages, FeedItems: feedItems, Content: template.HTML(htmlContent), Summary: wiki.Summary, } // Add structured data for article data.StructuredData = template.JS(g.generateArticleStructuredData(wiki, canonicalURL)) return g.renderTemplate("page.html", data) } // GenerateBlogPage generates the blog index page func (g *HTMLGenerator) GenerateBlogPage(blogIndex *nostr.IndexEvent, blogItems []BlogItemInfo, feedItems []FeedItemInfo) (string, error) { description := blogIndex.Summary if description == "" { description = "Blog articles from " + g.siteName } canonicalURL := g.siteURL + "/blog" data := PageData{ Title: "Blog", Description: description, CanonicalURL: canonicalURL, OGImage: g.siteURL + g.defaultImage, OGType: "website", SiteName: g.siteName, CurrentYear: time.Now().Year(), BlogItems: blogItems, FeedItems: feedItems, } return g.renderTemplate("blog.html", data) } // GenerateContactPage generates the contact form page func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, eventID string, formData map[string]string) (string, error) { // Prepare form data with defaults subject := "" content := "" labels := "" if formData != nil { subject = formData["subject"] content = formData["content"] labels = formData["labels"] } // Create form data struct for template type ContactFormData struct { Subject string Content string Labels string } type ContactPageData struct { PageData Success bool Error string EventID string FormData ContactFormData } data := ContactPageData{ PageData: PageData{ Title: "Contact", Description: "Contact " + g.siteName, CanonicalURL: g.siteURL + "/contact", OGImage: g.siteURL + g.defaultImage, OGType: "website", SiteName: g.siteName, CurrentYear: time.Now().Year(), WikiPages: []WikiPageInfo{}, // Will be populated if needed FeedItems: []FeedItemInfo{}, // Will be populated if needed }, Success: success, Error: errorMsg, EventID: eventID, FormData: ContactFormData{ Subject: subject, Content: content, Labels: labels, }, } // 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") } // Create a map that includes both PageData fields and contact-specific fields templateData := map[string]interface{}{ "Title": data.Title, "Description": data.Description, "CanonicalURL": data.CanonicalURL, "OGImage": data.OGImage, "OGType": data.OGType, "SiteName": data.SiteName, "CurrentYear": data.CurrentYear, "WikiPages": data.WikiPages, "BlogItems": data.BlogItems, "FeedItems": data.FeedItems, "Success": data.Success, "Error": data.Error, "EventID": data.EventID, "FormData": data.FormData, } // Execute the base template, which will use the contact.html "content" block var buf bytes.Buffer if err := baseTmpl.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) { var title, message string switch statusCode { case 404: title = "404 - Page Not Found" message = "The page you're looking for doesn't exist." case 500: title = "500 - Server Error" message = "Something went wrong on our end. Please try again later." default: title = "Error" message = "An error occurred." } data := map[string]interface{}{ "SiteName": siteName, "Title": title, "Message": message, } 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 } return buf.String(), nil } // 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") } var buf bytes.Buffer if err := baseTmpl.ExecuteTemplate(&buf, "base.html", data); err != nil { return "", fmt.Errorf("failed to execute base template: %w", err) } return buf.String(), nil } // generateArticleStructuredData generates JSON-LD structured data for an article func (g *HTMLGenerator) generateArticleStructuredData(wiki *nostr.WikiEvent, url string) string { // This is a simplified version - in production, use proper JSON encoding return `{ "@context": "https://schema.org", "@type": "Article", "headline": "` + wiki.Title + `", "description": "` + wiki.Summary + `", "url": "` + url + `", "publisher": { "@type": "Organization", "name": "` + g.siteName + `" } }` }