Browse Source

Handle wiki markup links

master
Silberengel 2 weeks ago
parent
commit
2c56e353c1
  1. 9
      internal/cache/rewarm.go
  2. 144
      internal/generator/html.go
  3. 214
      internal/server/handlers.go
  4. 2
      internal/server/server.go
  5. 15
      package-lock.json
  6. 3
      package.json
  7. 10
      static/css/highlight.js.css
  8. 113
      static/css/main.css
  9. 1213
      static/js/highlight.min.js
  10. 8
      templates/base.html
  11. 68
      templates/events.html
  12. 23
      templates/page.html

9
internal/cache/rewarm.go vendored

@ -209,11 +209,14 @@ func (r *Rewarmer) rewarmPages(ctx context.Context) {
continue continue
} }
// Process markdown content // Process content using gc-parser (handles Markdown, AsciiDoc, etc.)
html, err := r.htmlGenerator.ProcessMarkdown(article.Content) result, err := r.htmlGenerator.ProcessAsciiDoc(article.Content)
var html string
if err != nil { if err != nil {
logger.WithField("dtag", article.DTag).Warnf("Error processing markdown content: %v", err) logger.WithField("dtag", article.DTag).Warnf("Error processing content: %v", err)
html = article.Content // Fallback to raw content html = article.Content // Fallback to raw content
} else {
html = result
} }
articleItems = append(articleItems, generator.ArticleItemInfo{ articleItems = append(articleItems, generator.ArticleItemInfo{
DTag: article.DTag, DTag: article.DTag,

144
internal/generator/html.go

@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"html/template" "html/template"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
@ -179,6 +178,21 @@ type EBookInfo struct {
TimeISO string // ISO time TimeISO string // ISO time
} }
// EventCardInfo represents info about an event card for the events page
type EventCardInfo struct {
EventID string
Title string
DTag string
Author string
Summary string
Image string
Kind int
URL string // URL to the event page (based on kind:pubkey:dtag)
CreatedAt int64
Time string // Formatted time
TimeISO string // ISO time
}
// NewHTMLGenerator creates a new HTML generator // NewHTMLGenerator creates a new HTML generator
func NewHTMLGenerator(templateDir string, linkBaseURL, siteName, siteURL, defaultImage string, nostrClient *nostr.Client) (*HTMLGenerator, error) { func NewHTMLGenerator(templateDir string, linkBaseURL, siteName, siteURL, defaultImage string, nostrClient *nostr.Client) (*HTMLGenerator, error) {
tmpl := template.New("base").Funcs(getTemplateFuncs()) tmpl := template.New("base").Funcs(getTemplateFuncs())
@ -193,6 +207,7 @@ func NewHTMLGenerator(templateDir string, linkBaseURL, siteName, siteURL, defaul
"wiki.html", "wiki.html",
"contact.html", "contact.html",
"feed.html", "feed.html",
"events.html",
"404.html", "404.html",
"500.html", "500.html",
} }
@ -261,59 +276,6 @@ func (g *HTMLGenerator) ProcessAsciiDoc(content string) (string, error) {
return result.Content, nil return result.Content, nil
} }
// ProcessMarkdown processes Markdown content to HTML using marked via Node.js
func (g *HTMLGenerator) ProcessMarkdown(markdownContent string) (string, error) {
// Check if node is available
cmd := exec.Command("node", "--version")
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("node.js not found: %w", err)
}
// JavaScript code to run marked
jsCode := `
const { marked } = require('marked');
let content = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
content += chunk;
});
process.stdin.on('end', () => {
try {
// Configure marked options
marked.setOptions({
breaks: true,
gfm: true,
headerIds: true,
mangle: false
});
const html = marked.parse(content);
process.stdout.write(html);
} catch (error) {
console.error('Error converting Markdown:', error.message);
process.exit(1);
}
});
`
// Run node with the JavaScript code, passing content via stdin
cmd = exec.Command("node", "-e", jsCode)
cmd.Stdin = strings.NewReader(markdownContent)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("marked conversion failed: %w, stderr: %s", err, stderr.String())
}
return stdout.String(), nil
}
// GenerateLandingPage generates the static landing page // GenerateLandingPage generates the static landing page
func (g *HTMLGenerator) GenerateLandingPage(wikiPages []WikiPageInfo, newestBlogItem *BlogItemInfo, newestArticleItem *ArticleItemInfo, allArticleItems []ArticleItemInfo, allEBooks []EBookInfo) (string, error) { func (g *HTMLGenerator) GenerateLandingPage(wikiPages []WikiPageInfo, newestBlogItem *BlogItemInfo, newestArticleItem *ArticleItemInfo, allArticleItems []ArticleItemInfo, allEBooks []EBookInfo) (string, error) {
// Collect pubkeys from blog and article items // Collect pubkeys from blog and article items
@ -883,6 +845,80 @@ func (g *HTMLGenerator) GenerateErrorPage(statusCode int, feedItems []FeedItemIn
return g.renderTemplate(fmt.Sprintf("%d.html", statusCode), data) return g.renderTemplate(fmt.Sprintf("%d.html", statusCode), data)
} }
// GenerateEventsPage generates the events page for a specific d-tag
func (g *HTMLGenerator) GenerateEventsPage(dTag string, eventCards []EventCardInfo, feedItems []FeedItemInfo) (string, error) {
canonicalURL := g.siteURL + "/events?d=" + dTag
// Collect pubkeys from event cards
pubkeys := make([]string, 0, len(eventCards))
seenPubkeys := make(map[string]bool)
for _, card := range eventCards {
if card.Author != "" && !seenPubkeys[card.Author] {
pubkeys = append(pubkeys, card.Author)
seenPubkeys[card.Author] = true
}
}
// Fetch profiles for all authors
ctx := context.Background()
profiles := g.fetchProfilesBatch(ctx, pubkeys)
description := fmt.Sprintf("Events with d-tag: %s", dTag)
if len(eventCards) > 0 {
description = fmt.Sprintf("Browse %d events with d-tag: %s", len(eventCards), dTag)
}
data := PageData{
Title: fmt.Sprintf("Events: %s", dTag),
Description: description,
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
Profiles: profiles,
}
// Add event cards to a custom field - we'll need to extend PageData or use a map
// For now, let's use a custom template data structure
type EventsPageData struct {
PageData
EventCards []EventCardInfo
DTag string
}
eventsData := EventsPageData{
PageData: data,
EventCards: eventCards,
DTag: dTag,
}
// Use renderTemplate but with custom data for events
renderTmpl := template.New("events-render").Funcs(getTemplateFuncs())
files := []string{
filepath.Join(g.templateDir, "components.html"),
filepath.Join(g.templateDir, "base.html"),
filepath.Join(g.templateDir, "events.html"),
}
_, err := renderTmpl.ParseFiles(files...)
if err != nil {
return "", fmt.Errorf("failed to parse events templates: %w", err)
}
var buf bytes.Buffer
if err := renderTmpl.ExecuteTemplate(&buf, "base.html", eventsData); err != nil {
return "", fmt.Errorf("failed to execute events template: %w", err)
}
return buf.String(), nil
}
// GenerateFeedPage generates a full feed page // GenerateFeedPage generates a full feed page
func (g *HTMLGenerator) GenerateFeedPage(feedItems []FeedItemInfo) (string, error) { func (g *HTMLGenerator) GenerateFeedPage(feedItems []FeedItemInfo) (string, error) {
canonicalURL := g.siteURL + "/feed" canonicalURL := g.siteURL + "/feed"

214
internal/server/handlers.go

@ -70,6 +70,90 @@ func (s *Server) handleLanding(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleWiki(w http.ResponseWriter, r *http.Request) { func (s *Server) handleWiki(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path path := r.URL.Path
// Check for event ID first (fastest lookup)
eventID := r.URL.Query().Get("e")
if eventID != "" {
// Fetch event by ID (fastest method)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
filter := gonostr.Filter{
IDs: []string{eventID},
}
event, err := s.nostrClient.FetchEvent(ctx, filter)
if err == nil && event != nil {
// Parse as wiki event
wikiEvent, err := nostr.ParseWikiEvent(event, event.Kind)
if err == nil {
// Get wiki pages for navigation (empty for now, could be fetched)
wikiPages := []generator.WikiPageInfo{}
// Generate the wiki page
html, err := s.htmlGenerator.GenerateWikiPage(wikiEvent, wikiPages, []generator.FeedItemInfo{})
if err == nil {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
return
}
}
}
}
// Fallback: Check for hash-based event reference in query parameter
// Format: kind:pubkey:dtag (e.g., "30818:dd664d5e...:nkbip-04")
ref := r.URL.Query().Get("ref")
if ref == "" {
// Try to get from fragment (though browsers don't send this, JavaScript can convert it)
ref = r.URL.Query().Get("k") + ":" + r.URL.Query().Get("a") + ":" + r.URL.Query().Get("d")
if strings.HasPrefix(ref, ":") {
ref = ""
}
}
// If we have a reference, fetch and render that specific event
if ref != "" {
parts := strings.Split(ref, ":")
if len(parts) == 3 {
var kind int
if _, err := fmt.Sscanf(parts[0], "%d", &kind); err == nil {
pubkey := parts[1]
dTag := parts[2]
// Fetch the specific wiki event
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
filter := gonostr.Filter{
Kinds: []int{kind},
Authors: []string{pubkey},
Tags: map[string][]string{
"d": {dTag},
},
Limit: 1,
}
events, err := s.nostrClient.FetchEvents(ctx, filter)
if err == nil && len(events) > 0 {
// Parse as wiki event
wikiEvent, err := nostr.ParseWikiEvent(events[0], kind)
if err == nil {
// Get wiki pages for navigation (empty for now, could be fetched)
wikiPages := []generator.WikiPageInfo{}
// Generate the wiki page
html, err := s.htmlGenerator.GenerateWikiPage(wikiEvent, wikiPages, []generator.FeedItemInfo{})
if err == nil {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
return
}
}
}
}
}
}
// Handle wiki index page (/wiki or /wiki/) // Handle wiki index page (/wiki or /wiki/)
if path == "/wiki" || path == "/wiki/" { if path == "/wiki" || path == "/wiki/" {
page, exists := s.cache.Get("/wiki") page, exists := s.cache.Get("/wiki")
@ -180,93 +264,77 @@ func (s *Server) handleEvents(w http.ResponseWriter, r *http.Request) {
return return
} }
// Fetch profiles for all events // Convert events to EventCardInfo
pubkeys := make([]string, 0, len(allEvents)) eventCards := make([]generator.EventCardInfo, 0, len(allEvents))
seenPubkeys := make(map[string]bool)
for _, event := range allEvents { for _, event := range allEvents {
if !seenPubkeys[event.PubKey] { // Extract title, summary, image from tags
pubkeys = append(pubkeys, event.PubKey)
seenPubkeys[event.PubKey] = true
}
}
profiles := make(map[string]*nostr.Profile)
if len(pubkeys) > 0 {
profileCtx, profileCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer profileCancel()
fetchedProfiles, err := s.nostrClient.FetchProfilesBatch(profileCtx, pubkeys)
if err == nil {
profiles = fetchedProfiles
}
}
// Generate HTML page showing the events
// For now, we'll create a simple list - you can enhance this with a proper template
html := s.generateEventsPage(dTag, allEvents, profiles)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(html))
}
// generateEventsPage generates HTML for the events page
func (s *Server) generateEventsPage(dTag string, events []*gonostr.Event, profiles map[string]*nostr.Profile) string {
// Simple HTML generation - you can enhance this with a proper template
html := `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Events: ` + dTag + ` - ` + s.siteURL + `</title>
<link rel="stylesheet" href="/static/css/main.css">
</head>
<body>
<div class="container">
<h1>Events with d-tag: ` + dTag + `</h1>
<div class="events-list">`
for _, event := range events {
profile := profiles[event.PubKey]
authorName := event.PubKey[:16] + "..."
if profile != nil && profile.Name != "" {
authorName = profile.Name
}
// Extract title from tags
title := dTag title := dTag
summary := ""
image := ""
for _, tag := range event.Tags { for _, tag := range event.Tags {
if len(tag) > 0 && tag[0] == "title" && len(tag) > 1 { if len(tag) > 0 && len(tag) > 1 {
switch tag[0] {
case "title":
title = tag[1] title = tag[1]
break case "summary":
summary = tag[1]
case "image":
image = tag[1]
}
} }
} }
// Determine URL based on kind // Build URL based on kind:pubkey:dtag format
var url string var url string
switch event.Kind { switch event.Kind {
case nostr.KindBlog: case nostr.KindBlog:
url = "/blog#" + dTag // Blog uses hash format with full identifier
url = fmt.Sprintf("/blog#%d:%s:%s", event.Kind, event.PubKey, dTag)
case nostr.KindLongform: case nostr.KindLongform:
url = "/articles#" + dTag // Articles use hash format with full identifier
url = fmt.Sprintf("/articles#%d:%s:%s", event.Kind, event.PubKey, dTag)
case nostr.KindWiki:
// Wiki uses query parameter format with event ID for faster lookup
// Fallback to ref format if needed
url = fmt.Sprintf("/wiki?e=%s&ref=%d:%s:%s", event.ID, event.Kind, event.PubKey, dTag)
default: default:
url = "/wiki/" + dTag // Fallback
url = fmt.Sprintf("/events?d=%s", dTag)
}
// Format time
createdTime := time.Unix(int64(event.CreatedAt), 0)
timeStr := createdTime.Format("Jan 2, 2006")
timeISO := createdTime.Format(time.RFC3339)
eventCards = append(eventCards, generator.EventCardInfo{
EventID: event.ID,
Title: title,
DTag: dTag,
Author: event.PubKey,
Summary: summary,
Image: image,
Kind: event.Kind,
URL: url,
CreatedAt: int64(event.CreatedAt),
Time: timeStr,
TimeISO: timeISO,
})
} }
html += ` // Generate HTML page using the HTML generator
<div class="event-card"> html, err := s.htmlGenerator.GenerateEventsPage(dTag, eventCards, []generator.FeedItemInfo{})
<h2><a href="` + url + `">` + title + `</a></h2> if err != nil {
<p>Kind: ` + fmt.Sprintf("%d", event.Kind) + `</p> logger.WithFields(map[string]interface{}{
<p>Author: ` + authorName + `</p> "dtag": dTag,
<p>Created: ` + time.Unix(int64(event.CreatedAt), 0).Format("2006-01-02 15:04:05") + `</p> "error": err,
</div>` }).Error("Failed to generate events page")
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
} }
html += ` w.Header().Set("Content-Type", "text/html; charset=utf-8")
</div> w.Write([]byte(html))
</div>
</body>
</html>`
return html
} }
// handleContact handles the contact form (GET and POST) // handleContact handles the contact form (GET and POST)
@ -675,8 +743,8 @@ func (s *Server) middleware(next http.Handler) http.Handler {
w.Header().Set("X-XSS-Protection", "1; mode=block") w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
// CSP header - allow unpkg.com for Lucide icons and jsdelivr.net for nostr-tools // CSP header - all scripts and styles are served locally
w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:;") 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:;")
// Log request (only in debug mode to reduce noise) // Log request (only in debug mode to reduce noise)
start := time.Now() start := time.Now()

2
internal/server/server.go

@ -42,6 +42,8 @@ type IssueServiceInterface interface {
type HTMLGeneratorInterface interface { type HTMLGeneratorInterface interface {
GenerateContactPage(success bool, errorMsg string, eventID string, formData map[string]string, repoAnnouncement *nostr.RepoAnnouncement, feedItems []generator.FeedItemInfo, profile *nostr.Profile) (string, error) GenerateContactPage(success bool, errorMsg string, eventID string, formData map[string]string, repoAnnouncement *nostr.RepoAnnouncement, feedItems []generator.FeedItemInfo, profile *nostr.Profile) (string, error)
GenerateErrorPage(statusCode int, feedItems []generator.FeedItemInfo) (string, error) GenerateErrorPage(statusCode int, feedItems []generator.FeedItemInfo) (string, error)
GenerateEventsPage(dTag string, eventCards []generator.EventCardInfo, feedItems []generator.FeedItemInfo) (string, error)
GenerateWikiPage(wiki *nostr.WikiEvent, wikiPages []generator.WikiPageInfo, feedItems []generator.FeedItemInfo) (string, error)
} }
// NewServer creates a new HTTP server // NewServer creates a new HTTP server

15
package-lock.json generated

@ -6,6 +6,9 @@
"": { "": {
"dependencies": { "dependencies": {
"gc-parser": "git+https://git.imwald.eu/silberengel/gc-parser.git" "gc-parser": "git+https://git.imwald.eu/silberengel/gc-parser.git"
},
"devDependencies": {
"highlight.js": "^11.11.1"
} }
}, },
"node_modules/@asciidoctor/core": { "node_modules/@asciidoctor/core": {
@ -58,7 +61,7 @@
}, },
"node_modules/gc-parser": { "node_modules/gc-parser": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "git+https://git.imwald.eu/silberengel/gc-parser.git#80b70a87d09efada2d688ba77f5e35118593c1a1", "resolved": "git+https://git.imwald.eu/silberengel/gc-parser.git#f02450c08aeaf3f96b7afd979c959473fe0235a5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@asciidoctor/core": "^3.0.4" "@asciidoctor/core": "^3.0.4"
@ -84,6 +87,16 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/inflight": { "node_modules/inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",

3
package.json

@ -1,5 +1,8 @@
{ {
"dependencies": { "dependencies": {
"gc-parser": "git+https://git.imwald.eu/silberengel/gc-parser.git" "gc-parser": "git+https://git.imwald.eu/silberengel/gc-parser.git"
},
"devDependencies": {
"highlight.js": "^11.11.1"
} }
} }

10
static/css/highlight.js.css

@ -0,0 +1,10 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: GitHub Dark
Description: Dark theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-dark
Current colors taken from GitHub's CSS
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}

113
static/css/main.css

@ -2169,3 +2169,116 @@ p.icon-inline, h1.icon-inline, h2.icon-inline, h3.icon-inline, h4.icon-inline, h
font-weight: normal; font-weight: normal;
font-size: 0.95rem; font-size: 0.95rem;
} }
/* Events Page */
.events-page {
padding: 2rem 0;
}
.events-grid {
margin-top: 2rem;
}
.events-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.event-card {
background: var(--bg-secondary);
border-radius: 8px;
border: 1px solid var(--border-color);
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
.event-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.event-card-link {
display: block;
text-decoration: none;
color: inherit;
}
.event-card-image {
width: 100%;
aspect-ratio: 16 / 9;
overflow: hidden;
background: var(--bg-primary);
}
.event-card-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.event-card-content {
padding: 1.5rem;
}
.event-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
margin-bottom: 0.75rem;
}
.event-card-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
margin: 0;
flex: 1;
line-height: 1.3;
}
.event-card-kind {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
color: var(--text-secondary);
white-space: nowrap;
}
.event-card-summary {
color: var(--text-secondary);
margin: 0 0 1rem 0;
line-height: 1.5;
}
.event-card-meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
font-size: 0.875rem;
color: var(--text-secondary);
}
.event-card-author,
.event-card-date {
display: flex;
align-items: center;
gap: 0.5rem;
}
.empty-state {
text-align: center;
padding: 3rem 2rem;
color: var(--text-secondary);
}
.empty-state p {
font-size: 1.1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}

