Browse Source

tried and failed to get opengraph data more specific

imwald
Silberengel 4 months ago
parent
commit
e17b5b0b2f
  1. 8
      Dockerfile
  2. 13
      index.html
  3. 20
      src/components/WebPreview/index.tsx
  4. 217
      src/pages/secondary/NotePage/index.tsx
  5. 90
      src/pages/secondary/ProfilePage/index.tsx

8
Dockerfile

@ -26,8 +26,16 @@ RUN printf "server {\n\
server_name localhost;\n\ server_name localhost;\n\
root /usr/share/nginx/html;\n\ root /usr/share/nginx/html;\n\
index index.html;\n\ index index.html;\n\
\n\
# Detect social media scrapers and other bots\n\
set \$is_scraper 0;\n\
if (\$http_user_agent ~* \"facebookexternalhit|Twitterbot|LinkedInBot|Slackbot|WhatsApp|Applebot|Googlebot|bingbot|YandexBot|Baiduspider|Slurp|DuckDuckBot|Baiduspider|YandexBot|Sogou|Exabot|facebot|ia_archiver\") {\n\
set \$is_scraper 1;\n\
}\n\
\n\ \n\
location / {\n\ location / {\n\
# For scrapers, serve index.html (they'll see static meta tags)\n\
# Note: To get dynamic meta tags, you need SSR or a meta tag service\n\
try_files \$uri \$uri/ /index.html;\n\ try_files \$uri \$uri/ /index.html;\n\
}\n\ }\n\
\n\ \n\

13
index.html

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Jumble</title> <title>Jumble - Imwald Edition 🌲</title>
<meta <meta
name="description" name="description"
content="A user-friendly Nostr client focused on relay feed browsing and relay discovery" content="A user-friendly Nostr client focused on relay feed browsing and relay discovery"
@ -20,17 +20,22 @@
<meta name="theme-color" content="#171717" media="(prefers-color-scheme: dark)" /> <meta name="theme-color" content="#171717" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)" /> <meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)" />
<meta property="og:url" content="https://jumble.social" /> <meta property="og:url" content="https://jumble.imwald.eu" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content="Jumble" /> <meta property="og:title" content="Jumble - Imwald Edition 🌲" />
<meta <meta
property="og:description" property="og:description"
content="A user-friendly Nostr client focused on relay feed browsing and relay discovery" content="A user-friendly Nostr client focused on relay feed browsing and relay discovery. The Imwald edition focuses on publications and articles."
/> />
<meta <meta
property="og:image" property="og:image"
content="https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true" content="https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true"
/> />
<meta property="og:site_name" content="Jumble" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Jumble - Imwald Edition 🌲" />
<meta name="twitter:description" content="Jumble.imwald.eu - A user-friendly Nostr client focused on relay feed browsing and relay discovery." />
<meta name="twitter:image" content="https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

20
src/components/WebPreview/index.tsx

