From 3bdd01dedfcc170a96620beb34dc109030cf8456 Mon Sep 17 00:00:00 2001
From: bbe
Date: Tue, 23 Jun 2026 05:54:34 +0200
Subject: [PATCH] feat: user relay management, blog naddr sync, and OG image
update
Add UserRelay API and dashboard relays tab with NIP-65 import, store post
naddr for Nostr articles, and ship Prisma migrations for board tables,
user relays, and post metadata.
Co-authored-by: Cursor
---
.../migration.sql | 29 +++
.../migration.sql | 12 +
.../migration.sql | 5 +
backend/prisma/schema.prisma | 14 +-
backend/src/api/posts.ts | 78 ++++---
backend/src/api/relays.ts | 16 ++
backend/src/api/userRelays.ts | 137 +++++++++++
backend/src/index.ts | 2 +
backend/src/services/nostr.ts | 157 ++++++++-----
frontend/app/admin/blog/page.tsx | 71 ++++--
frontend/app/blog/[slug]/BlogPostClient.tsx | 51 +++-
frontend/app/blog/page.tsx | 8 +-
frontend/app/dashboard/page.tsx | 218 +++++++++++++++++-
frontend/app/layout.tsx | 6 +-
frontend/components/public/JsonLd.tsx | 4 +-
frontend/lib/api.ts | 21 +-
frontend/lib/nostr.ts | 199 +++++++++++++++-
frontend/public/og-default-old.png | Bin 0 -> 15886 bytes
frontend/public/og-default.png | Bin 15886 -> 1689170 bytes
19 files changed, 896 insertions(+), 132 deletions(-)
create mode 100644 backend/prisma/migrations/20260405073517_add_board_tables/migration.sql
create mode 100644 backend/prisma/migrations/20260405073752_add_user_relay/migration.sql
create mode 100644 backend/prisma/migrations/20260405081139_add_post_naddr/migration.sql
create mode 100644 backend/src/api/userRelays.ts
create mode 100644 frontend/public/og-default-old.png
diff --git a/backend/prisma/migrations/20260405073517_add_board_tables/migration.sql b/backend/prisma/migrations/20260405073517_add_board_tables/migration.sql
new file mode 100644
index 0000000..f19d688
--- /dev/null
+++ b/backend/prisma/migrations/20260405073517_add_board_tables/migration.sql
@@ -0,0 +1,29 @@
+-- CreateTable
+CREATE TABLE IF NOT EXISTS "BoardInvoicePending" (
+ "paymentHash" TEXT NOT NULL PRIMARY KEY,
+ "content" TEXT NOT NULL,
+ "guestName" TEXT,
+ "pubkey" TEXT,
+ "profilePic" TEXT,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+-- CreateTable
+CREATE TABLE IF NOT EXISTS "BoardMessage" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "paymentHash" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "authorName" TEXT NOT NULL,
+ "pubkey" TEXT,
+ "profilePic" TEXT,
+ "satsPaid" INTEGER NOT NULL,
+ "status" TEXT NOT NULL DEFAULT 'active',
+ "likeCount" INTEGER NOT NULL DEFAULT 0,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX IF NOT EXISTS "BoardMessage_paymentHash_key" ON "BoardMessage"("paymentHash");
+
+-- DropIndex (cleanup from organizer migration)
+DROP INDEX IF EXISTS "Meetup_organizerId_idx";
diff --git a/backend/prisma/migrations/20260405073752_add_user_relay/migration.sql b/backend/prisma/migrations/20260405073752_add_user_relay/migration.sql
new file mode 100644
index 0000000..ef04a1f
--- /dev/null
+++ b/backend/prisma/migrations/20260405073752_add_user_relay/migration.sql
@@ -0,0 +1,12 @@
+-- CreateTable
+CREATE TABLE "UserRelay" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "pubkey" TEXT NOT NULL,
+ "url" TEXT NOT NULL,
+ "read" BOOLEAN NOT NULL DEFAULT true,
+ "write" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "UserRelay_pubkey_url_key" ON "UserRelay"("pubkey", "url");
diff --git a/backend/prisma/migrations/20260405081139_add_post_naddr/migration.sql b/backend/prisma/migrations/20260405081139_add_post_naddr/migration.sql
new file mode 100644
index 0000000..48b9f5c
--- /dev/null
+++ b/backend/prisma/migrations/20260405081139_add_post_naddr/migration.sql
@@ -0,0 +1,5 @@
+-- AlterTable: add naddr column to Post
+ALTER TABLE "Post" ADD COLUMN "naddr" TEXT;
+
+-- AlterTable: set default for content on Post (new rows only; existing rows keep their content)
+-- SQLite doesn't support ALTER COLUMN DEFAULT, but Prisma handles this at the client level.
diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma
index d18b631..59e653a 100644
--- a/backend/prisma/schema.prisma
+++ b/backend/prisma/schema.prisma
@@ -61,9 +61,10 @@ model Media {
model Post {
id String @id @default(uuid())
nostrEventId String @unique
+ naddr String?
title String
slug String @unique
- content String
+ content String @default("")
excerpt String?
authorPubkey String
authorName String?
@@ -117,6 +118,17 @@ model Relay {
createdAt DateTime @default(now())
}
+model UserRelay {
+ id String @id @default(uuid())
+ pubkey String
+ url String
+ read Boolean @default(true)
+ write Boolean @default(true)
+ createdAt DateTime @default(now())
+
+ @@unique([pubkey, url])
+}
+
model Setting {
id String @id @default(uuid())
key String @unique
diff --git a/backend/src/api/posts.ts b/backend/src/api/posts.ts
index 8e738d8..b25d725 100644
--- a/backend/src/api/posts.ts
+++ b/backend/src/api/posts.ts
@@ -69,54 +69,72 @@ router.post(
requireRole(['ADMIN', 'MODERATOR']),
async (req: Request, res: Response) => {
try {
- const { eventId, naddr } = req.body;
- if (!eventId && !naddr) {
- res.status(400).json({ error: 'eventId or naddr is required' });
+ const { nostrEventId, naddr, title, excerpt, authorPubkey, publishedAt, tags } = req.body;
+
+ if (!nostrEventId || !title || !authorPubkey) {
+ res.status(400).json({ error: 'nostrEventId, title, and authorPubkey are required' });
return;
}
- let event: any = null;
- if (naddr) {
- event = await nostrService.fetchLongformEvent(naddr);
- } else if (eventId) {
- event = await nostrService.fetchEvent(eventId);
- }
-
- if (!event) {
- res.status(404).json({ error: 'Event not found on relays' });
- return;
- }
-
- const titleTag = event.tags?.find((t: string[]) => t[0] === 'title');
- const title = titleTag?.[1] || 'Untitled';
-
const slugBase = title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
- const slug = `${slugBase}-${event.id.slice(0, 8)}`;
-
- const excerpt = event.content.slice(0, 200).replace(/[#*_\n]/g, '').trim();
+ const slug = `${slugBase}-${nostrEventId.slice(0, 8)}`;
const post = await prisma.post.upsert({
- where: { nostrEventId: event.id },
+ where: { nostrEventId },
update: {
title,
- content: event.content,
- excerpt,
+ excerpt: excerpt || undefined,
+ naddr: naddr || undefined,
},
create: {
- nostrEventId: event.id,
+ nostrEventId,
+ naddr: naddr || null,
title,
slug,
- content: event.content,
- excerpt,
- authorPubkey: event.pubkey,
- publishedAt: new Date(event.created_at * 1000),
+ excerpt: excerpt || null,
+ authorPubkey,
+ publishedAt: publishedAt
+ ? new Date(typeof publishedAt === 'number' ? publishedAt * 1000 : publishedAt)
+ : new Date(),
},
});
- res.json(post);
+ if (Array.isArray(tags) && tags.length > 0) {
+ const categoryIds: string[] = [];
+ for (const tag of tags as string[]) {
+ const catSlug = tag.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
+ if (!catSlug) continue;
+ const category = await prisma.category.upsert({
+ where: { slug: catSlug },
+ update: {},
+ create: {
+ name: tag.charAt(0).toUpperCase() + tag.slice(1),
+ slug: catSlug,
+ },
+ });
+ categoryIds.push(category.id);
+ }
+
+ await prisma.postCategory.deleteMany({ where: { postId: post.id } });
+ if (categoryIds.length > 0) {
+ await prisma.postCategory.createMany({
+ data: categoryIds.map((categoryId) => ({
+ postId: post.id,
+ categoryId,
+ })),
+ });
+ }
+ }
+
+ const result = await prisma.post.findUnique({
+ where: { id: post.id },
+ include: { categories: { include: { category: true } } },
+ });
+
+ res.json(result);
} catch (err) {
console.error('Import post error:', err);
res.status(500).json({ error: 'Internal server error' });
diff --git a/backend/src/api/relays.ts b/backend/src/api/relays.ts
index ad89488..cb2586d 100644
--- a/backend/src/api/relays.ts
+++ b/backend/src/api/relays.ts
@@ -5,6 +5,22 @@ import { requireAuth, requireRole } from '../middleware/auth';
const router = Router();
+router.get(
+ '/public',
+ async (_req: Request, res: Response) => {
+ try {
+ const relays = await prisma.relay.findMany({
+ where: { active: true },
+ orderBy: { priority: 'asc' },
+ });
+ res.json({ relays: relays.map((r) => r.url) });
+ } catch (err) {
+ console.error('Public relays error:', err);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+ }
+);
+
router.get(
'/',
requireAuth,
diff --git a/backend/src/api/userRelays.ts b/backend/src/api/userRelays.ts
new file mode 100644
index 0000000..7d47673
--- /dev/null
+++ b/backend/src/api/userRelays.ts
@@ -0,0 +1,137 @@
+import { Router, Request, Response } from 'express';
+import { prisma } from '../db/prisma';
+import { requireAuth } from '../middleware/auth';
+import { nostrService } from '../services/nostr';
+
+const router = Router();
+
+router.get(
+ '/',
+ requireAuth,
+ async (req: Request, res: Response) => {
+ try {
+ const relays = await prisma.userRelay.findMany({
+ where: { pubkey: req.user!.pubkey },
+ orderBy: { createdAt: 'asc' },
+ });
+ res.json(relays);
+ } catch (err) {
+ console.error('List user relays error:', err);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+ }
+);
+
+router.post(
+ '/',
+ requireAuth,
+ async (req: Request, res: Response) => {
+ try {
+ const { url, read, write } = req.body;
+ if (!url || typeof url !== 'string') {
+ res.status(400).json({ error: 'url is required' });
+ return;
+ }
+
+ const normalized = url.trim().replace(/\/+$/, '');
+ if (!normalized.startsWith('wss://') && !normalized.startsWith('ws://')) {
+ res.status(400).json({ error: 'Relay URL must start with wss:// or ws://' });
+ return;
+ }
+
+ const relay = await prisma.userRelay.upsert({
+ where: {
+ pubkey_url: { pubkey: req.user!.pubkey, url: normalized },
+ },
+ update: {
+ read: read !== undefined ? read : true,
+ write: write !== undefined ? write : true,
+ },
+ create: {
+ pubkey: req.user!.pubkey,
+ url: normalized,
+ read: read !== undefined ? read : true,
+ write: write !== undefined ? write : true,
+ },
+ });
+
+ res.status(201).json(relay);
+ } catch (err) {
+ console.error('Add user relay error:', err);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+ }
+);
+
+router.delete(
+ '/:id',
+ requireAuth,
+ async (req: Request, res: Response) => {
+ try {
+ const relay = await prisma.userRelay.findUnique({
+ where: { id: req.params.id as string },
+ });
+ if (!relay || relay.pubkey !== req.user!.pubkey) {
+ res.status(404).json({ error: 'Relay not found' });
+ return;
+ }
+
+ await prisma.userRelay.delete({ where: { id: req.params.id as string } });
+ res.json({ success: true });
+ } catch (err) {
+ console.error('Delete user relay error:', err);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+ }
+);
+
+router.post(
+ '/import-nip65',
+ requireAuth,
+ async (req: Request, res: Response) => {
+ try {
+ const pubkey = req.user!.pubkey;
+ const nip65 = await nostrService.fetchNip65Relays(pubkey);
+
+ const allUrls = new Set();
+ const relayEntries: { url: string; read: boolean; write: boolean }[] = [];
+
+ for (const url of nip65.write) {
+ if (allUrls.has(url)) continue;
+ allUrls.add(url);
+ relayEntries.push({
+ url,
+ read: nip65.read.includes(url),
+ write: true,
+ });
+ }
+ for (const url of nip65.read) {
+ if (allUrls.has(url)) continue;
+ allUrls.add(url);
+ relayEntries.push({ url, read: true, write: false });
+ }
+
+ if (relayEntries.length === 0) {
+ res.json({ imported: 0, message: 'No NIP-65 relay list found for your pubkey' });
+ return;
+ }
+
+ let imported = 0;
+ for (const entry of relayEntries) {
+ await prisma.userRelay.upsert({
+ where: { pubkey_url: { pubkey, url: entry.url } },
+ update: { read: entry.read, write: entry.write },
+ create: { pubkey, url: entry.url, read: entry.read, write: entry.write },
+ });
+ imported++;
+ }
+
+ res.json({ imported, total: relayEntries.length });
+ } catch (err) {
+ console.error('Import NIP-65 error:', err);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+ }
+);
+
+export default router;
diff --git a/backend/src/index.ts b/backend/src/index.ts
index 8f9a921..aaa9890 100644
--- a/backend/src/index.ts
+++ b/backend/src/index.ts
@@ -24,6 +24,7 @@ import calendarRouter from './api/calendar';
import nip05Router from './api/nip05';
import messagesRouter from './api/messages';
import adminMessagesRouter from './api/adminMessages';
+import userRelaysRouter from './api/userRelays';
const app = express();
const PORT = parseInt(process.env.BACKEND_PORT || '4000', 10);
@@ -54,6 +55,7 @@ app.use('/api/calendar', calendarRouter);
app.use('/api/nip05', nip05Router);
app.use('/api/messages', messagesRouter);
app.use('/api/admin/messages', adminMessagesRouter);
+app.use('/api/user-relays', userRelaysRouter);
app.get('/api/health', (_req, res) => {
res.json({ status: 'ok' });
diff --git a/backend/src/services/nostr.ts b/backend/src/services/nostr.ts
index 3907cef..7aede6e 100644
--- a/backend/src/services/nostr.ts
+++ b/backend/src/services/nostr.ts
@@ -11,7 +11,62 @@ async function getRelayUrls(): Promise {
return relays.map((r) => r.url);
}
+function dedupeRelays(...lists: string[][]): string[] {
+ return [...new Set(lists.flat().filter(Boolean))];
+}
+
+async function cacheEvent(event: any) {
+ await prisma.nostrEventCache.upsert({
+ where: { eventId: event.id },
+ update: {
+ content: event.content,
+ tags: JSON.stringify(event.tags),
+ },
+ create: {
+ eventId: event.id,
+ kind: event.kind,
+ pubkey: event.pubkey,
+ content: event.content,
+ tags: JSON.stringify(event.tags),
+ createdAt: event.created_at,
+ },
+ });
+}
+
export const nostrService = {
+ async fetchNip65Relays(pubkey: string): Promise<{ write: string[]; read: string[] }> {
+ const siteRelays = await getRelayUrls();
+ if (siteRelays.length === 0) return { write: [], read: [] };
+
+ try {
+ const event = await pool.get(siteRelays, {
+ kinds: [10002],
+ authors: [pubkey],
+ });
+ if (!event) return { write: [], read: [] };
+
+ const write: string[] = [];
+ const read: string[] = [];
+ for (const tag of event.tags) {
+ if (tag[0] !== 'r' || !tag[1]) continue;
+ const url = tag[1];
+ const marker = tag[2];
+ if (marker === 'write') {
+ write.push(url);
+ } else if (marker === 'read') {
+ read.push(url);
+ } else {
+ write.push(url);
+ read.push(url);
+ }
+ }
+ return { write, read };
+ } catch (err) {
+ console.error('Failed to fetch NIP-65 relays for', pubkey, err);
+ return { write: [], read: [] };
+ }
+ },
+
async fetchEvent(eventId: string, skipCache = false) {
if (!skipCache) {
const cached = await prisma.nostrEventCache.findUnique({
@@ -29,34 +84,20 @@ export const nostrService = {
}
}
- const relays = await getRelayUrls();
- if (relays.length === 0) return null;
+ const siteRelays = await getRelayUrls();
+ if (siteRelays.length === 0) return null;
try {
- const event = await pool.get(relays, { ids: [eventId] });
- if (!event) return null;
-
- await prisma.nostrEventCache.upsert({
- where: { eventId: event.id },
- update: {
- content: event.content,
- tags: JSON.stringify(event.tags),
- },
- create: {
- eventId: event.id,
- kind: event.kind,
- pubkey: event.pubkey,
- content: event.content,
- tags: JSON.stringify(event.tags),
- createdAt: event.created_at,
- },
- });
-
- return event;
+ const event = await pool.get(siteRelays, { ids: [eventId] });
+ if (event) {
+ await cacheEvent(event);
+ return event;
+ }
} catch (err) {
- console.error('Failed to fetch event:', err);
- return null;
+ console.error('Failed to fetch event from site relays:', err);
}
+
+ return null;
},
async fetchLongformEvent(naddrStr: string) {
@@ -69,40 +110,44 @@ export const nostrService = {
return null;
}
- const relays = decoded.relays?.length
- ? decoded.relays
- : await getRelayUrls();
- if (relays.length === 0) return null;
+ const siteRelays = await getRelayUrls();
+ const naddrRelays = decoded.relays || [];
+ const filter = {
+ kinds: [decoded.kind],
+ authors: [decoded.pubkey],
+ '#d': [decoded.identifier],
+ };
- try {
- const event = await pool.get(relays, {
- kinds: [decoded.kind],
- authors: [decoded.pubkey],
- '#d': [decoded.identifier],
- });
- if (!event) return null;
-
- await prisma.nostrEventCache.upsert({
- where: { eventId: event.id },
- update: {
- content: event.content,
- tags: JSON.stringify(event.tags),
- },
- create: {
- eventId: event.id,
- kind: event.kind,
- pubkey: event.pubkey,
- content: event.content,
- tags: JSON.stringify(event.tags),
- createdAt: event.created_at,
- },
- });
-
- return event;
- } catch (err) {
- console.error('Failed to fetch longform event:', err);
- return null;
+ // Try naddr-embedded relays first, then site relays
+ const firstTry = dedupeRelays(naddrRelays, siteRelays);
+ if (firstTry.length > 0) {
+ try {
+ const event = await pool.get(firstTry, filter);
+ if (event) {
+ await cacheEvent(event);
+ return event;
+ }
+ } catch (err) {
+ console.error('Failed to fetch longform event (first pass):', err);
+ }
}
+
+ // Fallback: try author's NIP-65 write relays
+ const nip65 = await nostrService.fetchNip65Relays(decoded.pubkey);
+ if (nip65.write.length > 0) {
+ const nip65Relays = dedupeRelays(nip65.write);
+ try {
+ const event = await pool.get(nip65Relays, filter);
+ if (event) {
+ await cacheEvent(event);
+ return event;
+ }
+ } catch (err) {
+ console.error('Failed to fetch longform event (NIP-65 fallback):', err);
+ }
+ }
+
+ return null;
},
async fetchReactions(eventId: string) {
diff --git a/frontend/app/admin/blog/page.tsx b/frontend/app/admin/blog/page.tsx
index e29710a..c05bba0 100644
--- a/frontend/app/admin/blog/page.tsx
+++ b/frontend/app/admin/blog/page.tsx
@@ -3,6 +3,7 @@
import { useEffect, useState } from "react";
import { api } from "@/lib/api";
import { cn } from "@/lib/utils";
+import { fetchEventFromRelays, fetchLongformFromRelays } from "@/lib/nostr";
import {
Pencil,
Trash2,
@@ -56,13 +57,24 @@ export default function BlogPage() {
setFetching(true);
setError("");
try {
- const isNaddr = importInput.startsWith("naddr");
- const data = await api.fetchNostrEvent(
- isNaddr ? { naddr: importInput } : { eventId: importInput }
- );
- setImportPreview(data);
+ const isNaddr = importInput.trim().startsWith("naddr");
+ const event = isNaddr
+ ? await fetchLongformFromRelays(importInput.trim())
+ : await fetchEventFromRelays(importInput.trim());
+
+ if (!event) {
+ setError("Event not found on relays");
+ setImportPreview(null);
+ return;
+ }
+
+ const titleTag = event.tags?.find((t: string[]) => t[0] === "title");
+ setImportPreview({
+ ...event,
+ title: titleTag?.[1] || "Untitled",
+ });
} catch (err: any) {
- setError(err.message);
+ setError(err.message || "Failed to fetch event");
setImportPreview(null);
} finally {
setFetching(false);
@@ -70,14 +82,29 @@ export default function BlogPage() {
};
const handleImport = async () => {
- if (!importInput.trim()) return;
+ if (!importPreview) return;
setImporting(true);
setError("");
try {
- const isNaddr = importInput.startsWith("naddr");
- await api.importPost(
- isNaddr ? { naddr: importInput } : { eventId: importInput }
- );
+ const isNaddr = importInput.trim().startsWith("naddr");
+ const excerpt = (importPreview.content || "")
+ .slice(0, 200)
+ .replace(/[#*_\n]/g, "")
+ .trim();
+
+ const tags: string[] = (importPreview.tags || [])
+ .filter((t: string[]) => t[0] === "t" && t[1])
+ .map((t: string[]) => t[1].toLowerCase());
+
+ await api.importPost({
+ nostrEventId: importPreview.id,
+ naddr: isNaddr ? importInput.trim() : undefined,
+ title: importPreview.title || "Untitled",
+ excerpt: excerpt || undefined,
+ authorPubkey: importPreview.pubkey,
+ publishedAt: importPreview.created_at,
+ tags: tags.length > 0 ? tags : undefined,
+ });
setImportInput("");
setImportPreview(null);
setImportOpen(false);
@@ -95,7 +122,7 @@ export default function BlogPage() {
title: post.title || "",
slug: post.slug || "",
excerpt: post.excerpt || "",
- categories: post.categories?.map((c: any) => c.id || c) || [],
+ categories: post.categories?.map((c: any) => c.categoryId || c.category?.id || c) || [],
featured: post.featured || false,
visible: post.visible !== false,
});
@@ -184,6 +211,20 @@ export default function BlogPage() {
{importPreview.content?.slice(0, 300)}...
+ {(() => {
+ const previewTags = (importPreview.tags || [])
+ .filter((t: string[]) => t[0] === "t" && t[1])
+ .map((t: string[]) => t[1]);
+ return previewTags.length > 0 ? (
+
+ {previewTags.map((tag: string) => (
+
+ {tag}
+
+ ))}
+
+ ) : null;
+ })()}
/{post.slug}
{post.categories?.length > 0 && (
- {post.categories.map((cat: any) => (
+ {post.categories.map((pc: any) => (
- {cat.name || cat}
+ {pc.category?.name || pc.categoryId}
))}
diff --git a/frontend/app/blog/[slug]/BlogPostClient.tsx b/frontend/app/blog/[slug]/BlogPostClient.tsx
index 1a6515a..1bcf32e 100644
--- a/frontend/app/blog/[slug]/BlogPostClient.tsx
+++ b/frontend/app/blog/[slug]/BlogPostClient.tsx
@@ -7,7 +7,7 @@ import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { api } from "@/lib/api";
import { formatDate } from "@/lib/utils";
-import { hasNostrExtension, getPublicKey, signEvent, publishEvent, shortenPubkey, fetchNostrProfile, type NostrProfile } from "@/lib/nostr";
+import { hasNostrExtension, getPublicKey, signEvent, publishEvent, shortenPubkey, fetchNostrProfile, fetchLongformFromRelays, fetchEventFromRelays, type NostrProfile } from "@/lib/nostr";
import { Navbar } from "@/components/public/Navbar";
import { Footer } from "@/components/public/Footer";
import type { Components } from "react-markdown";
@@ -23,6 +23,7 @@ interface Post {
publishedAt?: string;
createdAt?: string;
nostrEventId?: string;
+ naddr?: string;
categories?: { category: { id: string; name: string; slug: string } }[];
}
@@ -147,6 +148,8 @@ export default function BlogPostClient({ slug }: { slug: string }) {
const [hasNostr, setHasNostr] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [authorProfile, setAuthorProfile] = useState(null);
+ const [liveContent, setLiveContent] = useState(null);
+ const [loadingContent, setLoadingContent] = useState(false);
useEffect(() => {
setHasNostr(hasNostrExtension());
@@ -156,18 +159,36 @@ export default function BlogPostClient({ slug }: { slug: string }) {
if (!slug) return;
setLoading(true);
setError(null);
+ setLiveContent(null);
api
.getPost(slug)
.then((data) => {
setPost(data);
+ setLoading(false);
if (data?.authorPubkey) {
fetchNostrProfile(data.authorPubkey)
.then((profile) => setAuthorProfile(profile))
.catch(() => {});
}
+ if (!data?.content && (data?.naddr || data?.nostrEventId)) {
+ setLoadingContent(true);
+ const fetchPromise = data.naddr
+ ? fetchLongformFromRelays(data.naddr)
+ : fetchEventFromRelays(data.nostrEventId!);
+ fetchPromise
+ .then((event) => {
+ if (event?.content) {
+ setLiveContent(event.content);
+ }
+ })
+ .catch(() => {})
+ .finally(() => setLoadingContent(false));
+ }
})
- .catch((err) => setError(err.message))
- .finally(() => setLoading(false));
+ .catch((err) => {
+ setError(err.message);
+ setLoading(false);
+ });
}, [slug]);
useEffect(() => {
@@ -306,12 +327,24 @@ export default function BlogPostClient({ slug }: { slug: string }) {
-
- {post.content}
-
+ {loadingContent ? (
+
+ {[85, 92, 78, 95, 88, 72, 90, 83].map((w, i) => (
+
+ ))}
+
+ ) : (
+
+ {post.content || liveContent || ""}
+
+ )}
diff --git a/frontend/app/blog/page.tsx b/frontend/app/blog/page.tsx
index c159f58..2827568 100644
--- a/frontend/app/blog/page.tsx
+++ b/frontend/app/blog/page.tsx
@@ -18,7 +18,7 @@ interface Post {
authorPubkey?: string;
publishedAt?: string;
createdAt?: string;
- categories?: { id: string; name: string; slug: string }[];
+ categories?: { category: { id: string; name: string; slug: string } }[];
featured?: boolean;
}
@@ -212,12 +212,12 @@ export default function BlogPage() {
>
{post.categories && post.categories.length > 0 && (
- {post.categories.map((cat) => (
+ {post.categories.map((pc) => (
- {cat.name}
+ {pc.category.name}
))}
diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx
index e7bd8c9..8b8453a 100644
--- a/frontend/app/dashboard/page.tsx
+++ b/frontend/app/dashboard/page.tsx
@@ -2,7 +2,7 @@
import { useState, useEffect, useCallback, useRef } from "react";
import Image from "next/image";
-import { Send, FileText, Clock, CheckCircle, XCircle, Plus, User, Loader2, AtSign } from "lucide-react";
+import { Send, FileText, Clock, CheckCircle, XCircle, Plus, User, Loader2, AtSign, Radio, Trash2, Download, Eye, Pencil } from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
import { api } from "@/lib/api";
import { shortenPubkey } from "@/lib/nostr";
@@ -37,7 +37,15 @@ const STATUS_CONFIG: Record | null>(null);
+ // Relay state
+ const [userRelays, setUserRelays] = useState([]);
+ const [loadingRelays, setLoadingRelays] = useState(true);
+ const [newRelayUrl, setNewRelayUrl] = useState("");
+ const [addingRelay, setAddingRelay] = useState(false);
+ const [importingNip65, setImportingNip65] = useState(false);
+ const [relayError, setRelayError] = useState("");
+ const [relaySuccess, setRelaySuccess] = useState("");
+
const displayName = user?.name || user?.displayName || shortenPubkey(user?.pubkey || "");
useEffect(() => {
@@ -92,9 +109,21 @@ export default function DashboardPage() {
}
}, []);
+ const loadRelays = useCallback(async () => {
+ try {
+ const data = await api.getUserRelays();
+ setUserRelays(data);
+ } catch {
+ // Silently handle
+ } finally {
+ setLoadingRelays(false);
+ }
+ }, []);
+
useEffect(() => {
loadSubmissions();
- }, [loadSubmissions]);
+ loadRelays();
+ }, [loadSubmissions, loadRelays]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -130,6 +159,63 @@ export default function DashboardPage() {
}
};
+ const handleAddRelay = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setRelayError("");
+ setRelaySuccess("");
+
+ const url = newRelayUrl.trim();
+ if (!url) {
+ setRelayError("Relay URL is required");
+ return;
+ }
+ if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
+ setRelayError("URL must start with wss:// or ws://");
+ return;
+ }
+
+ setAddingRelay(true);
+ try {
+ await api.addUserRelay({ url });
+ setNewRelayUrl("");
+ setRelaySuccess("Relay added");
+ await loadRelays();
+ } catch (err: any) {
+ setRelayError(err.message || "Failed to add relay");
+ } finally {
+ setAddingRelay(false);
+ }
+ };
+
+ const handleRemoveRelay = async (id: string) => {
+ setRelayError("");
+ try {
+ await api.removeUserRelay(id);
+ setUserRelays((prev) => prev.filter((r) => r.id !== id));
+ } catch (err: any) {
+ setRelayError(err.message || "Failed to remove relay");
+ }
+ };
+
+ const handleImportNip65 = async () => {
+ setRelayError("");
+ setRelaySuccess("");
+ setImportingNip65(true);
+ try {
+ const result = await api.importNip65Relays();
+ if (result.imported > 0) {
+ setRelaySuccess(`Imported ${result.imported} relay(s) from your NIP-65 list`);
+ await loadRelays();
+ } else {
+ setRelayError(result.message || "No NIP-65 relay list found for your pubkey");
+ }
+ } catch (err: any) {
+ setRelayError(err.message || "Failed to import NIP-65 relays");
+ } finally {
+ setImportingNip65(false);
+ }
+ };
+
const handleUsernameChange = (value: string) => {
setUsername(value);
setSaveError("");
@@ -247,6 +333,17 @@ export default function DashboardPage() {
Profile
+ setActiveTab("relays")}
+ className={`flex items-center gap-2 px-4 py-3 text-sm font-semibold border-b-2 transition-colors ${
+ activeTab === "relays"
+ ? "border-primary text-primary"
+ : "border-transparent text-on-surface-variant hover:text-on-surface"
+ }`}
+ >
+
+ Relays
+
{/* Submissions tab */}
@@ -516,6 +613,121 @@ export default function DashboardPage() {
)}
+
+ {/* Relays tab */}
+ {activeTab === "relays" && (
+
+ Your Relays
+
+ Manage the Nostr relays used for your interactions on this site.
+ These relays are used when publishing events and fetching your content.
+ You can also import your relay list from your Nostr profile (NIP-65).
+
+
+ {relaySuccess && (
+
+ {relaySuccess}
+
+ )}
+ {relayError && (
+
+ {relayError}
+
+ )}
+
+
+
+
+
+ {importingNip65 ? (
+
+ ) : (
+
+ )}
+ {importingNip65 ? "Importing…" : "Import from NIP-65"}
+
+
+
+
+ {loadingRelays ? (
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+ ) : userRelays.length === 0 ? (
+
+
+
+ No relays configured. Add a relay manually or import from your NIP-65 profile.
+
+
+ ) : (
+
+ {userRelays.map((relay) => (
+
+
+
+ {relay.url}
+
+
+ {relay.read && (
+
+
+ Read
+
+ )}
+ {relay.write && (
+
+
+ Write
+
+ )}
+
+
+
handleRemoveRelay(relay.id)}
+ className="text-on-surface-variant/50 hover:text-error transition-colors p-2 rounded-lg hover:bg-error/10"
+ title="Remove relay"
+ >
+
+
+
+ ))}
+
+ )}
+
+ )}
);
}
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index f93a1f2..60fcf37 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -23,7 +23,7 @@ export const metadata: Metadata = {
template: "%s | Belgian Bitcoin Embassy",
},
description:
- "Belgium's sovereign Bitcoin community. Monthly meetups in Antwerp, Bitcoin education, and curated Nostr content. No hype, just signal.",
+ "Discover Bitcoin meetups across Belgium. Real conversations, education, and a strong local community.",
keywords: [
"Bitcoin",
"Belgium",
@@ -44,7 +44,7 @@ export const metadata: Metadata = {
siteName: "Belgian Bitcoin Embassy",
title: "Belgian Bitcoin Embassy | Bitcoin Meetups & Education in Belgium",
description:
- "Belgium's sovereign Bitcoin community. Monthly meetups, education, and curated Nostr content.",
+ "Discover Bitcoin meetups across Belgium. Real conversations, education, and a strong local community.",
images: [
{
url: "/og-default.png",
@@ -58,7 +58,7 @@ export const metadata: Metadata = {
card: "summary_large_image",
title: "Belgian Bitcoin Embassy",
description:
- "Belgium's sovereign Bitcoin community. Monthly meetups, education, and curated Nostr content.",
+ "Discover Bitcoin meetups across Belgium. Real conversations, education, and a strong local community.",
images: ["/og-default.png"],
},
robots: {
diff --git a/frontend/components/public/JsonLd.tsx b/frontend/components/public/JsonLd.tsx
index cceb275..2505079 100644
--- a/frontend/components/public/JsonLd.tsx
+++ b/frontend/components/public/JsonLd.tsx
@@ -24,7 +24,7 @@ export function OrganizationJsonLd() {
url: siteUrl,
logo: `${siteUrl}/og-default.png`,
description:
- "Belgium's sovereign Bitcoin community. Monthly meetups, education, and curated Nostr content.",
+ "Discover Bitcoin meetups across Belgium. Real conversations, education, and a strong local community.",
sameAs: ["https://t.me/belgianbitcoinembassy"],
address: {
"@type": "PostalAddress",
@@ -45,7 +45,7 @@ export function WebSiteJsonLd() {
name: "Belgian Bitcoin Embassy",
url: siteUrl,
description:
- "Belgium's sovereign Bitcoin community. Monthly meetups, education, and curated Nostr content.",
+ "Discover Bitcoin meetups across Belgium. Real conversations, education, and a strong local community.",
publisher: {
"@type": "Organization",
name: "Belgian Bitcoin Embassy",
diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts
index e58b680..1ddbff8 100644
--- a/frontend/lib/api.ts
+++ b/frontend/lib/api.ts
@@ -43,7 +43,15 @@ export const api = {
request<{ count: number; reactions: any[] }>(`/posts/${slug}/reactions`),
getPostReplies: (slug: string) =>
request<{ count: number; replies: any[] }>(`/posts/${slug}/replies`),
- importPost: (data: { eventId?: string; naddr?: string }) =>
+ importPost: (data: {
+ nostrEventId: string;
+ naddr?: string;
+ title: string;
+ excerpt?: string;
+ authorPubkey: string;
+ publishedAt?: number;
+ tags?: string[];
+ }) =>
request("/posts/import", { method: "POST", body: JSON.stringify(data) }),
updatePost: (id: string, data: any) =>
request(`/posts/${id}`, { method: "PATCH", body: JSON.stringify(data) }),
@@ -116,6 +124,7 @@ export const api = {
request(`/organizers/${id}`, { method: "DELETE" }),
// Relays
+ getPublicRelays: () => request<{ relays: string[] }>("/relays/public"),
getRelays: () => request("/relays"),
addRelay: (data: { url: string; priority?: number }) =>
request("/relays", { method: "POST", body: JSON.stringify(data) }),
@@ -126,6 +135,16 @@ export const api = {
testRelay: (id: string) =>
request<{ success: boolean }>(`/relays/${id}/test`, { method: "POST" }),
+ // User Relays
+ getUserRelays: () =>
+ request("/user-relays"),
+ addUserRelay: (data: { url: string; read?: boolean; write?: boolean }) =>
+ request("/user-relays", { method: "POST", body: JSON.stringify(data) }),
+ removeUserRelay: (id: string) =>
+ request(`/user-relays/${id}`, { method: "DELETE" }),
+ importNip65Relays: () =>
+ request<{ imported: number; total?: number; message?: string }>("/user-relays/import-nip65", { method: "POST" }),
+
// Settings
getSettings: () => request>("/settings"),
getPublicSettings: () => request>("/settings/public"),
diff --git a/frontend/lib/nostr.ts b/frontend/lib/nostr.ts
index 2387d3c..3fe1ee0 100644
--- a/frontend/lib/nostr.ts
+++ b/frontend/lib/nostr.ts
@@ -56,24 +56,143 @@ export interface NostrProfile {
displayName?: string;
}
-const DEFAULT_RELAYS = [
+const FALLBACK_RELAYS = [
"wss://relay.damus.io",
"wss://nos.lol",
"wss://relay.nostr.band",
];
+let _siteRelaysCache: { relays: string[]; fetchedAt: number } | null = null;
+const SITE_RELAY_TTL = 5 * 60 * 1000; // 5 minutes
+
+export async function getSiteRelays(): Promise {
+ if (_siteRelaysCache && Date.now() - _siteRelaysCache.fetchedAt < SITE_RELAY_TTL) {
+ return _siteRelaysCache.relays;
+ }
+ try {
+ const res = await fetch("/api/relays/public");
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const data = await res.json();
+ if (Array.isArray(data.relays) && data.relays.length > 0) {
+ _siteRelaysCache = { relays: data.relays, fetchedAt: Date.now() };
+ return data.relays;
+ }
+ } catch {}
+ return FALLBACK_RELAYS;
+}
+
+export interface Nip65RelayList {
+ write: string[];
+ read: string[];
+ all: string[];
+}
+
+const _nip65Cache = new Map();
+const NIP65_TTL = 10 * 60 * 1000; // 10 minutes
+
+export async function fetchNip65RelayList(pubkey: string): Promise {
+ const cached = _nip65Cache.get(pubkey);
+ if (cached && Date.now() - cached.fetchedAt < NIP65_TTL) return cached.data;
+
+ const { SimplePool } = await import("nostr-tools/pool");
+ const siteRelays = await getSiteRelays();
+ const pool = new SimplePool();
+
+ try {
+ const event = await pool.get(siteRelays, {
+ kinds: [10002],
+ authors: [pubkey],
+ });
+
+ const result: Nip65RelayList = { write: [], read: [], all: [] };
+ if (!event) {
+ _nip65Cache.set(pubkey, { data: result, fetchedAt: Date.now() });
+ return result;
+ }
+
+ for (const tag of event.tags) {
+ if (tag[0] !== "r" || !tag[1]) continue;
+ const url = tag[1];
+ const marker = tag[2];
+ result.all.push(url);
+ if (marker === "write") {
+ result.write.push(url);
+ } else if (marker === "read") {
+ result.read.push(url);
+ } else {
+ result.write.push(url);
+ result.read.push(url);
+ }
+ }
+
+ _nip65Cache.set(pubkey, { data: result, fetchedAt: Date.now() });
+ return result;
+ } catch {
+ return { write: [], read: [], all: [] };
+ } finally {
+ pool.close(siteRelays);
+ }
+}
+
+let _userRelaysCache: { relays: { url: string; read: boolean; write: boolean }[]; fetchedAt: number } | null = null;
+const USER_RELAY_TTL = 2 * 60 * 1000; // 2 minutes
+
+export async function getUserStoredRelays(): Promise<{ url: string; read: boolean; write: boolean }[]> {
+ if (_userRelaysCache && Date.now() - _userRelaysCache.fetchedAt < USER_RELAY_TTL) {
+ return _userRelaysCache.relays;
+ }
+ try {
+ const token = typeof window !== "undefined" ? localStorage.getItem("bbe_token") : null;
+ if (!token) return [];
+ const res = await fetch("/api/user-relays", {
+ headers: { Authorization: `Bearer ${token}` },
+ });
+ if (!res.ok) return [];
+ const data = await res.json();
+ if (Array.isArray(data)) {
+ _userRelaysCache = { relays: data, fetchedAt: Date.now() };
+ return data;
+ }
+ } catch {}
+ return [];
+}
+
+export function clearUserRelaysCache(): void {
+ _userRelaysCache = null;
+}
+
export async function publishEvent(signedEvent: any): Promise {
const { SimplePool } = await import("nostr-tools/pool");
- let relayUrls: string[] = DEFAULT_RELAYS;
+ const siteRelays = await getSiteRelays();
+ const allRelays = new Set(siteRelays);
+
+ // Merge user's stored write relays
+ try {
+ const stored = await getUserStoredRelays();
+ stored
+ .filter((r) => r.write)
+ .forEach((r) => allRelays.add(r.url));
+ } catch {}
+
+ // Merge NIP-65 write relays for the event author
+ if (signedEvent.pubkey) {
+ try {
+ const nip65 = await fetchNip65RelayList(signedEvent.pubkey);
+ nip65.write.forEach((url) => allRelays.add(url));
+ } catch {}
+ }
+
+ // Merge extension relays
try {
if (window.nostr?.getRelays) {
const ext = await window.nostr.getRelays();
- const write = Object.entries(ext)
+ Object.entries(ext)
.filter(([, p]) => (p as any).write)
- .map(([url]) => url);
- if (write.length > 0) relayUrls = write;
+ .forEach(([url]) => allRelays.add(url));
}
} catch {}
+
+ const relayUrls = [...allRelays];
const pool = new SimplePool();
try {
await Promise.allSettled(pool.publish(relayUrls, signedEvent));
@@ -84,13 +203,21 @@ export async function publishEvent(signedEvent: any): Promise {
export async function fetchNostrProfile(
pubkey: string,
- relayUrls: string[] = DEFAULT_RELAYS
+ relayUrls?: string[]
): Promise {
const { SimplePool } = await import("nostr-tools/pool");
+ const allRelays = new Set(relayUrls || await getSiteRelays());
+
+ try {
+ const nip65 = await fetchNip65RelayList(pubkey);
+ nip65.write.forEach((url) => allRelays.add(url));
+ } catch {}
+
+ const urls = [...allRelays];
const pool = new SimplePool();
try {
- const event = await pool.get(relayUrls, {
+ const event = await pool.get(urls, {
kinds: [0],
authors: [pubkey],
});
@@ -107,6 +234,61 @@ export async function fetchNostrProfile(
};
} catch {
return {};
+ } finally {
+ pool.close(urls);
+ }
+}
+
+export async function fetchEventFromRelays(eventId: string): Promise {
+ const { SimplePool } = await import("nostr-tools/pool");
+ const siteRelays = await getSiteRelays();
+ const pool = new SimplePool();
+
+ try {
+ const event = await pool.get(siteRelays, { ids: [eventId] });
+ return event || null;
+ } catch {
+ return null;
+ } finally {
+ pool.close(siteRelays);
+ }
+}
+
+export async function fetchLongformFromRelays(naddrStr: string): Promise {
+ const { SimplePool } = await import("nostr-tools/pool");
+ const { nip19 } = await import("nostr-tools");
+
+ let decoded: { kind: number; pubkey: string; identifier: string; relays?: string[] };
+ try {
+ const result = nip19.decode(naddrStr);
+ if (result.type !== "naddr") return null;
+ decoded = result.data;
+ } catch {
+ return null;
+ }
+
+ const siteRelays = await getSiteRelays();
+ const naddrRelays = decoded.relays || [];
+ const allRelays = new Set([...naddrRelays, ...siteRelays]);
+
+ try {
+ const nip65 = await fetchNip65RelayList(decoded.pubkey);
+ nip65.write.forEach((url) => allRelays.add(url));
+ } catch {}
+
+ const relayUrls = [...allRelays];
+ const filter = {
+ kinds: [decoded.kind],
+ authors: [decoded.pubkey],
+ "#d": [decoded.identifier],
+ };
+
+ const pool = new SimplePool();
+ try {
+ const event = await pool.get(relayUrls, filter);
+ return event || null;
+ } catch {
+ return null;
} finally {
pool.close(relayUrls);
}
@@ -128,8 +310,9 @@ export async function createBunkerSigner(
// NIP-46: Generate a nostrconnect:// URI for QR display
export async function generateNostrConnectSetup(
- relayUrls: string[] = DEFAULT_RELAYS.slice(0, 2)
+ relayUrls?: string[]
): Promise<{ uri: string; clientSecretKey: Uint8Array }> {
+ if (!relayUrls) relayUrls = (await getSiteRelays()).slice(0, 2);
const { createNostrConnectURI } = await import("nostr-tools/nip46");
const clientSecretKey = generateSecretKey();
const clientPubkey = getPubKeyFromSecret(clientSecretKey);
diff --git a/frontend/public/og-default-old.png b/frontend/public/og-default-old.png
new file mode 100644
index 0000000000000000000000000000000000000000..0f2d48d0354c43e4da83acef03d9b6ecd27df574
GIT binary patch
literal 15886
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
jl;zjPKQfp`$R1Ew``^^~P|5#`%HhAI
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