diff --git a/internal/generator/html.go b/internal/generator/html.go
index 9d2b4fc..f6f27cc 100644
--- a/internal/generator/html.go
+++ b/internal/generator/html.go
@@ -190,6 +190,7 @@ func NewHTMLGenerator(templateDir string, linkBaseURL, siteName, siteURL, defaul
"blog.html",
"wiki.html",
"contact.html",
+ "feed.html",
"404.html",
"500.html",
}
@@ -793,6 +794,46 @@ func (g *HTMLGenerator) GenerateErrorPage(statusCode int, feedItems []FeedItemIn
return g.renderTemplate(fmt.Sprintf("%d.html", statusCode), data)
}
+// GenerateFeedPage generates a full feed page
+func (g *HTMLGenerator) GenerateFeedPage(feedItems []FeedItemInfo) (string, error) {
+ canonicalURL := g.siteURL + "/feed"
+
+ data := PageData{
+ Title: "TheForest Feed",
+ Description: "Recent notes from TheForest relay",
+ 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 comes from template
+ }
+
+ // Use renderTemplate but with custom data for feed
+ renderTmpl := template.New("feed-render").Funcs(getTemplateFuncs())
+
+ files := []string{
+ filepath.Join(g.templateDir, "components.html"),
+ filepath.Join(g.templateDir, "base.html"),
+ filepath.Join(g.templateDir, "feed.html"),
+ }
+
+ _, err := renderTmpl.ParseFiles(files...)
+ if err != nil {
+ return "", fmt.Errorf("failed to parse feed templates: %w", err)
+ }
+
+ var buf bytes.Buffer
+ if err := renderTmpl.ExecuteTemplate(&buf, "base.html", data); err != nil {
+ return "", fmt.Errorf("failed to execute feed template: %w", err)
+ }
+
+ return buf.String(), nil
+}
+
// renderTemplate renders a template with the base template
func (g *HTMLGenerator) renderTemplate(templateName string, data PageData) (string, error) {
// Parse base.html together with the specific template to ensure correct blocks are used
diff --git a/internal/server/handlers.go b/internal/server/handlers.go
index 0f51f36..00e494b 100644
--- a/internal/server/handlers.go
+++ b/internal/server/handlers.go
@@ -31,6 +31,7 @@ func (s *Server) setupRoutes(mux *http.ServeMux) {
mux.HandleFunc("/articles", s.handleArticles)
mux.HandleFunc("/ebooks", s.handleEBooks)
mux.HandleFunc("/contact", s.handleContact)
+ mux.HandleFunc("/feed", s.handleFeed)
// Health and metrics
mux.HandleFunc("/health", s.handleHealth)
@@ -118,6 +119,17 @@ func (s *Server) handleEBooks(w http.ResponseWriter, r *http.Request) {
s.servePage(w, r, page)
}
+// handleFeed handles the Feed page
+func (s *Server) handleFeed(w http.ResponseWriter, r *http.Request) {
+ page, exists := s.cache.Get("/feed")
+ if !exists {
+ http.Error(w, "Page not ready", http.StatusServiceUnavailable)
+ return
+ }
+
+ s.servePage(w, r, page)
+}
+
// handleContact handles the contact form (GET and POST)
func (s *Server) handleContact(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
diff --git a/static/css/main.css b/static/css/main.css
index 9662533..a4b902f 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -128,6 +128,20 @@ header {
align-items: center;
}
+.nav-separator {
+ color: var(--text-secondary);
+ padding: 0 0.5rem;
+ user-select: none;
+}
+
+.nav-section-label {
+ color: var(--text-secondary);
+ font-weight: 600;
+ font-size: 0.9em;
+ padding: 0 0.25rem;
+ user-select: none;
+}
+
.nav-menu a {
color: var(--text-primary);
text-decoration: none;
@@ -368,6 +382,7 @@ a:focus {
.btn:hover {
background: var(--link-hover);
+ color: #1a1a1a;
}
.btn:active {
@@ -383,6 +398,23 @@ a:focus {
outline-offset: var(--focus-offset);
}
+/* Ensure all text and icons inside buttons are black */
+.btn,
+.btn *,
+.btn span,
+.btn .icon-inline,
+.btn svg {
+ color: #1a1a1a !important;
+}
+
+.btn:hover,
+.btn:hover *,
+.btn:hover span,
+.btn:hover .icon-inline,
+.btn:hover svg {
+ color: #1a1a1a !important;
+}
+
/* Landing Page Styles */
.landing-page {
max-width: 1200px;
@@ -1221,6 +1253,7 @@ textarea:focus-visible {
.btn-primary:hover {
background: var(--link-hover);
+ color: #1a1a1a;
}
.btn-secondary {
@@ -1265,6 +1298,52 @@ textarea:focus-visible {
}
/* E-Books page styles */
+.ebooks-page {
+ max-width: 1000px;
+ margin: 0 auto;
+}
+
+.feed-about-blurb {
+ background: var(--bg-secondary);
+ padding: 1.5rem;
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+ margin-bottom: 2rem;
+}
+
+.feed-about-blurb h2 {
+ color: var(--text-primary);
+ margin-bottom: 1rem;
+ font-size: 1.5rem;
+}
+
+.feed-about-blurb p {
+ margin-bottom: 1rem;
+ line-height: 1.6;
+}
+
+.feed-about-blurb ul {
+ margin-left: 1.5rem;
+ margin-top: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.feed-about-blurb li {
+ margin-bottom: 0.5rem;
+}
+
+.feed-about-blurb code {
+ background: var(--bg-primary);
+ padding: 0.2rem 0.4rem;
+ border-radius: 4px;
+ font-family: 'Courier New', monospace;
+ color: var(--accent-color);
+}
+
+.feed-page .feed-container {
+ margin-top: 2rem;
+}
+
.ebooks-page {
max-width: 1200px;
margin: 0 auto;
diff --git a/templates/base.html b/templates/base.html
index bcb3ffd..6d7f669 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -48,11 +48,16 @@
diff --git a/templates/feed.html b/templates/feed.html
new file mode 100644
index 0000000..1bed205
--- /dev/null
+++ b/templates/feed.html
@@ -0,0 +1,23 @@
+{{define "content"}}
+
+
+
+
+
About TheForest Relay
+
TheForest is a Nostr relay operated by GitCitadel. It provides a reliable, fast, and open relay service for the Nostr protocol.
+
+ - Relay URL:
wss://theforest.nostr1.com
+ - Status: Online and operational
+ - Features: Supports all standard Nostr event kinds
+
+
TheForest relay hosts a variety of content, including longform markdown articles (kind 30023), e-books and structured publications (kind 30040), and short-form notes (kind 1).
+
+
+
+ {{template "feed" .}}
+
+
+{{end}}
diff --git a/templates/landing.html b/templates/landing.html
index d4fea55..8427e54 100644
--- a/templates/landing.html
+++ b/templates/landing.html
@@ -98,6 +98,12 @@
+
+
{{icon "book"}} E-Books
Discover and download e-books from the decentralized #Alexandria library.