diff --git a/src/pages/EditPostPage.tsx b/src/pages/EditPostPage.tsx
index 333e0bd..42e424a 100644
--- a/src/pages/EditPostPage.tsx
+++ b/src/pages/EditPostPage.tsx
@@ -1,9 +1,16 @@
import { useParams, Navigate } from 'react-router-dom';
+import { useSeoMeta } from '@unhead/react';
import { ProfessionalBlogPostForm } from '@/components/ProfessionalBlogPostForm';
export default function EditPostPage() {
const { identifier } = useParams<{ identifier: string }>();
+ useSeoMeta({
+ title: 'Edit Article - zelo.news',
+ description: 'Edit your article on the Nostr network',
+ robots: 'noindex', // Don't index editor pages
+ });
+
if (!identifier) {
return
;
}
diff --git a/src/pages/EventPage.tsx b/src/pages/EventPage.tsx
index 9831db5..c8a1687 100644
--- a/src/pages/EventPage.tsx
+++ b/src/pages/EventPage.tsx
@@ -1,6 +1,7 @@
import { useQuery } from '@tanstack/react-query';
import { Link } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
+import { useSeoMeta } from '@unhead/react';
import { useNostr } from '@nostrify/react';
import { useAuthor } from '@/hooks/useAuthor';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
@@ -54,6 +55,38 @@ export function EventPage({ eventId, relayHints, authorPubkey, kind }: EventPage
const displayName = metadata?.display_name || metadata?.name || genUserName(event?.pubkey || '');
const profileImage = metadata?.picture;
+ // Set SEO meta tags when event data is available
+ const siteUrl = window.location.origin;
+ const eventUrl = window.location.href;
+
+ // Create a description from event content
+ const description = event && event.content
+ ? (event.content.length > 160
+ ? event.content.substring(0, 157) + '...'
+ : event.content)
+ : event
+ ? `Kind ${event.kind} event by ${displayName} on zelo.news`
+ : 'Event on zelo.news';
+
+ useSeoMeta({
+ title: event ? `Event (kind ${event.kind}) by ${displayName} - zelo.news` : 'Event - zelo.news',
+ description,
+ author: displayName,
+ // Open Graph tags for social sharing
+ ogTitle: event ? `Kind ${event.kind} event by ${displayName}` : 'Event on zelo.news',
+ ogDescription: description,
+ ogType: 'article',
+ ogUrl: eventUrl,
+ ogImage: profileImage || `${siteUrl}/icon-512.png`,
+ ogSiteName: 'zelo.news',
+ // Twitter Card tags
+ twitterCard: 'summary',
+ twitterTitle: event ? `Kind ${event.kind} event by ${displayName}` : 'Event on zelo.news',
+ twitterDescription: description,
+ twitterImage: profileImage || `${siteUrl}/icon-512.png`,
+ twitterSite: '@zelo_news',
+ });
+
if (isLoading) {
return (
diff --git a/src/pages/FollowingPage.tsx b/src/pages/FollowingPage.tsx
index 6c8285d..0024f7e 100644
--- a/src/pages/FollowingPage.tsx
+++ b/src/pages/FollowingPage.tsx
@@ -1,3 +1,4 @@
+import { useSeoMeta } from '@unhead/react';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useFollowingBlogPosts } from '@/hooks/useFollowingBlogPosts';
import { ArticlePreview } from '@/components/ArticlePreview';
@@ -11,6 +12,12 @@ export default function FollowingPage() {
const { user } = useCurrentUser();
const { data: posts = [], isLoading, isError } = useFollowingBlogPosts();
+ useSeoMeta({
+ title: 'Following - zelo.news',
+ description: 'Read articles from people you follow on zelo.news',
+ robots: 'noindex', // Don't index personal following feeds
+ });
+
// Show login prompt if user is not logged in
if (!user) {
return (
diff --git a/src/pages/BlogHomePage.tsx b/src/pages/HomePage.tsx
similarity index 51%
rename from src/pages/BlogHomePage.tsx
rename to src/pages/HomePage.tsx
index 4c8068b..e4dfe02 100644
--- a/src/pages/BlogHomePage.tsx
+++ b/src/pages/HomePage.tsx
@@ -1,10 +1,26 @@
+import { useSeoMeta } from '@unhead/react';
import { SearchBar } from '@/components/SearchBar';
import { LatestArticles } from '@/components/LatestArticles';
import { LatestInHashtag } from '@/components/LatestInHashtag';
import { TrendingTags } from '@/components/TrendingTags';
-import { Music, Leaf, BrainCircuit, Bitcoin } from 'lucide-react';
+import { Music, Leaf, BrainCircuit, Bitcoin, Newspaper } from 'lucide-react';
-export default function BlogHomePage() {
+export default function HomePage() {
+ useSeoMeta({
+ title: 'zelo.news - Decentralized News on Nostr',
+ description: 'Your source for decentralized news and articles on the Nostr protocol. Read, publish, and discover content from the Nostr network.',
+ ogTitle: 'zelo.news - Decentralized News on Nostr',
+ ogDescription: 'Your source for decentralized news and articles on the Nostr protocol. Read, publish, and discover content from the Nostr network.',
+ ogType: 'website',
+ ogUrl: window.location.href,
+ ogImage: `${window.location.origin}/icon-512.png`,
+ ogSiteName: 'zelo.news',
+ twitterCard: 'summary_large_image',
+ twitterTitle: 'zelo.news - Decentralized News on Nostr',
+ twitterDescription: 'Your source for decentralized news and articles on the Nostr protocol.',
+ twitterImage: `${window.location.origin}/icon-512.png`,
+ twitterSite: '@zelo_news',
+ });
return (
@@ -20,6 +36,12 @@ export default function BlogHomePage() {
{/* Latest Articles */}
+ {/* Latest in #news */}
+
}
+ />
+
{/* Latest in #music */}
{
useSeoMeta({
- title: 'Welcome to Your Blank App',
- description: 'A modern Nostr client application built with React, TailwindCSS, and Nostrify.',
+ title: 'zelo.news - Decentralized News on Nostr',
+ description: 'Your source for decentralized news and articles on the Nostr protocol. Read, publish, and discover content from the Nostr network.',
+ ogTitle: 'zelo.news - Decentralized News on Nostr',
+ ogDescription: 'Your source for decentralized news and articles on the Nostr protocol. Read, publish, and discover content from the Nostr network.',
+ ogType: 'website',
+ ogUrl: window.location.href,
+ ogImage: `${window.location.origin}/icon-512.png`,
+ ogSiteName: 'zelo.news',
+ twitterCard: 'summary_large_image',
+ twitterTitle: 'zelo.news - Decentralized News on Nostr',
+ twitterDescription: 'Your source for decentralized news and articles on the Nostr protocol.',
+ twitterImage: `${window.location.origin}/icon-512.png`,
+ twitterSite: '@zelo_news',
});
return (
diff --git a/src/pages/NotePage.tsx b/src/pages/NotePage.tsx
index 995d4cb..5a759f6 100644
--- a/src/pages/NotePage.tsx
+++ b/src/pages/NotePage.tsx
@@ -1,6 +1,7 @@
import { useQuery } from '@tanstack/react-query';
import { Link } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
+import { useSeoMeta } from '@unhead/react';
import { useNostr } from '@nostrify/react';
import { useAuthor } from '@/hooks/useAuthor';
import { useCurrentUser } from '@/hooks/useCurrentUser';
@@ -55,6 +56,36 @@ export function NotePage({ eventId }: NotePageProps) {
react({ eventId: note.id, eventAuthor: note.pubkey });
};
+ // Set SEO meta tags when note data is available
+ const siteUrl = window.location.origin;
+ const noteUrl = window.location.href;
+
+ // Create a description from note content
+ const description = note
+ ? (note.content.length > 160
+ ? note.content.substring(0, 157) + '...'
+ : note.content)
+ : 'Note on zelo.news';
+
+ useSeoMeta({
+ title: note ? `${displayName}'s note - zelo.news` : 'Note - zelo.news',
+ description,
+ author: displayName,
+ // Open Graph tags for social sharing
+ ogTitle: `Note by ${displayName}`,
+ ogDescription: description,
+ ogType: 'article',
+ ogUrl: noteUrl,
+ ogImage: profileImage || `${siteUrl}/icon-512.png`,
+ ogSiteName: 'zelo.news',
+ // Twitter Card tags
+ twitterCard: 'summary',
+ twitterTitle: `Note by ${displayName}`,
+ twitterDescription: description,
+ twitterImage: profileImage || `${siteUrl}/icon-512.png`,
+ twitterSite: '@zelo_news',
+ });
+
if (isLoading) {
return (
diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx
index c8028cc..3860461 100644
--- a/src/pages/ProfilePage.tsx
+++ b/src/pages/ProfilePage.tsx
@@ -1,11 +1,13 @@
import { useParams } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
+import { useSeoMeta } from '@unhead/react';
import { useAuthor } from '@/hooks/useAuthor';
import { useAuthorBlogPosts } from '@/hooks/useAuthorBlogPosts';
import { useUserBookmarkedArticles } from '@/hooks/useUserBookmarkedArticles';
import { ProfileView } from '@/components/ProfileView';
import { ProfileSkeleton } from '@/components/ProfileSkeleton';
import NotFound from '@/pages/NotFound';
+import { genUserName } from '@/lib/genUserName';
export default function ProfilePage() {
const { nip19: npub } = useParams<{ nip19: string }>();
@@ -36,6 +38,50 @@ export default function ProfilePage() {
const { data: posts, isLoading: postsLoading } = useAuthorBlogPosts(pubkey);
const { data: bookmarkedArticles, isLoading: bookmarksLoading } = useUserBookmarkedArticles(pubkey);
+ const metadata = author.data?.metadata;
+ const displayName = metadata?.display_name || metadata?.name || genUserName(pubkey);
+ const about = metadata?.about;
+ const picture = metadata?.picture;
+ const banner = metadata?.banner;
+ const nip05 = metadata?.nip05;
+
+ // Set SEO meta tags when author data is available
+ const siteUrl = window.location.origin;
+ const profileUrl = window.location.href;
+
+ // Create a description from about or default
+ const description = about
+ ? (about.length > 160 ? about.substring(0, 157) + '...' : about)
+ : `View ${displayName}'s profile and articles on zelo.news`;
+
+ const articleCount = posts?.length || 0;
+ const enrichedDescription = author.data && articleCount > 0
+ ? `${description} • ${articleCount} article${articleCount !== 1 ? 's' : ''} published`
+ : description;
+
+ useSeoMeta({
+ title: author.data && isValidProfile ? `${displayName} - Profile - zelo.news` : 'Profile - zelo.news',
+ description: enrichedDescription,
+ author: displayName,
+ // Open Graph tags for social sharing
+ ogTitle: `${displayName} on zelo.news`,
+ ogDescription: enrichedDescription,
+ ogType: 'profile',
+ ogUrl: profileUrl,
+ ogImage: banner || picture || `${siteUrl}/icon-512.png`,
+ ogSiteName: 'zelo.news',
+ // Profile-specific OG tags
+ ...(author.data && isValidProfile && {
+ profileUsername: nip05 || displayName,
+ }),
+ // Twitter Card tags
+ twitterCard: picture ? 'summary_large_image' : 'summary',
+ twitterTitle: `${displayName} on zelo.news`,
+ twitterDescription: enrichedDescription,
+ twitterImage: banner || picture || `${siteUrl}/icon-512.png`,
+ twitterSite: '@zelo_news',
+ });
+
// If not a valid profile identifier, show 404
if (!isValidProfile || !pubkey) {
return ;
diff --git a/src/pages/SearchResultsPage.tsx b/src/pages/SearchResultsPage.tsx
index fd070f9..57b6373 100644
--- a/src/pages/SearchResultsPage.tsx
+++ b/src/pages/SearchResultsPage.tsx
@@ -1,5 +1,6 @@
import { useSearchParams, Link } from 'react-router-dom';
import { nip19 } from 'nostr-tools';
+import { useSeoMeta } from '@unhead/react';
import { useSearch } from '@/hooks/useSearch';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
@@ -17,6 +18,24 @@ export default function SearchResultsPage() {
const { data: results, isLoading } = useSearch(searchTerm, true);
+ // Set SEO meta tags
+ const resultCount = results?.length || 0;
+ const isHashtagSearch = searchTerm.startsWith('#');
+
+ const title = isHashtagSearch
+ ? `Articles tagged ${searchTerm} - zelo.news`
+ : `Search: ${searchTerm} - zelo.news`;
+
+ const description = isHashtagSearch
+ ? `Browse ${resultCount} article${resultCount !== 1 ? 's' : ''} tagged with ${searchTerm} on zelo.news`
+ : `Found ${resultCount} result${resultCount !== 1 ? 's' : ''} for "${searchTerm}" on zelo.news`;
+
+ useSeoMeta({
+ title,
+ description,
+ robots: 'noindex', // Don't index search results pages
+ });
+
const profiles = results?.filter(r => r.type === 'profile') || [];
const articles = results?.filter(r => r.type === 'article') || [];