diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx
index 2604a73..236580b 100644
--- a/src/AppRouter.tsx
+++ b/src/AppRouter.tsx
@@ -3,9 +3,9 @@ import { ScrollToTop } from "./components/ScrollToTop";
import { BlogLayout } from "./components/BlogLayout";
import BlogHomePage from "./pages/BlogHomePage";
-import BlogPostPage from "./pages/BlogPostPage";
import CreatePostPage from "./pages/CreatePostPage";
import EditPostPage from "./pages/EditPostPage";
+import { NIP19Page } from "./pages/NIP19Page";
import NotFound from "./pages/NotFound";
export function AppRouter() {
@@ -17,8 +17,8 @@ export function AppRouter() {
} />
} />
} />
- {/* NIP-19 route for naddr1 blog posts */}
- } />
+ {/* NIP-19 route for all Nostr identifiers (npub, nprofile, naddr, note, nevent) */}
+ } />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
} />
diff --git a/src/hooks/useAuthorBlogPosts.ts b/src/hooks/useAuthorBlogPosts.ts
new file mode 100644
index 0000000..74dd558
--- /dev/null
+++ b/src/hooks/useAuthorBlogPosts.ts
@@ -0,0 +1,62 @@
+import { useQuery } from '@tanstack/react-query';
+import { useNostr } from '@nostrify/react';
+import type { NostrEvent } from '@nostrify/nostrify';
+
+interface BlogPost extends NostrEvent {
+ kind: 30023;
+}
+
+/**
+ * Validates that a Nostr event is a valid NIP-23 blog post
+ */
+function validateBlogPost(event: NostrEvent): event is BlogPost {
+ // Must be kind 30023
+ if (event.kind !== 30023) return false;
+
+ // Must have required tags
+ const d = event.tags.find(([name]) => name === 'd')?.[1];
+ const title = event.tags.find(([name]) => name === 'title')?.[1];
+
+ // d and title are required for addressable events
+ if (!d || !title) return false;
+
+ return true;
+}
+
+/**
+ * Hook to fetch blog posts from a specific author
+ */
+export function useAuthorBlogPosts(pubkey: string) {
+ const { nostr } = useNostr();
+
+ return useQuery({
+ queryKey: ['author-blog-posts', pubkey],
+ queryFn: async (c) => {
+ const signal = AbortSignal.any([c.signal, AbortSignal.timeout(3000)]);
+
+ const events = await nostr.query(
+ [{
+ kinds: [30023],
+ authors: [pubkey],
+ limit: 50,
+ }],
+ { signal }
+ );
+
+ // Filter and validate events
+ const validPosts = events.filter(validateBlogPost);
+
+ // Sort by published_at (newest first), fallback to created_at
+ return validPosts.sort((a, b) => {
+ const aPublished = a.tags.find(([name]) => name === 'published_at')?.[1];
+ const bPublished = b.tags.find(([name]) => name === 'published_at')?.[1];
+
+ const aTime = aPublished ? parseInt(aPublished) : a.created_at;
+ const bTime = bPublished ? parseInt(bPublished) : b.created_at;
+
+ return bTime - aTime;
+ });
+ },
+ enabled: !!pubkey,
+ });
+}
diff --git a/src/pages/BlogPostPage.tsx b/src/pages/BlogPostPage.tsx
index aa43ba5..ec2bf5a 100644
--- a/src/pages/BlogPostPage.tsx
+++ b/src/pages/BlogPostPage.tsx
@@ -17,7 +17,7 @@ import { genUserName } from '@/lib/genUserName';
import NotFound from '@/pages/NotFound';
export default function BlogPostPage() {
- const { naddr } = useParams<{ naddr: string }>();
+ const { nip19: naddr } = useParams<{ nip19: string }>();
const navigate = useNavigate();
const { user } = useCurrentUser();
@@ -25,6 +25,7 @@ export default function BlogPostPage() {
let pubkey = '';
let identifier = '';
let kind = 0;
+ let isValidNaddr = false;
try {
if (naddr?.startsWith('naddr1')) {
@@ -33,6 +34,7 @@ export default function BlogPostPage() {
pubkey = decoded.data.pubkey;
identifier = decoded.data.identifier;
kind = decoded.data.kind;
+ isValidNaddr = true;
}
}
} catch (error) {
@@ -51,7 +53,7 @@ export default function BlogPostPage() {
const isPostAuthor = user?.pubkey === post?.pubkey;
const hasReacted = reactions?.likes.some(like => like.pubkey === user?.pubkey);
- if (!naddr || kind !== 30023) {
+ if (!isValidNaddr || !naddr || kind !== 30023) {
return ;
}
@@ -120,7 +122,7 @@ export default function BlogPostPage() {
{/* Author info and metadata */}
-
+
{displayName[0]?.toUpperCase()}
@@ -138,7 +140,7 @@ export default function BlogPostPage() {
-
+
{isPostAuthor && (