diff --git a/.well-known/nostr.json b/.well-known/nostr.json
new file mode 100644
index 0000000..4b45474
--- /dev/null
+++ b/.well-known/nostr.json
@@ -0,0 +1,96 @@
+{
+ "names": {
+ "GitCitadel": "846ebf79a0a8813274ec9727490621ad423f16a3e474d7fd66e6a98bfe4e39a4",
+ "ChipTuner": "036533caa872376946d4e4fdea4c1a0441eda38ca2d9d9417bb36006cbaabf58",
+ "ChipTunerDev": "011fb1f8cd1cacff53e2e0dc37a688c4dc4363a7c2d59fee16dd98d07601475a",
+ "buttercat1791": "70122128273bdc07af9be7725fa5c4bc0fc146866bec38d44360dc4bc6cc18b9",
+ "finrod": "ce1bf9ad92164df227bfcab2813193c60eb4021d35bf4bbbc6fa24c560d0f3e9",
+ "silberengel": "fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1",
+ "liminal": "dc4cd086cd7ce5b1832adf4fdd1211289880d2c7e295bcb0e684c01acee77c06",
+ "TheBeave": "0689df5847a8d3376892da29622d7c0fdc1ef1958f4bc4471d90966aa1eca9f2",
+ "LibertyGal": "8d34bd2432240c5637174a3db191878baa1c133aec739b64a264259f414be32b",
+ "testerin": "573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc",
+ "laeserin": "dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319",
+ "gc-publishing": "3e1ad0f3a5d3c12245db7788546c43ade3d97c6e046c594f6017cd6cd4164690",
+ "gc-devops": "d05e968d46c61307fae4b18aa5bf1d863b8d14d2e3ba089ed20123deb974b98f",
+ "nusa" : "d475ce4b3977507130f42c7f86346ef936800f3ae74d5ecf8089280cdc1923e9",
+ "matt": "fea186c2a4678dbc437704eed2160846e8a781e5fb17056e9bb333840d5bdef2"
+ },
+ "relays": {
+ "846ebf79a0a8813274ec9727490621ad423f16a3e474d7fd66e6a98bfe4e39a4": [
+ "wss://theforest.nostr1.com",
+ "wss://thecitadel.nostr1.com",
+ "wss://nostr.land"
+ ],
+ "036533caa872376946d4e4fdea4c1a0441eda38ca2d9d9417bb36006cbaabf58": [
+ "wss://theforest.nostr1.com",
+ "wss://thecitadel.nostr1.com",
+ "wss://nostr.land"
+ ],
+ "011fb1f8cd1cacff53e2e0dc37a688c4dc4363a7c2d59fee16dd98d07601475a": [
+ "wss://theforest.nostr1.com",
+ "wss://thecitadel.nostr1.com",
+ "wss://nostr.land"
+ ],
+ "70122128273bdc07af9be7725fa5c4bc0fc146866bec38d44360dc4bc6cc18b9": [
+ "wss://theforest.nostr1.com",
+ "wss://thecitadel.nostr1.com",
+ "wss://nostr.land"
+ ],
+ "ce1bf9ad92164df227bfcab2813193c60eb4021d35bf4bbbc6fa24c560d0f3e9": [
+ "wss://theforest.nostr1.com",
+ "wss://thecitadel.nostr1.com",
+ "wss://nostr.land"
+ ],
+ "fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1": [
+ "wss://theforest.nostr1.com",
+ "wss://thecitadel.nostr1.com",
+ "wss://nostr.land"
+ ],
+ "dc4cd086cd7ce5b1832adf4fdd1211289880d2c7e295bcb0e684c01acee77c06": [
+ "wss://theforest.nostr1.com",
+ "wss://thecitadel.nostr1.com",
+ "wss://nostr.land"
+ ],
+ "0689df5847a8d3376892da29622d7c0fdc1ef1958f4bc4471d90966aa1eca9f2": [
+ "wss://theforest.nostr1.com",
+ "wss://thecitadel.nostr1.com",
+ "wss://nostr.land"
+ ],
+ "8d34bd2432240c5637174a3db191878baa1c133aec739b64a264259f414be32b": [
+ "wss://theforest.nostr1.com",
+ "wss://thecitadel.nostr1.com",
+ "wss://nostr.land"
+ ],
+ "573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc": [
+ "wss://theforest.nostr1.com",
+ "wss://thecitadel.nostr1.com",
+ "wss://nostr.land"
+ ],
+ "dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319": [
+ "wss://theforest.nostr1.com",
+ "wss://thecitadel.nostr1.com",
+ "wss://nostr.land"
+ ],
+ "3e1ad0f3a5d3c12245db7788546c43ade3d97c6e046c594f6017cd6cd4164690": [
+ "wss://theforest.nostr1.com",
+ "wss://thecitadel.nostr1.com",
+ "wss://nostr.land"
+ ],
+ "d05e968d46c61307fae4b18aa5bf1d863b8d14d2e3ba089ed20123deb974b98f": [
+ "wss://theforest.nostr1.com",
+ "wss://thecitadel.nostr1.com",
+ "wss://nostr.land"
+ ],
+ "fea186c2a4678dbc437704eed2160846e8a781e5fb17056e9bb333840d5bdef2": [
+ "wss://theforest.nostr1.com",
+ "wss://thecitadel.nostr1.com",
+ "wss://nostr.land"
+ ],
+ "d475ce4b3977507130f42c7f86346ef936800f3ae74d5ecf8089280cdc1923e9": [
+ "wss://theforest.nostr1.com",
+ "wss://thecitadel.nostr1.com",
+ "wss://nostr.land"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/internal/asciidoc/processor.go b/internal/asciidoc/processor.go
index 12374ba..2a8501f 100644
--- a/internal/asciidoc/processor.go
+++ b/internal/asciidoc/processor.go
@@ -34,7 +34,10 @@ func (p *Processor) Process(asciidocContent string) (string, error) {
// Sanitize HTML to prevent XSS
sanitized := p.sanitizeHTML(html)
- return sanitized, nil
+ // Process links: make external links open in new tab, local links in same tab
+ processed := p.processLinks(sanitized)
+
+ return processed, nil
}
// rewriteLinks rewrites wikilinks and nostr: links in AsciiDoc content
@@ -185,3 +188,72 @@ func (p *Processor) sanitizeHTML(html string) string {
return html
}
+
+// processLinks processes HTML links to add target="_blank" to external links
+// External links are those that start with http:// or https:// and don't point to the linkBaseURL domain
+// Local links (including relative links and links to linkBaseURL) open in the same tab
+func (p *Processor) processLinks(html string) string {
+ // Extract domain from linkBaseURL for comparison
+ linkBaseDomain := ""
+ if strings.HasPrefix(p.linkBaseURL, "http://") || strings.HasPrefix(p.linkBaseURL, "https://") {
+ // Extract domain (e.g., "alexandria.gitcitadel.eu" from "https://alexandria.gitcitadel.eu")
+ parts := strings.Split(strings.TrimPrefix(strings.TrimPrefix(p.linkBaseURL, "https://"), "http://"), "/")
+ if len(parts) > 0 {
+ linkBaseDomain = parts[0]
+ }
+ }
+
+ // Regex to match tags with href attributes (more flexible pattern)
+ linkRegex := regexp.MustCompile(`]*?)href\s*=\s*["']([^"']+)["']([^>]*?)>`)
+
+ html = linkRegex.ReplaceAllStringFunc(html, func(match string) string {
+ // Extract href value
+ hrefMatch := regexp.MustCompile(`href\s*=\s*["']([^"']+)["']`)
+ hrefSubmatch := hrefMatch.FindStringSubmatch(match)
+ if len(hrefSubmatch) < 2 {
+ return match // No href found, return as-is
+ }
+ href := hrefSubmatch[1]
+
+ // Check if it's an external link (starts with http:// or https://)
+ isExternal := strings.HasPrefix(href, "http://") || strings.HasPrefix(href, "https://")
+
+ if isExternal {
+ // Check if it's pointing to our own domain
+ if linkBaseDomain != "" && strings.Contains(href, linkBaseDomain) {
+ // Same domain - open in same tab (remove any existing target attribute)
+ targetRegex := regexp.MustCompile(`\s*target\s*=\s*["'][^"']*["']`)
+ match = targetRegex.ReplaceAllString(match, "")
+ return match
+ }
+
+ // External link - add target="_blank" and rel="noopener noreferrer" if not already present
+ if !strings.Contains(match, `target=`) {
+ // Insert before the closing >
+ match = strings.TrimSuffix(match, ">")
+ if !strings.Contains(match, `rel=`) {
+ match += ` target="_blank" rel="noopener noreferrer">`
+ } else {
+ // Update existing rel attribute to include noopener if not present
+ relRegex := regexp.MustCompile(`rel\s*=\s*["']([^"']*)["']`)
+ match = relRegex.ReplaceAllStringFunc(match, func(relMatch string) string {
+ relValue := relRegex.FindStringSubmatch(relMatch)[1]
+ if !strings.Contains(relValue, "noopener") {
+ relValue += " noopener noreferrer"
+ }
+ return `rel="` + strings.TrimSpace(relValue) + `"`
+ })
+ match += ` target="_blank">`
+ }
+ }
+ } else {
+ // Local/relative link - ensure it opens in same tab (remove target if present)
+ targetRegex := regexp.MustCompile(`\s*target\s*=\s*["'][^"']*["']`)
+ match = targetRegex.ReplaceAllString(match, "")
+ }
+
+ return match
+ })
+
+ return html
+}
diff --git a/internal/generator/html.go b/internal/generator/html.go
index 5ef3750..a6c53a1 100644
--- a/internal/generator/html.go
+++ b/internal/generator/html.go
@@ -33,6 +33,14 @@ func getTemplateFuncs() template.FuncMap {
"shortenPubkey": func(pubkey string) string {
return nostr.ShortenPubkey(pubkey)
},
+ "pubkeyToNpub": func(pubkey string) string {
+ npub, err := nostr.PubkeyToNpub(pubkey)
+ if err != nil {
+ // Fallback to hex if conversion fails
+ return pubkey
+ }
+ return npub
+ },
"truncate": func(s string, maxLen int) string {
if len(s) <= maxLen {
return s
@@ -147,6 +155,7 @@ type ArticleItemInfo struct {
// FeedItemInfo represents info about a feed item
type FeedItemInfo struct {
+ EventID string
Author string
Content string
Time string
diff --git a/internal/nostr/feed.go b/internal/nostr/feed.go
index 14f62c8..653c32f 100644
--- a/internal/nostr/feed.go
+++ b/internal/nostr/feed.go
@@ -40,29 +40,29 @@ func (fs *FeedService) FetchFeedItems(ctx context.Context, feedRelay string, max
// Convert events to feed items
items := make([]FeedItem, 0, len(result.Events))
for _, event := range result.Events {
- item := FeedItem{
+ item := FeedItem{
EventID: event.ID,
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),
- }
+ }
- // Extract title, summary, and image tags
+ // Extract title, summary, and image tags
for _, tag := range event.Tags {
- if len(tag) > 0 && len(tag) > 1 {
- switch tag[0] {
- case "title":
- item.Title = tag[1]
- case "summary":
- item.Summary = tag[1]
- case "image":
- item.Image = tag[1]
+ if len(tag) > 0 && len(tag) > 1 {
+ switch tag[0] {
+ case "title":
+ item.Title = tag[1]
+ case "summary":
+ item.Summary = tag[1]
+ case "image":
+ item.Image = tag[1]
+ }
}
}
- }
- items = append(items, item)
+ items = append(items, item)
}
logger.WithFields(map[string]interface{}{
diff --git a/internal/server/server.go b/internal/server/server.go
index 0002947..d626b0a 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -105,6 +105,7 @@ func (s *Server) convertFeedItemsToInfo(items []cache.FeedItem) []generator.Feed
feedItems := make([]generator.FeedItemInfo, 0, len(items))
for _, item := range items {
feedItems = append(feedItems, generator.FeedItemInfo{
+ EventID: item.EventID,
Author: item.Author,
Content: item.Content,
Time: item.Time.Format("2006-01-02 15:04:05"),
diff --git a/server b/server
index 018a3c0..71fa4ea 100755
Binary files a/server and b/server differ
diff --git a/static/css/main.css b/static/css/main.css
index 68fa38b..4967f38 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -766,10 +766,18 @@ article {
}
.feed-item {
- padding: 1rem 0;
+ padding: 1.25rem 0;
border-bottom: 1px solid var(--border-color);
overflow-wrap: break-word;
word-wrap: break-word;
+ transition: background-color 0.2s;
+}
+
+.feed-item:hover {
+ background-color: var(--bg-primary);
+ margin: 0 -1.5rem;
+ padding: 1.25rem 1.5rem;
+ border-radius: 8px;
}
.feed-item:last-child {
@@ -780,13 +788,13 @@ article {
display: flex;
justify-content: space-between;
align-items: center;
- margin-bottom: 0.5rem;
+ margin-bottom: 0.75rem;
flex-wrap: wrap;
- gap: 0.5rem;
+ gap: 0.75rem;
}
.feed-author {
- font-weight: 600;
+ font-weight: 500;
color: var(--text-primary);
word-break: break-all;
overflow-wrap: anywhere;
@@ -794,9 +802,10 @@ article {
}
.feed-content {
- color: var(--text-secondary);
- margin-bottom: 0.5rem;
- font-size: 0.9rem;
+ color: var(--text-primary);
+ margin-bottom: 0.75rem;
+ font-size: 0.95rem;
+ line-height: 1.6;
overflow-wrap: break-word;
word-wrap: break-word;
}
@@ -1118,6 +1127,14 @@ footer {
padding: 1rem;
}
+.modal-options .btn,
+.modal-options a.btn {
+ text-decoration: none;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
.article-title {
font-size: 2.5rem;
margin: 0 0 0.5rem 0;
@@ -1529,6 +1546,61 @@ textarea:focus-visible {
margin-top: 2rem;
}
+/* Table of Contents */
+.table-of-contents {
+ margin-top: 2rem;
+ padding: 1.25rem;
+ background: var(--bg-secondary);
+ border-radius: 8px;
+ border: 1px solid var(--border-color);
+ font-size: 0.9rem;
+}
+
+.table-of-contents h2 {
+ font-size: 1.1rem;
+ margin-bottom: 1rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.table-of-contents ul {
+ margin: 0;
+ padding-left: 1.5rem;
+ list-style: none;
+}
+
+.table-of-contents ul ul {
+ margin-top: 0.25rem;
+ padding-left: 1.25rem;
+ border-left: 1px solid var(--border-color);
+}
+
+.table-of-contents ul ul ul {
+ padding-left: 1rem;
+}
+
+.table-of-contents li {
+ margin-bottom: 0.5rem;
+ line-height: 1.5;
+}
+
+.table-of-contents li:last-child {
+ margin-bottom: 0;
+}
+
+.table-of-contents a {
+ color: var(--link-color);
+ text-decoration: none;
+ word-break: break-word;
+ display: block;
+ padding: 0.25rem 0;
+}
+
+.table-of-contents a:hover {
+ text-decoration: underline;
+ color: var(--link-hover-color);
+}
+
.ebooks-page {
max-width: 1200px;
margin: 0 auto;
@@ -1643,6 +1715,14 @@ textarea:focus-visible {
border-radius: 4px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
+ text-decoration: none;
+ color: inherit;
+ transition: background-color 0.2s, border-color 0.2s;
+}
+
+.user-badge:hover {
+ background: var(--bg-primary);
+ border-color: var(--link-color);
}
.user-badge-avatar {
diff --git a/static/css/responsive.css b/static/css/responsive.css
index ac314d6..c27560e 100644
--- a/static/css/responsive.css
+++ b/static/css/responsive.css
@@ -508,7 +508,7 @@
.table-of-contents {
margin-top: 2rem;
- padding: 1rem;
+ padding: 1.25rem;
background: var(--bg-secondary);
border-radius: 8px;
border: 1px solid var(--border-color);
@@ -517,25 +517,47 @@
.table-of-contents h2 {
font-size: 1.1rem;
- margin-bottom: 0.75rem;
+ margin-bottom: 1rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid var(--border-color);
}
.table-of-contents ul {
- margin-left: 1rem;
+ margin: 0;
+ padding-left: 1.5rem;
+ list-style: none;
+ }
+
+ .table-of-contents ul ul {
+ margin-top: 0.25rem;
+ padding-left: 1.25rem;
+ border-left: 1px solid var(--border-color);
+ }
+
+ .table-of-contents ul ul ul {
+ padding-left: 1rem;
}
.table-of-contents li {
margin-bottom: 0.5rem;
+ line-height: 1.5;
+ }
+
+ .table-of-contents li:last-child {
+ margin-bottom: 0;
}
.table-of-contents a {
color: var(--link-color);
text-decoration: none;
word-break: break-word;
+ display: block;
+ padding: 0.25rem 0;
}
.table-of-contents a:hover {
text-decoration: underline;
+ color: var(--link-hover-color);
}
/* Feed */
diff --git a/templates/components.html b/templates/components.html
index 9331387..aecf2b4 100644
--- a/templates/components.html
+++ b/templates/components.html
@@ -11,7 +11,7 @@
{{else}}
@@ -102,7 +104,7 @@
{{if and $profile $profile.Name (not $profile.DisplayName)}}
@{{$profile.Name}}
{{end}}
-
+
{{end}}
{{/* Mobile Custom Dropdown Component - Shows title and profile pic
diff --git a/templates/contact.html b/templates/contact.html
index 5faae51..c3470c5 100644
--- a/templates/contact.html
+++ b/templates/contact.html
@@ -15,6 +15,11 @@
{{icon "github"}} GitHub: ShadowySupercode
+