1213
static/js/highlight.min.js vendored

File diff suppressed because one or more lines are too long

8
templates/base.html

@ -27,6 +27,9 @@
<link rel="stylesheet" href="/static/css/main.css"> <link rel="stylesheet" href="/static/css/main.css">
<link rel="stylesheet" href="/static/css/responsive.css"> <link rel="stylesheet" href="/static/css/responsive.css">
<link rel="stylesheet" href="/static/css/print.css" media="print"> <link rel="stylesheet" href="/static/css/print.css" media="print">
<!-- Highlight.js for code syntax highlighting -->
<link rel="stylesheet" href="/static/css/highlight.js.css">
<script src="/static/js/highlight.min.js"></script>
{{if .StructuredData}}<script type="application/ld+json">{{.StructuredData}}</script>{{end}} {{if .StructuredData}}<script type="application/ld+json">{{.StructuredData}}</script>{{end}}
</head> </head>
@ -124,6 +127,11 @@
window.location.href = this.value; window.location.href = this.value;
}); });
} }
// Initialize highlight.js for code blocks
if (typeof hljs !== 'undefined') {
hljs.highlightAll();
}
</script> </script>
</body> </body>
</html> </html>

68
templates/events.html

@ -0,0 +1,68 @@
{{define "content"}}
<article class="events-page">
<section class="hero">
<div class="hero-content">
<div class="hero-text">
<h1>Events: {{.DTag}}</h1>
<p class="lead">Found {{len .EventCards}} event{{if ne (len .EventCards) 1}}s{{end}} with this d-tag</p>
</div>
</div>
</section>
<section class="events-grid">
{{if .EventCards}}
<div class="events-container">
{{range .EventCards}}
<div class="event-card">
<a href="{{.URL}}" class="event-card-link">
{{if and .Image (ne .Image "")}}
<div class="event-card-image">
<img src="{{.Image}}" alt="{{.Title}}" />
</div>
{{end}}
<div class="event-card-content">
<div class="event-card-header">
<h3 class="event-card-title">{{.Title}}</h3>
<span class="event-card-kind">
{{if eq .Kind 30818}}
<span class="icon-inline">{{icon "book-open"}}</span> Wiki
{{else if eq .Kind 30041}}
<span class="icon-inline">{{icon "file-text"}}</span> Blog
{{else if eq .Kind 30023}}
<span class="icon-inline">{{icon "file-text"}}</span> Article
{{else}}
<span class="icon-inline">{{icon "file"}}</span> Kind {{.Kind}}
{{end}}
</span>
</div>
{{if .Summary}}
<p class="event-card-summary">{{truncate .Summary 200}}</p>
{{end}}
<div class="event-card-meta">
{{if .Author}}
<span class="event-card-author">
<span class="icon-inline">{{icon "user"}}</span>
{{template "user-badge-simple" (dict "Pubkey" .Author "Profiles" $.Profiles)}}
</span>
{{end}}
{{if .Time}}
<span class="event-card-date">
<span class="icon-inline">{{icon "clock"}}</span> {{.Time}}
</span>
{{end}}
</div>
</div>
</a>
</div>
{{end}}
</div>
{{else}}
<div class="empty-state">
<p><span class="icon-inline">{{icon "inbox"}}</span> No events found with d-tag: {{.DTag}}</p>
</div>
{{end}}
</section>
</article>
{{end}}
{{/* Feed is defined in components.html */}}

