diff --git a/internal/generator/html.go b/internal/generator/html.go
index 61e30dc..c2b6f53 100644
--- a/internal/generator/html.go
+++ b/internal/generator/html.go
@@ -31,6 +31,20 @@ func getTemplateFuncs() template.FuncMap {
"shortenPubkey": func(pubkey string) string {
return nostr.ShortenPubkey(pubkey)
},
+ "dict": func(values ...interface{}) (map[string]interface{}, error) {
+ if len(values)%2 != 0 {
+ return nil, fmt.Errorf("dict requires an even number of arguments")
+ }
+ dict := make(map[string]interface{})
+ for i := 0; i < len(values); i += 2 {
+ key, ok := values[i].(string)
+ if !ok {
+ return nil, fmt.Errorf("dict keys must be strings")
+ }
+ dict[key] = values[i+1]
+ }
+ return dict, nil
+ },
}
}
@@ -463,6 +477,7 @@ func (g *HTMLGenerator) GenerateContactPage(success bool, errorMsg string, event
SiteURL: g.siteURL,
CurrentYear: time.Now().Year(),
WikiPages: []WikiPageInfo{}, // Will be populated if needed
+ BlogItems: []BlogItemInfo{}, // Empty - no blog items on contact page
FeedItems: []FeedItemInfo{}, // Empty - feed only on landing page
Profiles: make(map[string]*nostr.Profile), // Empty profiles for contact page
},
diff --git a/internal/nostr/client.go b/internal/nostr/client.go
index d481c1d..fae8217 100644
--- a/internal/nostr/client.go
+++ b/internal/nostr/client.go
@@ -255,6 +255,21 @@ func (c *Client) IsConnected() bool {
return true // We connect on-demand
}
+// GetRelays returns all configured relay URLs (primary, fallback, additional fallback)
+func (c *Client) GetRelays() []string {
+ relays := []string{}
+ if c.primaryRelay != "" {
+ relays = append(relays, c.primaryRelay)
+ }
+ if c.fallbackRelay != "" {
+ relays = append(relays, c.fallbackRelay)
+ }
+ if c.additionalFallback != "" {
+ relays = append(relays, c.additionalFallback)
+ }
+ return relays
+}
+
// HealthCheck performs a health check on the relays
func (c *Client) HealthCheck(ctx context.Context, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(ctx, timeout)
diff --git a/internal/nostr/ebooks.go b/internal/nostr/ebooks.go
index 39a701b..26b29e8 100644
--- a/internal/nostr/ebooks.go
+++ b/internal/nostr/ebooks.go
@@ -142,10 +142,27 @@ func (es *EBooksService) FetchTopLevelIndexEvents(ctx context.Context) ([]EBookI
}
}
- // Build naddr
- // Format: naddr1 + bech32 encoded (kind:pubkey:dtag:relay)
- // For simplicity, we'll use the format: kind:pubkey:dtag
- ebook.Naddr = fmt.Sprintf("%d:%s:%s", es.indexKind, ebook.Author, ebook.DTag)
+ // Build naddr - create proper bech32-encoded naddr
+ // Extract relay hints from event tags if available
+ var relays []string
+ for _, tag := range event.Tags {
+ if len(tag) > 0 && tag[0] == "relays" {
+ relays = append(relays, tag[1:]...)
+ }
+ }
+ // If no relays in tags, use the relay we fetched from
+ if len(relays) == 0 {
+ relays = []string{es.relayURL}
+ }
+
+ naddr, err := CreateNaddr(es.indexKind, ebook.Author, ebook.DTag, relays)
+ if err != nil {
+ log.Printf("Failed to create naddr for ebook %s: %v", ebook.DTag, err)
+ // Fallback to kind:pubkey:dtag format
+ ebook.Naddr = fmt.Sprintf("%d:%s:%s", es.indexKind, ebook.Author, ebook.DTag)
+ } else {
+ ebook.Naddr = naddr
+ }
ebooks = append(ebooks, ebook)
}
diff --git a/internal/nostr/feed.go b/internal/nostr/feed.go
index 891dd42..e711722 100644
--- a/internal/nostr/feed.go
+++ b/internal/nostr/feed.go
@@ -3,6 +3,7 @@ package nostr
import (
"context"
"fmt"
+ "log"
"time"
"github.com/nbd-wtf/go-nostr"
@@ -22,38 +23,71 @@ func NewFeedService(client *Client, feedKind int) *FeedService {
}
}
-// FetchFeedItems fetches recent feed events from a specific relay
-func (fs *FeedService) FetchFeedItems(ctx context.Context, relay string, maxEvents int) ([]FeedItem, error) {
+// FetchFeedItems fetches recent feed events from the configured feed relay only, with retries
+func (fs *FeedService) FetchFeedItems(ctx context.Context, feedRelay string, maxEvents int) ([]FeedItem, error) {
+ if feedRelay == "" {
+ return nil, fmt.Errorf("feed relay not configured")
+ }
+
filter := nostr.Filter{
Kinds: []int{fs.feedKind},
Limit: maxEvents,
}
logFilter(filter, fmt.Sprintf("feed (kind %d)", fs.feedKind))
- // Connect to the specific feed relay (not the default client relays)
- relayConn, err := fs.client.ConnectToRelay(ctx, relay)
- if err != nil {
- return nil, fmt.Errorf("failed to connect to feed relay %s: %w", relay, err)
- }
- defer relayConn.Close()
+ const maxRetries = 3
+ var lastErr error
- events, err := relayConn.QuerySync(ctx, filter)
- if err != nil {
- return nil, fmt.Errorf("failed to fetch feed events from %s: %w", relay, err)
- }
+ // Retry up to 3 times for the configured feed relay only
+ for attempt := 1; attempt <= maxRetries; attempt++ {
+ if attempt > 1 {
+ log.Printf("Retrying feed relay %s (attempt %d/%d)", feedRelay, attempt, maxRetries)
+ // Wait a bit before retrying (exponential backoff: 1s, 2s)
+ waitTime := time.Duration(attempt-1) * time.Second
+ time.Sleep(waitTime)
+ }
+
+ // Create a context with timeout for this attempt
+ attemptCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
- items := make([]FeedItem, 0, len(events))
- for _, event := range events {
- item := FeedItem{
- Author: event.PubKey,
- Content: event.Content,
- Time: time.Unix(int64(event.CreatedAt), 0),
- Link: fmt.Sprintf("https://alexandria.gitcitadel.eu/events?id=nevent1%s", event.ID),
+ // Connect to the relay
+ relayConn, err := fs.client.ConnectToRelay(attemptCtx, feedRelay)
+ if err != nil {
+ log.Printf("Failed to connect to feed relay %s (attempt %d/%d): %v", feedRelay, attempt, maxRetries, err)
+ lastErr = err
+ cancel()
+ continue // Try next attempt
}
- items = append(items, item)
+
+ // Try to fetch events
+ events, err := relayConn.QuerySync(attemptCtx, filter)
+ relayConn.Close()
+ cancel()
+
+ if err != nil {
+ log.Printf("Failed to fetch feed events from %s (attempt %d/%d): %v", feedRelay, attempt, maxRetries, err)
+ lastErr = err
+ continue // Try next attempt
+ }
+
+ // Success! Convert events to feed items
+ items := make([]FeedItem, 0, len(events))
+ for _, event := range events {
+ item := FeedItem{
+ Author: event.PubKey,
+ Content: event.Content,
+ Time: time.Unix(int64(event.CreatedAt), 0),
+ Link: fmt.Sprintf("https://alexandria.gitcitadel.eu/events?id=nevent1%s", event.ID),
+ }
+ items = append(items, item)
+ }
+
+ log.Printf("Successfully fetched %d feed items from %s (attempt %d/%d)", len(items), feedRelay, attempt, maxRetries)
+ return items, nil
}
- return items, nil
+ // All retries failed
+ return nil, fmt.Errorf("failed to fetch feed from %s after %d retries (last error: %w)", feedRelay, maxRetries, lastErr)
}
// FeedItem represents a feed item
diff --git a/internal/nostr/naddr.go b/internal/nostr/naddr.go
index d58913e..c7a98db 100644
--- a/internal/nostr/naddr.go
+++ b/internal/nostr/naddr.go
@@ -69,3 +69,14 @@ func (n *Naddr) ToFilter() nostr.Filter {
},
}
}
+
+// CreateNaddr creates a bech32-encoded naddr from kind, pubkey, dtag, and optional relays
+func CreateNaddr(kind int, pubkey string, dtag string, relays []string) (string, error) {
+ // nip19.EncodeEntity signature: (publicKey string, kind int, identifier string, relays []string)
+ naddr, err := nip19.EncodeEntity(pubkey, kind, dtag, relays)
+ if err != nil {
+ return "", fmt.Errorf("failed to encode naddr: %w", err)
+ }
+
+ return naddr, nil
+}
diff --git a/internal/server/handlers.go b/internal/server/handlers.go
index 069a3a1..b7eb85d 100644
--- a/internal/server/handlers.go
+++ b/internal/server/handlers.go
@@ -364,8 +364,8 @@ func (s *Server) middleware(next http.Handler) http.Handler {
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
- // CSP header
- w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;")
+ // CSP header - allow unpkg.com for Lucide icons
+ w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;")
// Log request
start := time.Now()
diff --git a/static/css/main.css b/static/css/main.css
index c332017..2482939 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -43,6 +43,8 @@ body {
min-height: 100vh;
display: flex;
flex-direction: column;
+ margin: 0;
+ padding: 0;
padding-top: 80px; /* Space for fixed header */
}
@@ -72,10 +74,15 @@ header {
right: 0;
width: 100%;
z-index: 1000;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ margin: 0;
+ padding: 0;
}
.navbar {
padding: 1rem 0;
+ width: 100%;
+ margin: 0;
}
.nav-container {
@@ -111,12 +118,24 @@ header {
list-style: none;
gap: 2rem;
align-items: center;
+ margin: 0;
+ padding: 0;
+}
+
+.nav-menu li {
+ margin: 0;
+ padding: 0;
+ list-style: none;
}
.nav-menu a {
color: var(--text-primary);
text-decoration: none;
transition: color 0.2s;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 0;
}
.nav-menu a:hover {
@@ -168,9 +187,12 @@ header {
list-style: none;
min-width: 200px;
padding: 0.5rem 0;
+ margin: 0;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
+ z-index: 1001;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.dropdown:hover .dropdown-menu,
@@ -196,6 +218,7 @@ header {
padding: 2rem 1rem;
gap: 2rem;
flex: 1;
+ min-height: calc(100vh - 80px - 4rem); /* Account for header and padding */
}
.wiki-layout {
diff --git a/static/css/responsive.css b/static/css/responsive.css
index 2237f13..e27e8ce 100644
--- a/static/css/responsive.css
+++ b/static/css/responsive.css
@@ -33,16 +33,18 @@
.nav-menu {
position: fixed;
- top: 60px;
+ top: 80px; /* Match header height */
left: -100%;
width: 100%;
- height: calc(100vh - 60px);
+ height: calc(100vh - 80px);
background: var(--bg-secondary);
flex-direction: column;
align-items: flex-start;
padding: 2rem;
transition: left 0.3s;
border-top: 1px solid var(--border-color);
+ z-index: 999;
+ box-shadow: 2px 0 8px rgba(0, 0, 0, 0.2);
}
.nav-menu.active {
diff --git a/templates/base.html b/templates/base.html
index 1253ab2..3323f6d 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -74,7 +74,7 @@
{{block "content" .}}{{end}}
- {{if or (eq .CanonicalURL .SiteURL) (eq .CanonicalURL (printf "%s/" .SiteURL))}}
+ {{if and (or (eq .CanonicalURL .SiteURL) (eq .CanonicalURL (printf "%s/" .SiteURL))) (gt (len .FeedItems) 0)}}
@@ -87,12 +87,53 @@