You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
331 lines
8.8 KiB
331 lines
8.8 KiB
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 + `" |
|
} |
|
}` |
|
}
|
|
|