23
templates/page.html

@ -5,17 +5,32 @@
{{if .Summary}}<p class="page-summary">{{.Summary}}</p>{{end}} {{if .Summary}}<p class="page-summary">{{.Summary}}</p>{{end}}
</header> </header>
<div class="page-content">
{{.Content}}
</div>
{{if .TableOfContents}} {{if .TableOfContents}}
<aside class="table-of-contents"> <aside class="table-of-contents">
<h2><span class="icon-inline">{{icon "list"}}</span> Table of Contents</h2> <h2><span class="icon-inline">{{icon "list"}}</span> Table of Contents</h2>
{{.TableOfContents}} {{.TableOfContents}}
</aside> </aside>
{{end}} {{end}}
<div class="page-content">
{{.Content}}
</div>
</article> </article>
<script>
// Handle hash-based wiki URLs: convert /wiki#kind:pubkey:dtag to /wiki?ref=kind:pubkey:dtag
(function() {
if (window.location.pathname === '/wiki' || window.location.pathname === '/wiki/') {
const hash = window.location.hash.substring(1); // Remove the '#'
if (hash && /^\d+:[a-fA-F0-9]+:[^:]+$/.test(hash)) {
// Hash matches format: kind:pubkey:dtag
// Convert to query parameter and reload
const newUrl = window.location.pathname + '?ref=' + encodeURIComponent(hash);
window.location.replace(newUrl);
}
}
})();
</script>
{{end}} {{end}}
{{/* Feed is defined in components.html */}} {{/* Feed is defined in components.html */}}

Loading…
Cancel
Save