diff --git a/cmd/server/main.go b/cmd/server/main.go index 2d04801..da7dea9 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -88,7 +88,25 @@ func main() { time.Duration(cfg.Feed.PollIntervalMinutes)*time.Minute, ) - // Start cache rewarming + // Generate minimal landing page immediately so server can start serving + logger.Info("Generating initial landing page...") + initialLandingHTML, err := htmlGenerator.GenerateLandingPage( + []generator.WikiPageInfo{}, + []generator.FeedItemInfo{}, + nil, // newestBlogItem + nil, // newestArticleItem + ) + if err == nil { + if err := pageCache.Set("/", initialLandingHTML); err != nil { + logger.Warnf("Failed to cache initial landing page: %v", err) + } else { + logger.Info("Initial landing page cached") + } + } else { + logger.Warnf("Failed to generate initial landing page: %v", err) + } + + // Start cache rewarming (runs in background) rewarmer.Start(ctx) // Initialize HTTP server @@ -102,10 +120,7 @@ func main() { }() logger.Infof("Server started on port %d", cfg.Server.Port) - logger.Info("Waiting for initial cache population...") - - // Wait a bit for initial cache - time.Sleep(5 * time.Second) + logger.Info("Cache rewarming in progress... (pages will update as they become available)") // Wait for shutdown signal httpServer.WaitForShutdown() diff --git a/internal/generator/html.go b/internal/generator/html.go index 6a969f1..fdfcecf 100644 --- a/internal/generator/html.go +++ b/internal/generator/html.go @@ -120,6 +120,7 @@ type BlogItemInfo struct { Summary string Content template.HTML Author string + Image string CreatedAt int64 Time string // Formatted time TimeISO string // ISO time @@ -132,6 +133,7 @@ type ArticleItemInfo struct { Summary string Content template.HTML Author string + Image string CreatedAt int64 Time string // Formatted time TimeISO string // ISO time @@ -299,7 +301,7 @@ func (g *HTMLGenerator) ProcessMarkdown(markdownContent string) (string, error) } // GenerateLandingPage generates the static landing page -func (g *HTMLGenerator) GenerateLandingPage(wikiPages []WikiPageInfo, feedItems []FeedItemInfo) (string, error) { +func (g *HTMLGenerator) GenerateLandingPage(wikiPages []WikiPageInfo, feedItems []FeedItemInfo, newestBlogItem *BlogItemInfo, newestArticleItem *ArticleItemInfo) (string, error) { // Collect pubkeys from feed items pubkeys := make([]string, 0, len(feedItems)) for _, item := range feedItems { @@ -308,7 +310,15 @@ func (g *HTMLGenerator) GenerateLandingPage(wikiPages []WikiPageInfo, feedItems } } - // Fetch profiles for feed authors + // Add blog and article author pubkeys if available + if newestBlogItem != nil && newestBlogItem.Author != "" { + pubkeys = append(pubkeys, newestBlogItem.Author) + } + if newestArticleItem != nil && newestArticleItem.Author != "" { + pubkeys = append(pubkeys, newestArticleItem.Author) + } + + // Fetch profiles for feed authors, blog author, and article author ctx := context.Background() profiles := g.fetchProfilesBatch(ctx, pubkeys) @@ -326,7 +336,39 @@ func (g *HTMLGenerator) GenerateLandingPage(wikiPages []WikiPageInfo, feedItems Profiles: profiles, } - return g.renderTemplate("landing.html", data) + // Add newest blog and article items to template data + type LandingPageData struct { + PageData + NewestBlogItem *BlogItemInfo + NewestArticleItem *ArticleItemInfo + } + + landingData := LandingPageData{ + PageData: data, + NewestBlogItem: newestBlogItem, + NewestArticleItem: newestArticleItem, + } + + // Use renderTemplate but with custom data - need to include base.html for DOCTYPE + renderTmpl := template.New("landing-render").Funcs(getTemplateFuncs()) + + files := []string{ + filepath.Join(g.templateDir, "components.html"), + filepath.Join(g.templateDir, "base.html"), + filepath.Join(g.templateDir, "landing.html"), + } + + _, err := renderTmpl.ParseFiles(files...) + if err != nil { + return "", fmt.Errorf("failed to parse landing templates: %w", err) + } + + var buf bytes.Buffer + if err := renderTmpl.ExecuteTemplate(&buf, "base.html", landingData); err != nil { + return "", fmt.Errorf("failed to render landing template: %w", err) + } + + return buf.String(), nil } // GenerateWikiPage generates a wiki article page diff --git a/internal/nostr/events.go b/internal/nostr/events.go index 095de18..4c5ac5e 100644 --- a/internal/nostr/events.go +++ b/internal/nostr/events.go @@ -186,6 +186,7 @@ type BlogEvent struct { Title string Summary string Content string + Image string } // ParseBlogEvent parses a blog event @@ -226,6 +227,14 @@ func ParseBlogEvent(event *nostr.Event, expectedKind int) (*BlogEvent, error) { } } + // Extract image tag (optional) + for _, tag := range event.Tags { + if len(tag) > 0 && tag[0] == "image" && len(tag) > 1 { + blog.Image = tag[1] + break + } + } + return blog, nil } @@ -236,6 +245,7 @@ type LongformEvent struct { Title string Summary string Content string + Image string } // ParseLongformEvent parses a longform article event @@ -276,6 +286,14 @@ func ParseLongformEvent(event *nostr.Event, expectedKind int) (*LongformEvent, e } } + // Extract image tag (optional) + for _, tag := range event.Tags { + if len(tag) > 0 && tag[0] == "image" && len(tag) > 1 { + longform.Image = tag[1] + break + } + } + return longform, nil } diff --git a/static/css/main.css b/static/css/main.css index b2d16e6..107646c 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -518,6 +518,47 @@ a:focus { align-self: flex-start; } +/* Feature Image Container */ +.feature-image-container { + margin-bottom: 1rem; + width: 100%; +} + +.feature-image-wrapper { + position: relative; + width: 100%; + aspect-ratio: 16 / 9; + overflow: hidden; + border-radius: 8px; + background: var(--bg-primary); +} + +.feature-image { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.feature-image-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 50%, transparent 100%); + padding: 1.5rem 1rem 1rem; + color: #ffffff; +} + +.feature-image-title { + margin: 0; + font-size: 1.1rem; + font-weight: 600; + color: #ffffff; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); + line-height: 1.3; +} + /* Articles and Pages */ article { background: var(--bg-secondary); diff --git a/templates/landing.html b/templates/landing.html index d91135b..8348578 100644 --- a/templates/landing.html +++ b/templates/landing.html @@ -26,12 +26,58 @@
Read the latest articles and updates from the GitCitadel project blog.
{{icon "arrow-right"}} View BlogLongform markdown articles from the TheForest relay.
{{icon "arrow-right"}} View Articles