@ -260,9 +260,13 @@ export default function WebPreview({ url, className }: { url: string; className?
> >
<Image image={{ url: image }} className="w-full max-w-[400px] h-44 rounded-none" hideIfError /> <Image image={{ url: image }} className="w-full max-w-[400px] h-44 rounded-none" hideIfError />
<div className="bg-muted p-2 w-full"> <div className="bg-muted p-2 w-full">
<div className="text-xs text-muted-foreground">{hostname}</div> <div className="flex items-center gap-2">
<div className="text-xs text-muted-foreground truncate">{hostname}</div>
<ExternalLink className="w-3 h-3 text-muted-foreground flex-shrink-0" />
</div>
{title && <div className="font-semibold line-clamp-1">{title}</div>} {title && <div className="font-semibold line-clamp-1">{title}</div>}
{!title && description && <div className="font-semibold line-clamp-1">{description}</div>} {!title && description && <div className="font-semibold line-clamp-1">{description}</div>}
<div className="text-xs text-muted-foreground truncate mt-1">{url}</div>
</div> </div>
</div> </div>
) )
@ -270,7 +274,7 @@ export default function WebPreview({ url, className }: { url: string; className?
return ( return (
<div <div
className={cn('p-0 clickable flex w-full border rounded-lg overflow-hidden', className)} className={cn('p-2 clickable flex w-full border rounded-lg overflow-hidden gap-2', className)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
window.open(url, '_blank') window.open(url, '_blank')
@ -279,18 +283,22 @@ export default function WebPreview({ url, className }: { url: string; className?
{image && ( {image && (
<Image <Image
image={{ url: image }} image={{ url: image }}
className="aspect-[4/3] xl:aspect-video bg-foreground h-44 max-w-[400px] rounded-none" className="aspect-[4/3] xl:aspect-video bg-foreground h-44 max-w-[400px] rounded-none flex-shrink-0"
hideIfError hideIfError
/> />
)} )}
<div className="flex-1 w-0 p-2"> <div className="flex-1 w-0 p-2">
<div className="text-xs text-muted-foreground">{hostname}</div> <div className="flex items-center gap-2 mb-1">
{title && <div className="font-semibold line-clamp-2">{title}</div>} <div className="text-xs text-muted-foreground truncate">{hostname}</div>
<ExternalLink className="w-3 h-3 text-muted-foreground flex-shrink-0" />
</div>
{title && <div className="font-semibold line-clamp-2 mb-1">{title}</div>}
{description && ( {description && (
<div className={cn("line-clamp-5", title ? "text-xs text-muted-foreground" : "text-sm font-semibold")}> <div className={cn("line-clamp-3 mb-1", title ? "text-xs text-muted-foreground" : "text-sm font-semibold")}>
{description} {description}
</div> </div>
)} )}
<div className="text-xs text-muted-foreground truncate">{url}</div>
</div> </div>
</div> </div>
) )

217
src/pages/secondary/NotePage/index.tsx

@ -8,7 +8,7 @@ import UserAvatar from '@/components/UserAvatar'
import { Card } from '@/components/ui/card' import { Card } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { useFetchEvent } from '@/hooks' import { useFetchEvent, useFetchProfile } from '@/hooks'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { getParentBech32Id, getParentETag, getRootBech32Id } from '@/lib/event' import { getParentBech32Id, getParentETag, getRootBech32Id } from '@/lib/event'
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata' import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
@ -22,6 +22,69 @@ import { forwardRef, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import NotFound from './NotFound' import NotFound from './NotFound'
// Helper function to get event type name (matching WebPreview)
function getEventTypeName(kind: number): string {
switch (kind) {
case kinds.ShortTextNote:
return 'Text Post'
case kinds.LongFormArticle:
return 'Longform Article'
case ExtendedKind.PICTURE:
return 'Picture'
case ExtendedKind.VIDEO:
return 'Video'
case ExtendedKind.SHORT_VIDEO:
return 'Short Video'
case ExtendedKind.POLL:
return 'Poll'
case ExtendedKind.COMMENT:
return 'Comment'
case ExtendedKind.VOICE:
return 'Voice Post'
case ExtendedKind.VOICE_COMMENT:
return 'Voice Comment'
case kinds.Highlights:
return 'Highlight'
case ExtendedKind.PUBLICATION:
return 'Publication'
case ExtendedKind.PUBLICATION_CONTENT:
return 'Publication Content'
case ExtendedKind.WIKI_ARTICLE:
return 'Wiki Article'
case ExtendedKind.WIKI_ARTICLE_MARKDOWN:
return 'Wiki Article'
case ExtendedKind.DISCUSSION:
return 'Discussion'
default:
return `Event (kind ${kind})`
}
}
// Helper function to extract and strip markdown/asciidoc for preview (matching WebPreview)
function stripMarkdown(content: string): string {
let text = content
// Remove markdown headers
text = text.replace(/^#{1,6}\s+/gm, '')
// Remove markdown bold/italic
text = text.replace(/\*\*([^*]+)\*\*/g, '$1')
text = text.replace(/\*([^*]+)\*/g, '$1')
// Remove markdown links
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
// Remove asciidoc headers
text = text.replace(/^=+\s+/gm, '')
// Remove asciidoc bold/italic
text = text.replace(/\*\*([^*]+)\*\*/g, '$1')
text = text.replace(/_([^_]+)_/g, '$1')
// Remove code blocks
text = text.replace(/```[\s\S]*?```/g, '')
text = text.replace(/`([^`]+)`/g, '$1')
// Remove HTML tags
text = text.replace(/<[^>]+>/g, '')
// Clean up whitespace
text = text.replace(/\n{3,}/g, '\n\n')
return text.trim()
}
const NotePage = forwardRef(({ id, index, hideTitlebar = false }: { id?: string; index?: number; hideTitlebar?: boolean }, ref) => { const NotePage = forwardRef(({ id, index, hideTitlebar = false }: { id?: string; index?: number; hideTitlebar?: boolean }, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const { event, isFetching } = useFetchEvent(id) const { event, isFetching } = useFetchEvent(id)
@ -37,6 +100,9 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false }: { id?: string;
const { isFetching: isFetchingRootEvent, event: rootEvent } = useFetchEvent(rootEventId) const { isFetching: isFetchingRootEvent, event: rootEvent } = useFetchEvent(rootEventId)
const { isFetching: isFetchingParentEvent, event: parentEvent } = useFetchEvent(parentEventId) const { isFetching: isFetchingParentEvent, event: parentEvent } = useFetchEvent(parentEventId)
// Fetch profile for author (for OpenGraph metadata)
const { profile: authorProfile } = useFetchProfile(finalEvent?.pubkey)
const getNoteTypeTitle = (kind: number): string => { const getNoteTypeTitle = (kind: number): string => {
switch (kind) { switch (kind) {
case 1: // kinds.ShortTextNote case 1: // kinds.ShortTextNote
@ -114,24 +180,41 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false }: { id?: string;
// Remove property prefix if present (e.g., 'og:title' or 'property="og:title"') // Remove property prefix if present (e.g., 'og:title' or 'property="og:title"')
const prop = property.startsWith('og:') || property.startsWith('article:') ? property : property.replace(/^property="|"$/, '') const prop = property.startsWith('og:') || property.startsWith('article:') ? property : property.replace(/^property="|"$/, '')
let meta = document.querySelector(`meta[property="${prop}"]`) // Handle Twitter card tags (they use name attribute, not property)
const isTwitterTag = prop.startsWith('twitter:')
const selector = isTwitterTag ? `meta[name="${prop}"]` : `meta[property="${prop}"]`
let meta = document.querySelector(selector)
if (!meta) { if (!meta) {
meta = document.createElement('meta') meta = document.createElement('meta')
if (isTwitterTag) {
meta.setAttribute('name', prop)
} else {
meta.setAttribute('property', prop) meta.setAttribute('property', prop)
}
document.head.appendChild(meta) document.head.appendChild(meta)
} }
meta.setAttribute('content', content) meta.setAttribute('content', content)
} }
// Update OpenGraph metadata for articles // Update OpenGraph metadata to match fallback cards
useEffect(() => { useEffect(() => {
if (!articleMetadata || !finalEvent) { if (!finalEvent) {
// Reset to default meta tags // Reset to default meta tags with richer information
updateMetaTag('og:title', 'Jumble') const defaultUrl = window.location.href
updateMetaTag('og:description', 'A user-friendly Nostr client focused on relay feed browsing and relay discovery') const truncatedDefaultUrl = defaultUrl.length > 150 ? defaultUrl.substring(0, 147) + '...' : defaultUrl
updateMetaTag('og:title', 'Jumble - Imwald Edition 🌲')
updateMetaTag('og:description', `${truncatedDefaultUrl} - A user-friendly Nostr client focused on relay feed browsing and relay discovery. The Imwald edition focuses on publications and articles.`)
updateMetaTag('og:image', 'https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true') updateMetaTag('og:image', 'https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true')
updateMetaTag('og:type', 'website') updateMetaTag('og:type', 'website')
updateMetaTag('og:url', window.location.href) updateMetaTag('og:url', window.location.href)
updateMetaTag('og:site_name', 'Jumble - Imwald Edition 🌲')
// Twitter card meta tags
updateMetaTag('twitter:card', 'summary_large_image')
updateMetaTag('twitter:title', 'Jumble - Imwald Edition 🌲')
updateMetaTag('twitter:description', `${truncatedDefaultUrl} - A user-friendly Nostr client focused on relay feed browsing and relay discovery. The Imwald edition focuses on publications and articles.`)
updateMetaTag('twitter:image', 'https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true')
// Remove article:tag if it exists // Remove article:tag if it exists
const articleTagMeta = document.querySelector('meta[property="article:tag"]') const articleTagMeta = document.querySelector('meta[property="article:tag"]')
@ -142,17 +225,100 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false }: { id?: string;
return return
} }
// Set article-specific OpenGraph metadata // Get event metadata matching fallback card format
const title = articleMetadata.title || 'Article' const eventMetadata = getLongFormArticleMetadataFromEvent(finalEvent)
const description = articleMetadata.summary || '' const eventTypeName = getEventTypeName(finalEvent.kind)
const image = articleMetadata.image || 'https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true' const eventTitle = eventMetadata?.title || eventTypeName
const tags = articleMetadata.tags || [] const eventSummary = eventMetadata?.summary || ''
// Generate content preview (matching fallback card)
let contentPreview = ''
if (finalEvent.content) {
const stripped = stripMarkdown(finalEvent.content)
contentPreview = stripped.length > 500 ? stripped.substring(0, 500) + '...' : stripped
}
// Build description matching fallback card: username • event type, title, summary, content preview, URL
// Always show note-specific info, even if profile isn't loaded yet
const authorName = authorProfile?.username || ''
const parts: string[] = []
// Always include event type (this is note-specific)
if (eventTypeName) {
parts.push(eventTypeName)
}
if (authorName) {
parts.push(`@${authorName}`)
}
let ogDescription = ''
if (parts.length > 0) {
ogDescription = parts.join(' • ')
} else {
// Fallback if nothing available yet
ogDescription = 'Event'
}
// Always show title if available (note-specific)
if (eventTitle && eventTitle !== eventTypeName) {
ogDescription += (ogDescription ? ' | ' : '') + eventTitle
}
// Show summary if available (note-specific)
if (eventSummary) {
ogDescription += (ogDescription ? ' - ' : '') + eventSummary
}
// Truncate URL to 150 chars before adding it
const fullUrl = window.location.href
const truncatedUrl = fullUrl.length > 150 ? fullUrl.substring(0, 147) + '...' : fullUrl
// Calculate remaining space for content preview (max 300 chars total, leave room for URL)
const maxDescLength = 300
const urlPart = ` | ${truncatedUrl}`
const remainingLength = maxDescLength - (ogDescription.length + urlPart.length)
// Always try to include content preview if available (this is note-specific!)
if (contentPreview && remainingLength > 20) {
const truncatedContent = contentPreview.length > remainingLength
? contentPreview.substring(0, remainingLength - 3) + '...'
: contentPreview
ogDescription += (ogDescription ? ' ' : '') + truncatedContent
}
// Add truncated URL at the end
ogDescription += (ogDescription ? urlPart : truncatedUrl)
updateMetaTag('og:title', title) // Ensure we have note-specific content - if description is still too generic, add more event info
updateMetaTag('og:description', description) if (!authorName && !eventSummary && !contentPreview && ogDescription.includes('Event') && !ogDescription.includes('|')) {
// Add at least the event kind or some identifier to make it note-specific
ogDescription = ogDescription.replace('Event', `${eventTypeName} (kind ${finalEvent.kind})`)
}
const image = eventMetadata?.image || (authorProfile?.avatar ? `https://jumble.imwald.eu/api/avatar/${authorProfile.pubkey}` : 'https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true')
const tags = eventMetadata?.tags || []
// For articles, use article type; for other events, use website type
const isArticle = articleMetadata !== null
const ogType = isArticle ? 'article' : 'website'
updateMetaTag('og:title', `${eventTitle} - Jumble Imwald Edition`)
updateMetaTag('og:description', ogDescription)
updateMetaTag('og:image', image) updateMetaTag('og:image', image)
updateMetaTag('og:type', 'article') updateMetaTag('og:type', ogType)
updateMetaTag('og:url', window.location.href) updateMetaTag('og:url', window.location.href)
updateMetaTag('og:site_name', 'Jumble - Imwald Edition 🌲')
// Add author for articles
if (isArticle && authorName) {
updateMetaTag('article:author', authorName)
}
// Twitter card meta tags
updateMetaTag('twitter:card', 'summary_large_image')
updateMetaTag('twitter:title', `${eventTitle} - Jumble Imwald Edition`)
updateMetaTag('twitter:description', ogDescription.length > 200 ? ogDescription.substring(0, 197) + '...' : ogDescription)
updateMetaTag('twitter:image', image)
// Remove old article:tag if it exists // Remove old article:tag if it exists
const oldArticleTagMeta = document.querySelector('meta[property="article:tag"]') const oldArticleTagMeta = document.querySelector('meta[property="article:tag"]')
@ -161,31 +327,40 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false }: { id?: string;
} }
// Add article-specific tags (one meta tag per tag) // Add article-specific tags (one meta tag per tag)
if (isArticle) {
tags.forEach(tag => { tags.forEach(tag => {
const tagMeta = document.createElement('meta') const tagMeta = document.createElement('meta')
tagMeta.setAttribute('property', 'article:tag') tagMeta.setAttribute('property', 'article:tag')
tagMeta.setAttribute('content', tag) tagMeta.setAttribute('content', tag)
document.head.appendChild(tagMeta) document.head.appendChild(tagMeta)
}) })
}
// Update document title // Update document title
document.title = `${title} - Jumble` document.title = `${eventTitle} - Jumble Imwald Edition`
// Cleanup function // Cleanup function
return () => { return () => {
// Reset to default on unmount // Reset to default on unmount with richer information
updateMetaTag('og:title', 'Jumble') const cleanupUrl = window.location.href
updateMetaTag('og:description', 'A user-friendly Nostr client focused on relay feed browsing and relay discovery') const truncatedCleanupUrl = cleanupUrl.length > 150 ? cleanupUrl.substring(0, 147) + '...' : cleanupUrl
updateMetaTag('og:title', 'Jumble - Imwald Edition 🌲')
updateMetaTag('og:description', `${truncatedCleanupUrl} - A user-friendly Nostr client focused on relay feed browsing and relay discovery. The Imwald edition focuses on publications and articles.`)
updateMetaTag('og:image', 'https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true') updateMetaTag('og:image', 'https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true')
updateMetaTag('og:type', 'website') updateMetaTag('og:type', 'website')
updateMetaTag('og:url', window.location.href) updateMetaTag('og:url', window.location.href)
updateMetaTag('og:site_name', 'Jumble - Imwald Edition 🌲')
// Remove article:tag meta tags // Remove article:tag meta tags
document.querySelectorAll('meta[property="article:tag"]').forEach(meta => meta.remove()) document.querySelectorAll('meta[property="article:tag"]').forEach(meta => meta.remove())
const authorMeta = document.querySelector('meta[property="article:author"]')
if (authorMeta) {
authorMeta.remove()
}
document.title = 'Jumble' document.title = 'Jumble - Imwald Edition 🌲'
} }
}, [articleMetadata, finalEvent]) }, [finalEvent, articleMetadata, authorProfile])
if (!event && isFetching) { if (!event && isFetching) {
return ( return (

90
src/pages/secondary/ProfilePage/index.tsx

@ -1,11 +1,99 @@
import Profile from '@/components/Profile' import Profile from '@/components/Profile'
import { useFetchProfile } from '@/hooks' import { useFetchProfile } from '@/hooks'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { forwardRef } from 'react' import { forwardRef, useEffect } from 'react'
// Helper function to update or create meta tags
function updateMetaTag(property: string, content: string) {
const prop = property.startsWith('og:') || property.startsWith('article:') ? property : property.replace(/^property="|"$/, '')
// Handle Twitter card tags (they use name attribute, not property)
const isTwitterTag = prop.startsWith('twitter:')
const selector = isTwitterTag ? `meta[name="${prop}"]` : `meta[property="${prop}"]`
let meta = document.querySelector(selector)
if (!meta) {
meta = document.createElement('meta')
if (isTwitterTag) {
meta.setAttribute('name', prop)
} else {
meta.setAttribute('property', prop)
}
document.head.appendChild(meta)
}
meta.setAttribute('content', content)
}
const ProfilePage = forwardRef(({ id, index, hideTitlebar = false }: { id?: string; index?: number; hideTitlebar?: boolean }, ref) => { const ProfilePage = forwardRef(({ id, index, hideTitlebar = false }: { id?: string; index?: number; hideTitlebar?: boolean }, ref) => {
const { profile } = useFetchProfile(id) const { profile } = useFetchProfile(id)
// Update OpenGraph metadata to match fallback card format for profiles
useEffect(() => {
if (!profile) {
// Reset to default meta tags
const defaultUrl = window.location.href
const truncatedDefaultUrl = defaultUrl.length > 150 ? defaultUrl.substring(0, 147) + '...' : defaultUrl
updateMetaTag('og:title', 'Jumble - Imwald Edition 🌲')
updateMetaTag('og:description', `${truncatedDefaultUrl} - A user-friendly Nostr client focused on relay feed browsing and relay discovery. The Imwald edition focuses on publications and articles.`)
updateMetaTag('og:image', 'https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true')
updateMetaTag('og:type', 'profile')
updateMetaTag('og:url', window.location.href)
updateMetaTag('og:site_name', 'Jumble - Imwald Edition 🌲')
// Twitter card meta tags
updateMetaTag('twitter:card', 'summary')
updateMetaTag('twitter:title', 'Jumble - Imwald Edition 🌲')
updateMetaTag('twitter:description', `${truncatedDefaultUrl} - Profile`)
updateMetaTag('twitter:image', 'https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true')
return
}
// Build description matching fallback card: username, hostname, URL
const username = profile.username || ''
const ogTitle = username || 'Profile'
// Truncate URL to 150 chars
const fullUrl = window.location.href
const truncatedUrl = fullUrl.length > 150 ? fullUrl.substring(0, 147) + '...' : fullUrl
let ogDescription = username ? `@${username}` : 'Profile'
ogDescription += ` | ${truncatedUrl}`
// Use profile avatar or default image
const image = profile.avatar ? `https://jumble.imwald.eu/api/avatar/${profile.pubkey}` : 'https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true'
updateMetaTag('og:title', `${ogTitle} - Jumble Imwald Edition`)
updateMetaTag('og:description', ogDescription)
updateMetaTag('og:image', image)
updateMetaTag('og:type', 'profile')
updateMetaTag('og:url', window.location.href)
updateMetaTag('og:site_name', 'Jumble - Imwald Edition 🌲')
// Twitter card meta tags
updateMetaTag('twitter:card', 'summary')
updateMetaTag('twitter:title', `${ogTitle} - Jumble Imwald Edition`)
updateMetaTag('twitter:description', ogDescription.length > 200 ? ogDescription.substring(0, 197) + '...' : ogDescription)
updateMetaTag('twitter:image', image)
// Update document title
document.title = `${ogTitle} - Jumble Imwald Edition`
// Cleanup function
return () => {
// Reset to default on unmount
const cleanupUrl = window.location.href
const truncatedCleanupUrl = cleanupUrl.length > 150 ? cleanupUrl.substring(0, 147) + '...' : cleanupUrl
updateMetaTag('og:title', 'Jumble - Imwald Edition 🌲')
updateMetaTag('og:description', `${truncatedCleanupUrl} - A user-friendly Nostr client focused on relay feed browsing and relay discovery. The Imwald edition focuses on publications and articles.`)
updateMetaTag('og:image', 'https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true')
updateMetaTag('og:type', 'website')
updateMetaTag('og:url', window.location.href)
updateMetaTag('og:site_name', 'Jumble - Imwald Edition 🌲')
document.title = 'Jumble - Imwald Edition 🌲'
}
}, [profile])
return ( return (
<SecondaryPageLayout index={index} title={hideTitlebar ? undefined : profile?.username} displayScrollToTopButton ref={ref}> <SecondaryPageLayout index={index} title={hideTitlebar ? undefined : profile?.username} displayScrollToTopButton ref={ref}>
<Profile id={id} /> <Profile id={id} />

Loading…
Cancel
Save