mirror of
https://github.com/mroxso/zelo-news.git
synced 2026-06-04 09:31:14 +02:00
Add comprehensive SEO optimization with dynamic meta tags and social sharing support (#23)
* Initial plan * Add SEO optimization with dynamic meta tags for all pages Co-authored-by: mroxso <24775431+mroxso@users.noreply.github.com> * Fix SEO meta tags to use useSeoMeta correctly without useEffect Co-authored-by: mroxso <24775431+mroxso@users.noreply.github.com> * Add SEO verification documentation Co-authored-by: mroxso <24775431+mroxso@users.noreply.github.com> * Add comprehensive SEO examples documentation Co-authored-by: mroxso <24775431+mroxso@users.noreply.github.com> * Refactor routing to use HomePage component and update blog post fetching limit --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mroxso <24775431+mroxso@users.noreply.github.com> Co-authored-by: highperfocused <highperfocused@pm.me>
This commit is contained in:
253
SEO_EXAMPLES.md
Normal file
253
SEO_EXAMPLES.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# SEO Implementation Examples
|
||||
|
||||
This document shows concrete examples of how the SEO meta tags appear on different pages of zelo.news.
|
||||
|
||||
## Example 1: Article Page
|
||||
|
||||
When viewing an article titled "The Future of Decentralized Social Media" by Alice:
|
||||
|
||||
### What Gets Rendered:
|
||||
```html
|
||||
<head>
|
||||
<title>The Future of Decentralized Social Media - Alice - zelo.news</title>
|
||||
<meta name="description" content="Exploring how Nostr and other decentralized protocols are reshaping social media. This article covers the key advantages and challenges...">
|
||||
<meta name="author" content="Alice">
|
||||
|
||||
<!-- Open Graph for Facebook, LinkedIn, WhatsApp -->
|
||||
<meta property="og:title" content="The Future of Decentralized Social Media">
|
||||
<meta property="og:description" content="Exploring how Nostr and other decentralized protocols are reshaping social media...">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:url" content="https://zelo.news/naddr1...">
|
||||
<meta property="og:image" content="https://example.com/article-image.jpg">
|
||||
<meta property="og:site_name" content="zelo.news">
|
||||
<meta property="article:published_time" content="2025-10-12T10:30:00.000Z">
|
||||
<meta property="article:author" content="Alice">
|
||||
<meta property="article:tag" content="nostr">
|
||||
<meta property="article:tag" content="decentralization">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="The Future of Decentralized Social Media">
|
||||
<meta name="twitter:description" content="Exploring how Nostr and other decentralized protocols...">
|
||||
<meta name="twitter:image" content="https://example.com/article-image.jpg">
|
||||
<meta name="twitter:site" content="@zelo_news">
|
||||
</head>
|
||||
```
|
||||
|
||||
### What This Looks Like When Shared:
|
||||
|
||||
**WhatsApp Preview:**
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ [Article Image] │
|
||||
├─────────────────────────────────────────┤
|
||||
│ The Future of Decentralized Social │
|
||||
│ Media │
|
||||
│ Exploring how Nostr and other │
|
||||
│ decentralized protocols are reshaping │
|
||||
│ social media... │
|
||||
│ zelo.news │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Twitter/X Preview:**
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ [Large Article Image] │
|
||||
├─────────────────────────────────────────┤
|
||||
│ The Future of Decentralized Social │
|
||||
│ Media │
|
||||
│ Exploring how Nostr and other │
|
||||
│ decentralized protocols... │
|
||||
│ 🔗 zelo.news │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Example 2: Profile Page
|
||||
|
||||
When viewing Bob's profile who has published 15 articles:
|
||||
|
||||
### What Gets Rendered:
|
||||
```html
|
||||
<head>
|
||||
<title>Bob - Profile - zelo.news</title>
|
||||
<meta name="description" content="Bitcoin enthusiast and writer exploring decentralized systems. • 15 articles published">
|
||||
<meta name="author" content="Bob">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="Bob on zelo.news">
|
||||
<meta property="og:description" content="Bitcoin enthusiast and writer exploring decentralized systems. • 15 articles published">
|
||||
<meta property="og:type" content="profile">
|
||||
<meta property="og:url" content="https://zelo.news/npub1...">
|
||||
<meta property="og:image" content="https://example.com/bob-avatar.jpg">
|
||||
<meta property="og:site_name" content="zelo.news">
|
||||
<meta property="profile:username" content="bob@nostr.com">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Bob on zelo.news">
|
||||
<meta name="twitter:description" content="Bitcoin enthusiast and writer exploring decentralized systems. • 15 articles published">
|
||||
<meta name="twitter:image" content="https://example.com/bob-avatar.jpg">
|
||||
<meta name="twitter:site" content="@zelo_news">
|
||||
</head>
|
||||
```
|
||||
|
||||
### What This Looks Like When Shared:
|
||||
|
||||
**Facebook/LinkedIn Preview:**
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ [Bob's Profile Banner or Avatar] │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Bob on zelo.news │
|
||||
│ Bitcoin enthusiast and writer exploring │
|
||||
│ decentralized systems. • 15 articles │
|
||||
│ published │
|
||||
│ 🔗 ZELO.NEWS │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Example 3: Home Page
|
||||
|
||||
When viewing the home page:
|
||||
|
||||
### What Gets Rendered:
|
||||
```html
|
||||
<head>
|
||||
<title>zelo.news - Decentralized News on Nostr</title>
|
||||
<meta name="description" content="Your source for decentralized news and articles on the Nostr protocol. Read, publish, and discover content from the Nostr network.">
|
||||
<meta name="keywords" content="nostr, decentralized, news, articles, blog, bitcoin, lightning, web3">
|
||||
<meta name="author" content="zelo.news">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="zelo.news - Decentralized News on Nostr">
|
||||
<meta property="og:description" content="Your source for decentralized news and articles on the Nostr protocol. Read, publish, and discover content from the Nostr network.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://zelo.news/">
|
||||
<meta property="og:image" content="https://zelo.news/icon-512.png">
|
||||
<meta property="og:site_name" content="zelo.news">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="zelo.news - Decentralized News on Nostr">
|
||||
<meta name="twitter:description" content="Your source for decentralized news and articles on the Nostr protocol.">
|
||||
<meta name="twitter:image" content="https://zelo.news/icon-512.png">
|
||||
<meta name="twitter:site" content="@zelo_news">
|
||||
</head>
|
||||
```
|
||||
|
||||
## Example 4: Search Results Page
|
||||
|
||||
When searching for "bitcoin":
|
||||
|
||||
### What Gets Rendered:
|
||||
```html
|
||||
<head>
|
||||
<title>Search: bitcoin - zelo.news</title>
|
||||
<meta name="description" content="Found 42 results for 'bitcoin' on zelo.news">
|
||||
<meta name="robots" content="noindex">
|
||||
<!-- Note: robots: noindex prevents search engines from indexing search results -->
|
||||
</head>
|
||||
```
|
||||
|
||||
## Example 5: Note Page (Short Post)
|
||||
|
||||
When viewing a short text note from Charlie:
|
||||
|
||||
### What Gets Rendered:
|
||||
```html
|
||||
<head>
|
||||
<title>Charlie's note - zelo.news</title>
|
||||
<meta name="description" content="Just published my thoughts on the future of decentralized identity. Check out my latest article!">
|
||||
<meta name="author" content="Charlie">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="Note by Charlie">
|
||||
<meta property="og:description" content="Just published my thoughts on the future of decentralized identity. Check out my latest article!">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:url" content="https://zelo.news/note1...">
|
||||
<meta property="og:image" content="https://example.com/charlie-avatar.jpg">
|
||||
<meta property="og:site_name" content="zelo.news">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="Note by Charlie">
|
||||
<meta name="twitter:description" content="Just published my thoughts on the future of decentralized identity...">
|
||||
<meta name="twitter:image" content="https://example.com/charlie-avatar.jpg">
|
||||
<meta name="twitter:site" content="@zelo_news">
|
||||
</head>
|
||||
```
|
||||
|
||||
## Benefits of This Implementation
|
||||
|
||||
### 1. Better Search Engine Rankings
|
||||
- Unique titles for every page
|
||||
- Descriptive meta descriptions
|
||||
- Proper semantic HTML structure
|
||||
- Keywords in meta tags
|
||||
|
||||
### 2. Rich Social Media Previews
|
||||
When users share links on:
|
||||
- **WhatsApp**: Shows article image, title, and description
|
||||
- **Facebook**: Rich preview with image, title, and description
|
||||
- **Twitter/X**: Large image card with content preview
|
||||
- **LinkedIn**: Professional preview with article details
|
||||
- **Discord**: Embedded preview with image and text
|
||||
- **Slack**: Link unfurling with full preview
|
||||
- **Telegram**: Rich message preview
|
||||
|
||||
### 3. Dynamic Content Handling
|
||||
- Meta tags update automatically when content loads
|
||||
- Fallback values ensure something always displays
|
||||
- Social media crawlers execute JavaScript and see updated tags
|
||||
|
||||
### 4. Professional Appearance
|
||||
- Consistent branding across all social platforms
|
||||
- Author attribution on all content
|
||||
- Timestamp information for articles
|
||||
- Category tags for better discovery
|
||||
|
||||
## Testing Your Implementation
|
||||
|
||||
### Quick Test with Twitter
|
||||
1. Copy any article URL from your deployed site
|
||||
2. Paste it into a new tweet
|
||||
3. Twitter will show a preview - you should see the article image, title, and description
|
||||
|
||||
### Full Test with Facebook Debugger
|
||||
1. Go to https://developers.facebook.com/tools/debug/
|
||||
2. Enter any page URL from your deployed site
|
||||
3. Click "Scrape Again"
|
||||
4. Review the preview - should show all meta tags and image
|
||||
|
||||
### Verify in Browser
|
||||
1. Open any page
|
||||
2. Right-click → Inspect
|
||||
3. Go to Elements tab
|
||||
4. Look at `<head>` section
|
||||
5. Should see all meta tags listed above
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue: Social preview not updating
|
||||
**Solution**: Use the social media debuggers to force a re-scrape
|
||||
- Facebook: https://developers.facebook.com/tools/debug/
|
||||
- Twitter: https://cards-dev.twitter.com/validator
|
||||
- LinkedIn: https://www.linkedin.com/post-inspector/
|
||||
|
||||
### Issue: Image not showing in preview
|
||||
**Solution**: Ensure images use absolute URLs (https://...) not relative (/image.jpg)
|
||||
|
||||
### Issue: Old title/description showing
|
||||
**Solution**: Clear browser cache or test in incognito mode
|
||||
|
||||
## Conclusion
|
||||
|
||||
The SEO implementation provides:
|
||||
✅ Professional social media previews on all platforms
|
||||
✅ Better search engine visibility
|
||||
✅ Dynamic updates for Nostr content
|
||||
✅ Proper handling of images and descriptions
|
||||
✅ Consistent branding across the web
|
||||
|
||||
All without requiring server-side rendering - it works entirely in the browser with the @unhead library!
|
||||
152
SEO_VERIFICATION.md
Normal file
152
SEO_VERIFICATION.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# SEO Optimization Verification
|
||||
|
||||
This document describes the SEO optimizations implemented in zelo.news and how to verify they work correctly.
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
### 1. Dynamic Page Titles
|
||||
Each page now has a unique, descriptive title that updates based on content:
|
||||
- **Article Pages**: `[Article Title] - [Author Name] - zelo.news`
|
||||
- **Profile Pages**: `[Username] - Profile - zelo.news`
|
||||
- **Home Page**: `zelo.news - Decentralized News on Nostr`
|
||||
- **Search Pages**: `Search: [query] - zelo.news` or `Articles tagged #[tag] - zelo.news`
|
||||
- **Other Pages**: Descriptive titles for bookmarks, following, etc.
|
||||
|
||||
### 2. Meta Descriptions
|
||||
Every page includes a relevant description:
|
||||
- **Articles**: Uses the article summary, or first 160 characters of content
|
||||
- **Profiles**: Uses the user's "about" text, or a default description
|
||||
- **Notes/Events**: Uses the first 160 characters of the content
|
||||
- **Search**: Includes result count and search term
|
||||
|
||||
### 3. Open Graph Tags (for Facebook, LinkedIn, WhatsApp, etc.)
|
||||
All pages include Open Graph meta tags:
|
||||
- `og:title` - Page/content title
|
||||
- `og:description` - Page/content description
|
||||
- `og:type` - "article" for content pages, "website" for home
|
||||
- `og:url` - Current page URL
|
||||
- `og:image` - Article image, profile picture, or default icon
|
||||
- `og:site_name` - "zelo.news"
|
||||
|
||||
Article pages also include:
|
||||
- `article:published_time` - Publication timestamp
|
||||
- `article:author` - Author name
|
||||
- `article:tag` - Article hashtags
|
||||
|
||||
### 4. Twitter Card Tags
|
||||
All pages include Twitter Card meta tags for Twitter/X sharing:
|
||||
- `twitter:card` - "summary_large_image" for articles, "summary" for others
|
||||
- `twitter:title` - Page/content title
|
||||
- `twitter:description` - Page/content description
|
||||
- `twitter:image` - Article image, profile picture, or default icon
|
||||
- `twitter:site` - "@zelo_news"
|
||||
|
||||
### 5. Additional SEO Features
|
||||
- Keywords meta tag in base HTML
|
||||
- Author meta tag in base HTML
|
||||
- `robots: noindex` for personal pages (bookmarks, following, search results, editor)
|
||||
- Proper semantic HTML structure
|
||||
|
||||
## How to Verify
|
||||
|
||||
### Method 1: Browser DevTools
|
||||
1. Open the application in a browser
|
||||
2. Open DevTools (F12)
|
||||
3. Go to the Elements/Inspector tab
|
||||
4. Look at the `<head>` section
|
||||
5. You should see dynamically injected meta tags from @unhead
|
||||
|
||||
### Method 2: View Page Source
|
||||
1. Right-click on any page and select "View Page Source"
|
||||
2. Look for `<meta>` tags in the `<head>`
|
||||
3. Default tags will be visible in the static HTML
|
||||
4. Dynamic tags are injected by JavaScript after page load
|
||||
|
||||
### Method 3: Social Media Sharing Debuggers
|
||||
These tools show what social media platforms will see:
|
||||
|
||||
**Facebook/Open Graph Debugger:**
|
||||
- Visit: https://developers.facebook.com/tools/debug/
|
||||
- Enter a page URL from your deployed site
|
||||
- Click "Scrape Again" to see the latest meta tags
|
||||
- Should show article title, description, and image
|
||||
|
||||
**Twitter Card Validator:**
|
||||
- Visit: https://cards-dev.twitter.com/validator
|
||||
- Enter a page URL from your deployed site
|
||||
- Should show a preview of how the link will appear on Twitter
|
||||
|
||||
**LinkedIn Post Inspector:**
|
||||
- Visit: https://www.linkedin.com/post-inspector/
|
||||
- Enter a page URL from your deployed site
|
||||
- Should show how the link will appear on LinkedIn
|
||||
|
||||
### Method 4: Browser Extensions
|
||||
Install a meta tag viewer extension:
|
||||
- **SEO Meta in 1 Click** (Chrome/Edge)
|
||||
- **META SEO inspector** (Firefox)
|
||||
- These will show all meta tags on the current page
|
||||
|
||||
## Example: Article Page Meta Tags
|
||||
|
||||
When viewing an article page, the following meta tags should be present:
|
||||
|
||||
```html
|
||||
<title>[Article Title] - [Author Name] - zelo.news</title>
|
||||
<meta name="description" content="[Article summary or first 160 chars]">
|
||||
<meta name="author" content="[Author Name]">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="[Article Title]">
|
||||
<meta property="og:description" content="[Article summary or first 160 chars]">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:url" content="[Current URL]">
|
||||
<meta property="og:image" content="[Article image or default icon]">
|
||||
<meta property="og:site_name" content="zelo.news">
|
||||
<meta property="article:published_time" content="[ISO timestamp]">
|
||||
<meta property="article:author" content="[Author Name]">
|
||||
<meta property="article:tag" content="[hashtag1]">
|
||||
<meta property="article:tag" content="[hashtag2]">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="[Article Title]">
|
||||
<meta name="twitter:description" content="[Article summary or first 160 chars]">
|
||||
<meta name="twitter:image" content="[Article image or default icon]">
|
||||
<meta name="twitter:site" content="@zelo_news">
|
||||
```
|
||||
|
||||
## Testing Dynamic Content Loading
|
||||
|
||||
The SEO implementation handles dynamically loaded content:
|
||||
|
||||
1. **Initial Load**: Default meta tags from `index.html` are visible
|
||||
2. **After Data Loads**: Meta tags are updated with actual content
|
||||
3. **Social Media Crawlers**: They execute JavaScript and will see the updated tags
|
||||
|
||||
To verify this works:
|
||||
1. Open a page (e.g., an article page)
|
||||
2. Open DevTools > Network tab
|
||||
3. Reload the page
|
||||
4. Watch the meta tags in the Elements tab - they should update as data loads
|
||||
|
||||
## Notes
|
||||
|
||||
- **Server-Side Rendering**: Currently not implemented. Social media crawlers execute JavaScript, so they will see the dynamically set meta tags.
|
||||
- **@unhead Library**: This library is SSR-compatible and properly manages meta tags.
|
||||
- **Image URLs**: Uses absolute URLs so social media platforms can fetch images.
|
||||
- **Default Fallbacks**: All pages have sensible defaults if content is not available.
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `index.html` - Updated default meta tags
|
||||
- `src/pages/ArticlePage.tsx` - Added article-specific SEO
|
||||
- `src/pages/ProfilePage.tsx` - Added profile-specific SEO
|
||||
- `src/pages/BlogHomePage.tsx` - Added home page SEO
|
||||
- `src/pages/SearchResultsPage.tsx` - Added search page SEO (noindex)
|
||||
- `src/pages/BookmarksPage.tsx` - Added bookmarks page SEO (noindex)
|
||||
- `src/pages/FollowingPage.tsx` - Added following page SEO (noindex)
|
||||
- `src/pages/CreatePostPage.tsx` - Added create page SEO (noindex)
|
||||
- `src/pages/EditPostPage.tsx` - Added edit page SEO (noindex)
|
||||
- `src/pages/NotePage.tsx` - Added note page SEO
|
||||
- `src/pages/EventPage.tsx` - Added event page SEO
|
||||
17
index.html
17
index.html
@@ -3,8 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>zelo.news - Your Source for Decentralized News</title>
|
||||
<meta name="description" content="Your Source for Decentralized News" />
|
||||
<title>zelo.news - Decentralized News on Nostr</title>
|
||||
<meta name="description" content="Your source for decentralized news and articles on the Nostr protocol. Read, publish, and discover content from the Nostr network." />
|
||||
<meta name="keywords" content="nostr, decentralized, news, articles, blog, bitcoin, lightning, web3" />
|
||||
<meta name="author" content="zelo.news" />
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="theme-color" content="#000000" />
|
||||
@@ -17,8 +19,15 @@
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="zelo.news - Your Source for Decentralized News" />
|
||||
<meta property="og:description" content="Your Source for Decentralized News" />
|
||||
<meta property="og:title" content="zelo.news - Decentralized News on Nostr" />
|
||||
<meta property="og:description" content="Your source for decentralized news and articles on the Nostr protocol. Read, publish, and discover content from the Nostr network." />
|
||||
<meta property="og:site_name" content="zelo.news" />
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="zelo.news - Decentralized News on Nostr" />
|
||||
<meta name="twitter:description" content="Your source for decentralized news and articles on the Nostr protocol." />
|
||||
<meta name="twitter:site" content="@zelo_news" />
|
||||
|
||||
<meta http-equiv="content-security-policy" content="default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self' https:; font-src 'self'; base-uri 'self'; manifest-src 'self'; connect-src 'self' blob: https: wss:; img-src 'self' data: blob: https:; media-src 'self' https:">
|
||||
</head>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import { ScrollToTop } from "./components/ScrollToTop";
|
||||
import { BlogLayout } from "./components/BlogLayout";
|
||||
|
||||
import BlogHomePage from "./pages/BlogHomePage";
|
||||
import CreatePostPage from "./pages/CreatePostPage";
|
||||
import EditPostPage from "./pages/EditPostPage";
|
||||
import SearchResultsPage from "./pages/SearchResultsPage";
|
||||
@@ -12,6 +10,7 @@ import Nip05ProfilePage from "./pages/Nip05ProfilePage";
|
||||
import ArticleByDTagPage from "./pages/ArticleByDTagPage";
|
||||
import { NIP19Page } from "./pages/NIP19Page";
|
||||
import NotFound from "./pages/NotFound";
|
||||
import HomePage from "./pages/HomePage";
|
||||
|
||||
export function AppRouter() {
|
||||
return (
|
||||
@@ -19,7 +18,7 @@ export function AppRouter() {
|
||||
<ScrollToTop />
|
||||
<BlogLayout>
|
||||
<Routes>
|
||||
<Route path="/" element={<BlogHomePage />} />
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/create" element={<CreatePostPage />} />
|
||||
<Route path="/edit/:identifier" element={<EditPostPage />} />
|
||||
<Route path="/search" element={<SearchResultsPage />} />
|
||||
|
||||
@@ -15,7 +15,7 @@ const INITIAL_POSTS_COUNT = 3;
|
||||
|
||||
export function LatestInHashtag({ hashtag, icon }: LatestInHashtagProps) {
|
||||
const navigate = useNavigate();
|
||||
const { data: posts, isLoading } = useBlogPostsByHashtag(hashtag);
|
||||
const { data: posts, isLoading } = useBlogPostsByHashtag(hashtag, 4);
|
||||
|
||||
// Loading state
|
||||
if (isLoading) {
|
||||
@@ -60,9 +60,9 @@ export function LatestInHashtag({ hashtag, icon }: LatestInHashtagProps) {
|
||||
<h2 className="text-3xl font-bold tracking-tight">
|
||||
Latest in #{hashtag}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{/* <p className="text-sm text-muted-foreground mt-1">
|
||||
{posts.length} {posts.length === 1 ? 'article' : 'articles'} in this category
|
||||
</p>
|
||||
</p> */}
|
||||
</div>
|
||||
{hasMore && (
|
||||
<Button
|
||||
|
||||
@@ -26,7 +26,7 @@ function validateBlogPost(event: NostrEvent): event is BlogPost {
|
||||
/**
|
||||
* Hook to fetch blog posts filtered by a specific hashtag
|
||||
*/
|
||||
export function useBlogPostsByHashtag(hashtag: string) {
|
||||
export function useBlogPostsByHashtag(hashtag: string, limit: number = 50) {
|
||||
const { nostr } = useNostr();
|
||||
|
||||
return useQuery({
|
||||
@@ -38,7 +38,7 @@ export function useBlogPostsByHashtag(hashtag: string) {
|
||||
[{
|
||||
kinds: [30023],
|
||||
'#t': [hashtag.toLowerCase()],
|
||||
limit: 50,
|
||||
limit: limit,
|
||||
}],
|
||||
{ signal }
|
||||
);
|
||||
|
||||
@@ -31,7 +31,7 @@ export function useSearch(searchTerm: string, enabled = true) {
|
||||
{
|
||||
kinds: [30023],
|
||||
'#t': [tagValue],
|
||||
limit: 100,
|
||||
// limit: 100,
|
||||
},
|
||||
],
|
||||
{ signal }
|
||||
@@ -45,7 +45,7 @@ export function useSearch(searchTerm: string, enabled = true) {
|
||||
[
|
||||
{
|
||||
kinds: [0, 30023],
|
||||
limit: 100,
|
||||
// limit: 100,
|
||||
},
|
||||
],
|
||||
{ signal }
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { useBlogPost } from '@/hooks/useBlogPost';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { ArticleView } from '@/components/ArticleView';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import NotFound from '@/pages/NotFound';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
|
||||
export default function ArticlePage() {
|
||||
const { nip19: naddr } = useParams<{ nip19: string }>();
|
||||
@@ -28,6 +31,58 @@ export default function ArticlePage() {
|
||||
}
|
||||
|
||||
const { data: post, isLoading } = useBlogPost(pubkey, identifier);
|
||||
const author = useAuthor(pubkey);
|
||||
|
||||
// Extract article metadata
|
||||
const title = post?.tags.find(([name]) => name === 'title')?.[1] || 'Article';
|
||||
const summary = post?.tags.find(([name]) => name === 'summary')?.[1];
|
||||
const image = post?.tags.find(([name]) => name === 'image')?.[1];
|
||||
const publishedAt = post?.tags.find(([name]) => name === 'published_at')?.[1];
|
||||
const hashtags = post?.tags
|
||||
.filter(([name]) => name === 't')
|
||||
.map(([, value]) => value) || [];
|
||||
|
||||
const metadata = author.data?.metadata;
|
||||
const authorName = metadata?.display_name || metadata?.name || genUserName(pubkey);
|
||||
|
||||
const date = publishedAt
|
||||
? new Date(parseInt(publishedAt) * 1000)
|
||||
: post ? new Date(post.created_at * 1000) : new Date();
|
||||
|
||||
// Set SEO meta tags when post data is available
|
||||
const siteUrl = window.location.origin;
|
||||
const articleUrl = window.location.href;
|
||||
|
||||
// Create a description from summary or content
|
||||
const description = post && (summary ||
|
||||
(post.content.length > 160
|
||||
? post.content.substring(0, 157) + '...'
|
||||
: post.content)) || 'Article on zelo.news';
|
||||
|
||||
useSeoMeta({
|
||||
title: post && isValidNaddr ? `${title} - ${authorName} - zelo.news` : 'Article - zelo.news',
|
||||
description,
|
||||
author: authorName,
|
||||
// Open Graph tags for social sharing
|
||||
ogTitle: title,
|
||||
ogDescription: description,
|
||||
ogType: 'article',
|
||||
ogUrl: articleUrl,
|
||||
ogImage: image || `${siteUrl}/icon-512.png`,
|
||||
ogSiteName: 'zelo.news',
|
||||
// Article-specific OG tags
|
||||
...(post && isValidNaddr && {
|
||||
articlePublishedTime: date.toISOString(),
|
||||
articleAuthor: [authorName],
|
||||
...(hashtags.length > 0 && { articleTag: hashtags }),
|
||||
}),
|
||||
// Twitter Card tags
|
||||
twitterCard: 'summary_large_image',
|
||||
twitterTitle: title,
|
||||
twitterDescription: description,
|
||||
twitterImage: image || `${siteUrl}/icon-512.png`,
|
||||
twitterSite: '@zelo_news',
|
||||
});
|
||||
|
||||
if (!isValidNaddr || !naddr || kind !== 30023) {
|
||||
return <NotFound />;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { ArticlePreview } from '@/components/ArticlePreview';
|
||||
import { RelaySelector } from '@/components/RelaySelector';
|
||||
import { LoginArea } from '@/components/auth/LoginArea';
|
||||
@@ -13,6 +14,12 @@ export function BookmarksPage() {
|
||||
const { data: bookmarks = [], isLoading: isLoadingBookmarks } = useBookmarks();
|
||||
const { data: articles = [], isLoading: isLoadingArticles } = useBookmarkedArticles();
|
||||
|
||||
useSeoMeta({
|
||||
title: 'My Bookmarks - zelo.news',
|
||||
description: 'View your saved articles and bookmarks on zelo.news',
|
||||
robots: 'noindex', // Don't index personal bookmarks pages
|
||||
});
|
||||
|
||||
const isLoading = isLoadingBookmarks || isLoadingArticles;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { useSeoMeta } from '@unhead/react';
|
||||
import { ProfessionalBlogPostForm } from '@/components/ProfessionalBlogPostForm';
|
||||
|
||||
export default function CreatePostPage() {
|
||||
useSeoMeta({
|
||||
title: 'Create Article - zelo.news',
|
||||
description: 'Write and publish a new article on the Nostr network',
|
||||
robots: 'noindex', // Don't index editor pages
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="container max-w-7xl py-6 px-4 sm:px-6 lg:px-8">
|
||||
<ProfessionalBlogPostForm />
|
||||
|
||||
@@ -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 <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="min-h-screen">
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
<div className="min-h-screen">
|
||||
@@ -20,6 +36,12 @@ export default function BlogHomePage() {
|
||||
{/* Latest Articles */}
|
||||
<LatestArticles />
|
||||
|
||||
{/* Latest in #news */}
|
||||
<LatestInHashtag
|
||||
hashtag="news"
|
||||
icon={<Newspaper className="h-6 w-6 text-primary" />}
|
||||
/>
|
||||
|
||||
{/* Latest in #music */}
|
||||
<LatestInHashtag
|
||||
hashtag="music"
|
||||
@@ -2,8 +2,19 @@ import { useSeoMeta } from '@unhead/react';
|
||||
|
||||
const Index = () => {
|
||||
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 (
|
||||
|
||||
@@ -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 (
|
||||
<div className="min-h-screen">
|
||||
|
||||
@@ -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 <NotFound />;
|
||||
|
||||
@@ -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') || [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user