feat: parse and display summary tag for nostr articles

- Extract 'summary' tag from kind:30023 article bookmarks
- Display summary in place of truncated content for articles
- Show summary in all view modes (compact, cards, large)
- Add article-summary CSS class for potential styling
- Follows NIP-23 long-form content specification
This commit is contained in:
Gigi
2025-10-07 05:12:11 +01:00
parent 0124de8318
commit fd28a6e171
4 changed files with 30 additions and 9 deletions

View File

@@ -37,11 +37,12 @@ export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index, onS
const firstUrl = hasUrls ? extractedUrls[0] : null
const firstUrlClassification = firstUrl ? classifyUrl(firstUrl) : null
// For kind:30023 articles, extract image tag (per NIP-23)
// For kind:30023 articles, extract image and summary tags (per NIP-23)
// Note: We extract directly from tags here since we don't have the full event.
// When we have full events, we use getArticleImage() helper (see articleService.ts)
const isArticle = bookmark.kind === 30023
const articleImage = isArticle ? bookmark.tags.find(t => t[0] === 'image')?.[1] : undefined
const articleSummary = isArticle ? bookmark.tags.find(t => t[0] === 'summary')?.[1] : undefined
// Fetch OG image for large view (hook must be at top level)
const instantPreview = firstUrl ? getPreviewImage(firstUrl, firstUrlClassification?.type || '') : null
@@ -113,7 +114,8 @@ export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index, onS
eventNevent,
getAuthorDisplayName,
handleReadNow,
articleImage
articleImage,
articleSummary
}
if (viewMode === 'compact') {

View File

@@ -21,6 +21,7 @@ interface CardViewProps {
getAuthorDisplayName: () => string
handleReadNow: (e: React.MouseEvent<HTMLButtonElement>) => void
articleImage?: string
articleSummary?: string
}
export const CardView: React.FC<CardViewProps> = ({
@@ -35,7 +36,8 @@ export const CardView: React.FC<CardViewProps> = ({
eventNevent,
getAuthorDisplayName,
handleReadNow,
articleImage
articleImage,
articleSummary
}) => {
const [expanded, setExpanded] = useState(false)
const [urlsExpanded, setUrlsExpanded] = useState(false)
@@ -122,7 +124,11 @@ export const CardView: React.FC<CardViewProps> = ({
</div>
)}
{bookmark.parsedContent ? (
{isArticle && articleSummary ? (
<div className="bookmark-content article-summary">
<ContentWithResolvedProfiles content={articleSummary} />
</div>
) : bookmark.parsedContent ? (
<div className="bookmark-content">
{shouldTruncate && bookmark.content
? <ContentWithResolvedProfiles content={`${bookmark.content.slice(0, 210).trimEnd()}`} />

View File

@@ -15,6 +15,7 @@ interface CompactViewProps {
getIconForUrlType: IconGetter
firstUrlClassification: { buttonText: string } | null
articleImage?: string
articleSummary?: string
}
export const CompactView: React.FC<CompactViewProps> = ({
@@ -24,7 +25,8 @@ export const CompactView: React.FC<CompactViewProps> = ({
extractedUrls,
onSelectUrl,
getIconForUrlType,
firstUrlClassification
firstUrlClassification,
articleSummary
}) => {
const isArticle = bookmark.kind === 30023
const isWebBookmark = bookmark.kind === 39701
@@ -40,6 +42,11 @@ export const CompactView: React.FC<CompactViewProps> = ({
}
}
// For articles, prefer summary; for others, use content
const displayText = isArticle && articleSummary
? articleSummary
: bookmark.content
return (
<div key={`${bookmark.id}-${index}`} className={`individual-bookmark compact ${bookmark.isPrivate ? 'private-bookmark' : ''}`}>
<div
@@ -63,9 +70,9 @@ export const CompactView: React.FC<CompactViewProps> = ({
<FontAwesomeIcon icon={faBookmark} className="bookmark-visibility public" />
)}
</span>
{bookmark.content && (
{displayText && (
<div className="compact-text">
<ContentWithResolvedProfiles content={bookmark.content.slice(0, 60) + (bookmark.content.length > 60 ? '…' : '')} />
<ContentWithResolvedProfiles content={displayText.slice(0, 60) + (displayText.length > 60 ? '…' : '')} />
</div>
)}
<span className="bookmark-date-compact">{formatDate(bookmark.created_at)}</span>

View File

@@ -18,6 +18,7 @@ interface LargeViewProps {
eventNevent?: string
getAuthorDisplayName: () => string
handleReadNow: (e: React.MouseEvent<HTMLButtonElement>) => void
articleSummary?: string
}
export const LargeView: React.FC<LargeViewProps> = ({
@@ -32,7 +33,8 @@ export const LargeView: React.FC<LargeViewProps> = ({
authorNpub,
eventNevent,
getAuthorDisplayName,
handleReadNow
handleReadNow,
articleSummary
}) => {
const isArticle = bookmark.kind === 30023
@@ -59,7 +61,11 @@ export const LargeView: React.FC<LargeViewProps> = ({
)}
<div className="large-content">
{bookmark.content && (
{isArticle && articleSummary ? (
<div className="large-text article-summary">
<ContentWithResolvedProfiles content={articleSummary} />
</div>
) : bookmark.content && (
<div className="large-text">
<ContentWithResolvedProfiles content={bookmark.content} />
</div>