feat: implement dynamic browser title based on content

- Add useDocumentTitle hook to manage document title dynamically
- Update useArticleLoader to set title when articles load
- Update useExternalUrlLoader to set title for external URLs/videos
- Update useEventLoader to set title for events
- Reset title to default when navigating away from content
- Browser title now shows article/video title instead of always 'Boris'
This commit is contained in:
Gigi
2025-10-25 01:07:55 +02:00
parent 1ebaf7ccd2
commit 92145af2bb
5 changed files with 70 additions and 4 deletions

View File

@@ -14,6 +14,7 @@ import { useBookmarksUI } from '../hooks/useBookmarksUI'
import { useRelayStatus } from '../hooks/useRelayStatus' import { useRelayStatus } from '../hooks/useRelayStatus'
import { useOfflineSync } from '../hooks/useOfflineSync' import { useOfflineSync } from '../hooks/useOfflineSync'
import { useEventLoader } from '../hooks/useEventLoader' import { useEventLoader } from '../hooks/useEventLoader'
import { useDocumentTitle } from '../hooks/useDocumentTitle'
import { Bookmark } from '../types/bookmarks' import { Bookmark } from '../types/bookmarks'
import ThreePaneLayout from './ThreePaneLayout' import ThreePaneLayout from './ThreePaneLayout'
import Explore from './Explore' import Explore from './Explore'
@@ -58,6 +59,12 @@ const Bookmarks: React.FC<BookmarksProps> = ({
const showSupport = location.pathname === '/support' const showSupport = location.pathname === '/support'
const eventId = eventIdParam const eventId = eventIdParam
// Manage document title based on current route
const isViewingContent = !!(naddr || externalUrl || eventId)
useDocumentTitle({
title: isViewingContent ? undefined : 'Boris - Read, Highlight, Explore'
})
// Extract tab from explore routes // Extract tab from explore routes
const exploreTab = location.pathname === '/explore/writings' ? 'writings' : 'highlights' const exploreTab = location.pathname === '/explore/writings' ? 'writings' : 'highlights'

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, Dispatch, SetStateAction } from 'react' import { useEffect, useRef, useState, Dispatch, SetStateAction } from 'react'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { RelayPool } from 'applesauce-relay' import { RelayPool } from 'applesauce-relay'
import type { IEventStore } from 'applesauce-core' import type { IEventStore } from 'applesauce-core'
@@ -12,6 +12,7 @@ import { ReadableContent } from '../services/readerService'
import { Highlight } from '../types/highlights' import { Highlight } from '../types/highlights'
import { NostrEvent } from 'nostr-tools' import { NostrEvent } from 'nostr-tools'
import { UserSettings } from '../services/settingsService' import { UserSettings } from '../services/settingsService'
import { useDocumentTitle } from './useDocumentTitle'
interface PreviewData { interface PreviewData {
title: string title: string
@@ -64,6 +65,10 @@ export function useArticleLoader({
// Extract preview data from navigation state (from blog post cards) // Extract preview data from navigation state (from blog post cards)
const previewData = (location.state as { previewData?: PreviewData })?.previewData const previewData = (location.state as { previewData?: PreviewData })?.previewData
// Track the current article title for document title
const [currentTitle, setCurrentTitle] = useState<string | undefined>()
useDocumentTitle({ title: currentTitle })
useEffect(() => { useEffect(() => {
mountedRef.current = true mountedRef.current = true
@@ -82,6 +87,7 @@ export function useArticleLoader({
// If we have preview data from navigation, show it immediately (no skeleton!) // If we have preview data from navigation, show it immediately (no skeleton!)
if (previewData) { if (previewData) {
setCurrentTitle(previewData.title)
setReaderContent({ setReaderContent({
title: previewData.title, title: previewData.title,
markdown: '', // Will be loaded from store or relay markdown: '', // Will be loaded from store or relay
@@ -121,6 +127,7 @@ export function useArticleLoader({
latestEvent = storedEvent as NostrEvent latestEvent = storedEvent as NostrEvent
firstEmitted = true firstEmitted = true
const title = Helpers.getArticleTitle(storedEvent) || 'Untitled Article' const title = Helpers.getArticleTitle(storedEvent) || 'Untitled Article'
setCurrentTitle(title)
const image = Helpers.getArticleImage(storedEvent) const image = Helpers.getArticleImage(storedEvent)
const summary = Helpers.getArticleSummary(storedEvent) const summary = Helpers.getArticleSummary(storedEvent)
const published = Helpers.getArticlePublished(storedEvent) const published = Helpers.getArticlePublished(storedEvent)
@@ -167,6 +174,7 @@ export function useArticleLoader({
if (!firstEmitted) { if (!firstEmitted) {
firstEmitted = true firstEmitted = true
const title = Helpers.getArticleTitle(evt) || 'Untitled Article' const title = Helpers.getArticleTitle(evt) || 'Untitled Article'
setCurrentTitle(title)
const image = Helpers.getArticleImage(evt) const image = Helpers.getArticleImage(evt)
const summary = Helpers.getArticleSummary(evt) const summary = Helpers.getArticleSummary(evt)
const published = Helpers.getArticlePublished(evt) const published = Helpers.getArticlePublished(evt)
@@ -194,6 +202,7 @@ export function useArticleLoader({
const finalEvent = (events.sort((a, b) => b.created_at - a.created_at)[0]) || latestEvent const finalEvent = (events.sort((a, b) => b.created_at - a.created_at)[0]) || latestEvent
if (finalEvent) { if (finalEvent) {
const title = Helpers.getArticleTitle(finalEvent) || 'Untitled Article' const title = Helpers.getArticleTitle(finalEvent) || 'Untitled Article'
setCurrentTitle(title)
const image = Helpers.getArticleImage(finalEvent) const image = Helpers.getArticleImage(finalEvent)
const summary = Helpers.getArticleSummary(finalEvent) const summary = Helpers.getArticleSummary(finalEvent)
const published = Helpers.getArticlePublished(finalEvent) const published = Helpers.getArticlePublished(finalEvent)
@@ -215,6 +224,7 @@ export function useArticleLoader({
// As a last resort, fall back to the legacy helper (which includes cache) // As a last resort, fall back to the legacy helper (which includes cache)
const article = await fetchArticleByNaddr(relayPool, naddr, false, settingsRef.current) const article = await fetchArticleByNaddr(relayPool, naddr, false, settingsRef.current)
if (!mountedRef.current || currentRequestIdRef.current !== requestId) return if (!mountedRef.current || currentRequestIdRef.current !== requestId) return
setCurrentTitle(article.title)
setReaderContent({ setReaderContent({
title: article.title, title: article.title,
markdown: article.markdown, markdown: article.markdown,

View File

@@ -0,0 +1,35 @@
import { useEffect, useRef } from 'react'
const DEFAULT_TITLE = 'Boris - Read, Highlight, Explore'
interface UseDocumentTitleProps {
title?: string
fallback?: string
}
export function useDocumentTitle({ title, fallback }: UseDocumentTitleProps) {
const originalTitleRef = useRef<string>(document.title)
useEffect(() => {
// Store the original title on first mount
if (originalTitleRef.current === DEFAULT_TITLE) {
originalTitleRef.current = document.title
}
// Set the new title if provided, otherwise use fallback or default
const newTitle = title || fallback || DEFAULT_TITLE
document.title = newTitle
// Cleanup: restore original title when component unmounts
return () => {
document.title = originalTitleRef.current
}
}, [title, fallback])
// Return a function to manually reset to default
const resetTitle = () => {
document.title = DEFAULT_TITLE
}
return { resetTitle }
}

View File

@@ -1,10 +1,11 @@
import { useEffect, useCallback } from 'react' import { useEffect, useCallback, useState } from 'react'
import { RelayPool } from 'applesauce-relay' import { RelayPool } from 'applesauce-relay'
import { IEventStore } from 'applesauce-core' import { IEventStore } from 'applesauce-core'
import { NostrEvent } from 'nostr-tools' import { NostrEvent } from 'nostr-tools'
import { ReadableContent } from '../services/readerService' import { ReadableContent } from '../services/readerService'
import { eventManager } from '../services/eventManager' import { eventManager } from '../services/eventManager'
import { fetchProfiles } from '../services/profileService' import { fetchProfiles } from '../services/profileService'
import { useDocumentTitle } from './useDocumentTitle'
interface UseEventLoaderProps { interface UseEventLoaderProps {
eventId?: string eventId?: string
@@ -25,6 +26,9 @@ export function useEventLoader({
setReaderLoading, setReaderLoading,
setIsCollapsed setIsCollapsed
}: UseEventLoaderProps) { }: UseEventLoaderProps) {
// Track the current event title for document title
const [currentTitle, setCurrentTitle] = useState<string | undefined>()
useDocumentTitle({ title: currentTitle })
const displayEvent = useCallback((event: NostrEvent) => { const displayEvent = useCallback((event: NostrEvent) => {
// Escape HTML in content and convert newlines to breaks for plain text display // Escape HTML in content and convert newlines to breaks for plain text display
const escapedContent = event.content const escapedContent = event.content
@@ -46,6 +50,7 @@ export function useEventLoader({
title, title,
published: event.created_at published: event.created_at
} }
setCurrentTitle(title)
setReaderContent(baseContent) setReaderContent(baseContent)
// Background: resolve author profile for kind:1 and update title // Background: resolve author profile for kind:1 and update title
@@ -80,7 +85,9 @@ export function useEventLoader({
} }
if (resolved) { if (resolved) {
setReaderContent({ ...baseContent, title: `Note by @${resolved}` }) const updatedTitle = `Note by @${resolved}`
setCurrentTitle(updatedTitle)
setReaderContent({ ...baseContent, title: updatedTitle })
} }
} catch { } catch {
// ignore profile failures; keep fallback title // ignore profile failures; keep fallback title
@@ -119,6 +126,7 @@ export function useEventLoader({
html: `<div style="padding: 1rem; color: var(--color-error, red);">Failed to load event: ${err instanceof Error ? err.message : 'Unknown error'}</div>`, html: `<div style="padding: 1rem; color: var(--color-error, red);">Failed to load event: ${err instanceof Error ? err.message : 'Unknown error'}</div>`,
title: 'Error' title: 'Error'
} }
setCurrentTitle('Error')
setReaderContent(errorContent) setReaderContent(errorContent)
setReaderLoading(false) setReaderLoading(false)
} }

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef, useMemo } from 'react' import { useEffect, useRef, useMemo, useState } from 'react'
import { RelayPool } from 'applesauce-relay' import { RelayPool } from 'applesauce-relay'
import { IEventStore } from 'applesauce-core' import { IEventStore } from 'applesauce-core'
import { fetchReadableContent, ReadableContent } from '../services/readerService' import { fetchReadableContent, ReadableContent } from '../services/readerService'
@@ -7,6 +7,7 @@ import { Highlight } from '../types/highlights'
import { useStoreTimeline } from './useStoreTimeline' import { useStoreTimeline } from './useStoreTimeline'
import { eventToHighlight } from '../services/highlightEventProcessor' import { eventToHighlight } from '../services/highlightEventProcessor'
import { KINDS } from '../config/kinds' import { KINDS } from '../config/kinds'
import { useDocumentTitle } from './useDocumentTitle'
// Helper to extract filename from URL // Helper to extract filename from URL
function getFilenameFromUrl(url: string): string { function getFilenameFromUrl(url: string): string {
@@ -52,6 +53,10 @@ export function useExternalUrlLoader({
// Track in-flight request to prevent stale updates when switching quickly // Track in-flight request to prevent stale updates when switching quickly
const currentRequestIdRef = useRef(0) const currentRequestIdRef = useRef(0)
// Track the current content title for document title
const [currentTitle, setCurrentTitle] = useState<string | undefined>()
useDocumentTitle({ title: currentTitle })
// Load cached URL-specific highlights from event store // Load cached URL-specific highlights from event store
const urlFilter = useMemo(() => { const urlFilter = useMemo(() => {
if (!url) return null if (!url) return null
@@ -88,6 +93,7 @@ export function useExternalUrlLoader({
if (!mountedRef.current) return if (!mountedRef.current) return
if (currentRequestIdRef.current !== requestId) return if (currentRequestIdRef.current !== requestId) return
setCurrentTitle(content.title)
setReaderContent(content) setReaderContent(content)
setReaderLoading(false) setReaderLoading(false)