You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
271 lines
9.9 KiB
271 lines
9.9 KiB
/** Shared Open Graph / Twitter / document title helpers (client-side). */ |
|
|
|
export const SITE_NAME = 'Imwald' |
|
|
|
const SITE_TAGLINE = |
|
'A user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery.' |
|
|
|
export function getSiteOrigin(): string { |
|
if (typeof window === 'undefined') return 'https://jumble.imwald.eu' |
|
return window.location.origin |
|
} |
|
|
|
export function defaultOgImageAbsoluteUrl(): string { |
|
return `${getSiteOrigin()}/og-image.png` |
|
} |
|
|
|
export function avatarProxyUrl(pubkey: string): string { |
|
return `${getSiteOrigin()}/api/avatar/${pubkey}` |
|
} |
|
|
|
export function updateMetaTag(property: string, content: string): void { |
|
if (typeof document === 'undefined') return |
|
const prop = |
|
property.startsWith('og:') || property.startsWith('article:') || property.startsWith('profile:') |
|
? property |
|
: property.replace(/^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) |
|
} |
|
|
|
export function removeMetaByProperty(property: string): void { |
|
if (typeof document === 'undefined') return |
|
document.querySelectorAll(`meta[property="${property}"]`).forEach((m) => m.remove()) |
|
} |
|
|
|
export function applyDefaultSiteSocialMeta(): void { |
|
if (typeof window === 'undefined') return |
|
const href = window.location.href |
|
const truncatedUrl = href.length > 150 ? href.substring(0, 147) + '...' : href |
|
const desc = `${truncatedUrl} — ${SITE_TAGLINE}` |
|
const img = defaultOgImageAbsoluteUrl() |
|
|
|
updateMetaTag('og:title', SITE_NAME) |
|
updateMetaTag('og:description', desc) |
|
updateMetaTag('og:image', img) |
|
updateMetaTag('og:type', 'website') |
|
updateMetaTag('og:url', href) |
|
updateMetaTag('og:site_name', SITE_NAME) |
|
|
|
updateMetaTag('twitter:card', 'summary_large_image') |
|
updateMetaTag('twitter:title', SITE_NAME) |
|
updateMetaTag('twitter:description', desc) |
|
updateMetaTag('twitter:image', img) |
|
} |
|
|
|
const PRIMARY_PAGE_LABEL: Record<string, string> = { |
|
explore: 'Explore', |
|
feed: 'Feed', |
|
me: 'Me', |
|
profile: 'Profile', |
|
relay: 'Relay', |
|
search: 'Search', |
|
rss: 'RSS', |
|
settings: 'Settings', |
|
spells: 'Spells', |
|
calendar: 'Calendar' |
|
} |
|
|
|
function relayHostnameFromPath(pathname: string): string | null { |
|
const m = |
|
pathname.match(/\/(?:home|explore)\/relays\/(.+)$/i) || pathname.match(/^\/relays\/(.+)$/i) |
|
if (!m?.[1]) return null |
|
try { |
|
const decoded = decodeURIComponent(m[1].split('/')[0]) |
|
const asHttp = decoded.startsWith('wss://') |
|
? 'https://' + decoded.slice(6) |
|
: decoded.startsWith('ws://') |
|
? 'http://' + decoded.slice(5) |
|
: decoded |
|
const u = new URL(asHttp.includes('://') ? asHttp : `https://${asHttp}`) |
|
return u.hostname || decoded |
|
} catch { |
|
return m[1].slice(0, 80) |
|
} |
|
} |
|
|
|
export type TRouteSocialCopy = { pageTitle: string; ogTitle: string; description: string } |
|
|
|
/** Note detail URLs set OG tags in NotePage. */ |
|
export function isNoteDetailPathname(pathname: string): boolean { |
|
const path = pathname.split('?')[0].split('#')[0] |
|
return ( |
|
/\/notes\/[^/?#]+/.test(path) || |
|
/\/(?:discussions|search|profile|home|feed|spells|explore|rss|calendar)\/notes\/[^/?#]+/.test( |
|
path |
|
) |
|
) |
|
} |
|
|
|
/** Profile detail (/users/:id) sets OG in ProfilePage. */ |
|
export function isProfileDetailPathname(pathname: string): boolean { |
|
const path = pathname.split('?')[0].split('#')[0].replace(/\/$/, '') || '/' |
|
return /^\/users\/[^/]+$/.test(path) |
|
} |
|
|
|
/** Labels for static Imwald URLs (in-app link previews + route-level OG when no note/profile). */ |
|
export function resolveImwaldRouteSocialCopy( |
|
pathname: string, |
|
currentPrimaryPage: string |
|
): TRouteSocialCopy { |
|
const path = pathname.split('?')[0].split('#')[0].replace(/\/$/, '') || '/' |
|
const href = typeof window !== 'undefined' ? window.location.href : '' |
|
const relayHost = relayHostnameFromPath(path) |
|
|
|
let pageTitle = SITE_NAME |
|
let ogTitle = SITE_NAME |
|
let description = href ? `${SITE_TAGLINE} ${href}` : SITE_TAGLINE |
|
|
|
if (path.startsWith('/settings')) { |
|
if (path.includes('/general')) { |
|
pageTitle = `General · ${SITE_NAME}` |
|
ogTitle = `General settings · ${SITE_NAME}` |
|
} else if (path.includes('/relays')) { |
|
pageTitle = `Relays · ${SITE_NAME}` |
|
ogTitle = `Relay & storage settings · ${SITE_NAME}` |
|
} else if (path.includes('/cache')) { |
|
pageTitle = `Cache · ${SITE_NAME}` |
|
ogTitle = `Cache & offline storage · ${SITE_NAME}` |
|
} else if (path.includes('/wallet')) { |
|
pageTitle = `Wallet · ${SITE_NAME}` |
|
ogTitle = `Wallet settings · ${SITE_NAME}` |
|
} else if (path.includes('/posts')) { |
|
pageTitle = `Posts · ${SITE_NAME}` |
|
ogTitle = `Post settings · ${SITE_NAME}` |
|
} else if (path.includes('/translation')) { |
|
pageTitle = `Translation · ${SITE_NAME}` |
|
ogTitle = `Translation settings · ${SITE_NAME}` |
|
} else if (path.includes('/rss-feeds')) { |
|
pageTitle = `RSS feeds · ${SITE_NAME}` |
|
ogTitle = `RSS feed settings · ${SITE_NAME}` |
|
} else if (path.includes('/follow-sets')) { |
|
pageTitle = `Follow sets · ${SITE_NAME}` |
|
ogTitle = `Follow sets · ${SITE_NAME}` |
|
} else if (path.includes('/personal-lists')) { |
|
pageTitle = `Lists · ${SITE_NAME}` |
|
ogTitle = `Personal lists · ${SITE_NAME}` |
|
} else { |
|
pageTitle = `Settings · ${SITE_NAME}` |
|
ogTitle = `Settings · ${SITE_NAME}` |
|
} |
|
description = `${ogTitle}. ${SITE_TAGLINE}` |
|
} else if (path === '/search' || path.startsWith('/search/')) { |
|
pageTitle = `Search · ${SITE_NAME}` |
|
ogTitle = pageTitle |
|
description = `Search notes and people on Nostr with ${SITE_NAME}.` |
|
} else if (relayHost) { |
|
const host = relayHost |
|
pageTitle = `${host} · ${SITE_NAME}` |
|
ogTitle = `Relay ${host} · ${SITE_NAME}` |
|
description = `Relay ${host} on ${SITE_NAME}. ${SITE_TAGLINE}` |
|
} else if (path.startsWith('/rss-item') || path.includes('/rss-item/')) { |
|
pageTitle = `Article · ${SITE_NAME}` |
|
ogTitle = `RSS article · ${SITE_NAME}` |
|
description = `Read an RSS-sourced article in ${SITE_NAME}.` |
|
} else if (path === '/users') { |
|
pageTitle = `People · ${SITE_NAME}` |
|
ogTitle = `People on ${SITE_NAME}` |
|
description = `Browse Nostr profiles in ${SITE_NAME}.` |
|
} else if (path === '/bookmarks') { |
|
pageTitle = `Bookmarks · ${SITE_NAME}` |
|
ogTitle = pageTitle |
|
description = `Your bookmarked notes on ${SITE_NAME}.` |
|
} else if (path === '/mutes') { |
|
pageTitle = `Muted users · ${SITE_NAME}` |
|
ogTitle = pageTitle |
|
description = `Muted users in ${SITE_NAME}.` |
|
} else if (path === '/pins') { |
|
pageTitle = `Pinned notes · ${SITE_NAME}` |
|
ogTitle = pageTitle |
|
description = `Pinned notes in ${SITE_NAME}.` |
|
} else if (path === '/interests') { |
|
pageTitle = `Interests · ${SITE_NAME}` |
|
ogTitle = pageTitle |
|
description = `Interest lists in ${SITE_NAME}.` |
|
} else if (path === '/profile-editor') { |
|
pageTitle = `Edit profile · ${SITE_NAME}` |
|
ogTitle = pageTitle |
|
description = `Edit your Nostr profile in ${SITE_NAME}.` |
|
} else if (path === '/follow-packs') { |
|
pageTitle = `Follow packs · ${SITE_NAME}` |
|
ogTitle = pageTitle |
|
description = `Follow packs on ${SITE_NAME}.` |
|
} else if (path === '/notes') { |
|
pageTitle = `Notes · ${SITE_NAME}` |
|
ogTitle = pageTitle |
|
description = `Notes in ${SITE_NAME}.` |
|
} else if (path.match(/^\/users\/[^/]+\/following$/)) { |
|
pageTitle = `Following · ${SITE_NAME}` |
|
ogTitle = `Following list · ${SITE_NAME}` |
|
description = `Following list on ${SITE_NAME}.` |
|
} else if (path.match(/^\/users\/[^/]+\/relays$/)) { |
|
pageTitle = `Relays · ${SITE_NAME}` |
|
ogTitle = `User relays · ${SITE_NAME}` |
|
description = `Relay list on ${SITE_NAME}.` |
|
} else if (path === '/' || path === '/home') { |
|
pageTitle = `Home · ${SITE_NAME}` |
|
ogTitle = `Home · ${SITE_NAME}` |
|
description = `${SITE_TAGLINE} ${href}` |
|
} else { |
|
const seg = path.split('/').filter(Boolean)[0] |
|
if (seg && PRIMARY_PAGE_LABEL[seg]) { |
|
const label = PRIMARY_PAGE_LABEL[seg] |
|
pageTitle = `${label} · ${SITE_NAME}` |
|
ogTitle = pageTitle |
|
description = `${label} in ${SITE_NAME}. ${SITE_TAGLINE}` |
|
} else if (currentPrimaryPage && PRIMARY_PAGE_LABEL[currentPrimaryPage]) { |
|
const label = PRIMARY_PAGE_LABEL[currentPrimaryPage] |
|
pageTitle = `${label} · ${SITE_NAME}` |
|
ogTitle = pageTitle |
|
description = `${label} in ${SITE_NAME}. ${SITE_TAGLINE}` |
|
} |
|
} |
|
|
|
return { pageTitle, ogTitle, description } |
|
} |
|
|
|
/** |
|
* Browser tab + social tags for routes that do not set their own (e.g. settings, lists). |
|
* Note and profile detail pages set richer tags in their components. |
|
*/ |
|
export function applyRouteDocumentMeta(pathname: string, currentPrimaryPage: string): void { |
|
if (typeof window === 'undefined') return |
|
|
|
const { pageTitle, ogTitle, description } = resolveImwaldRouteSocialCopy(pathname, currentPrimaryPage) |
|
const href = window.location.href |
|
const img = defaultOgImageAbsoluteUrl() |
|
|
|
document.title = pageTitle |
|
|
|
updateMetaTag('og:title', ogTitle) |
|
updateMetaTag('og:description', description.length > 300 ? description.slice(0, 297) + '...' : description) |
|
updateMetaTag('og:image', img) |
|
updateMetaTag('og:type', 'website') |
|
updateMetaTag('og:url', href) |
|
updateMetaTag('og:site_name', SITE_NAME) |
|
|
|
updateMetaTag('twitter:card', 'summary_large_image') |
|
updateMetaTag('twitter:title', ogTitle) |
|
updateMetaTag( |
|
'twitter:description', |
|
description.length > 200 ? description.slice(0, 197) + '...' : description |
|
) |
|
updateMetaTag('twitter:image', img) |
|
|
|
removeMetaByProperty('article:tag') |
|
removeMetaByProperty('article:author') |
|
document.querySelector('meta[property="article:author:url"]')?.remove() |
|
}
|
|
|