fix: properly render links and formatting in RSS feeds
Replace broken text extraction logic with renderEdraContent() which correctly handles TipTap marks including links, bold, italic, etc. Before: Links were stripped from RSS content (only plain text extracted) After: Full HTML with proper links, formatting preserved in CDATA sections Files updated: - src/routes/rss/+server.ts: Main RSS feed - src/routes/rss/universe/+server.ts: Universe RSS feed Fixes issue where content.blocks was expected but TipTap uses content.type='doc' format.
This commit is contained in:
parent
86b072c70f
commit
ffa5ae7f02
2 changed files with 12 additions and 165 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import type { RequestHandler } from './$types'
|
||||
import { prisma } from '$lib/server/database'
|
||||
import { logger } from '$lib/server/logger'
|
||||
import { renderEdraContent } from '$lib/utils/content'
|
||||
|
||||
// Helper function to escape XML special characters
|
||||
function escapeXML(str: string): string {
|
||||
|
|
@ -14,159 +15,18 @@ function escapeXML(str: string): string {
|
|||
}
|
||||
|
||||
// Helper function to convert content to HTML for full content
|
||||
// Uses the same rendering logic as the website for consistency
|
||||
function convertContentToHTML(content: any): string {
|
||||
if (!content) return ''
|
||||
|
||||
|
||||
// Handle legacy content format (if it's just a string)
|
||||
if (typeof content === 'string') {
|
||||
return `<p>${escapeXML(content)}</p>`
|
||||
}
|
||||
|
||||
// Handle TipTap/Edra format with doc root
|
||||
if (content.type === 'doc' && content.content && Array.isArray(content.content)) {
|
||||
return content.content
|
||||
.map((node: any) => {
|
||||
switch (node.type) {
|
||||
case 'paragraph':
|
||||
const text = extractTextFromNode(node)
|
||||
return text ? `<p>${text}</p>` : ''
|
||||
case 'heading':
|
||||
const headingText = extractTextFromNode(node)
|
||||
const level = node.attrs?.level || 2
|
||||
return headingText ? `<h${level}>${headingText}</h${level}>` : ''
|
||||
case 'bulletList':
|
||||
case 'orderedList':
|
||||
const listItems = (node.content || [])
|
||||
.map((item: any) => {
|
||||
if (item.type === 'listItem') {
|
||||
const itemText = extractTextFromNode(item)
|
||||
return itemText ? `<li>${itemText}</li>` : ''
|
||||
}
|
||||
return ''
|
||||
})
|
||||
.filter((item: string) => item)
|
||||
.join('')
|
||||
if (!listItems) return ''
|
||||
return node.type === 'orderedList' ? `<ol>${listItems}</ol>` : `<ul>${listItems}</ul>`
|
||||
case 'blockquote':
|
||||
const quoteText = extractTextFromNode(node)
|
||||
return quoteText ? `<blockquote>${quoteText}</blockquote>` : ''
|
||||
case 'codeBlock':
|
||||
const code = extractTextFromNode(node)
|
||||
return code ? `<pre><code>${code}</code></pre>` : ''
|
||||
case 'image':
|
||||
const src = node.attrs?.src || ''
|
||||
const alt = node.attrs?.alt || ''
|
||||
return src ? `<figure><img src="${escapeXML(src)}" alt="${escapeXML(alt)}" /></figure>` : ''
|
||||
case 'video':
|
||||
const videoSrc = node.attrs?.src || ''
|
||||
if (!videoSrc) return ''
|
||||
// Check if it's a YouTube URL
|
||||
const youtubeMatch = videoSrc.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/)
|
||||
if (youtubeMatch) {
|
||||
const videoId = youtubeMatch[1]
|
||||
return `<div class="video-embed"><iframe width="560" height="315" src="https://www.youtube.com/embed/${escapeXML(videoId)}" frameborder="0" allowfullscreen></iframe><p><a href="${escapeXML(videoSrc)}">Watch on YouTube</a></p></div>`
|
||||
}
|
||||
// For other video sources, include a video tag
|
||||
return `<video controls><source src="${escapeXML(videoSrc)}" type="video/mp4">Your browser does not support the video tag. <a href="${escapeXML(videoSrc)}">Download video</a></video>`
|
||||
case 'urlEmbed':
|
||||
const embedUrl = node.attrs?.url || ''
|
||||
const embedTitle = node.attrs?.title || ''
|
||||
const embedDescription = node.attrs?.description || ''
|
||||
const embedImage = node.attrs?.image || ''
|
||||
const embedSiteName = node.attrs?.siteName || ''
|
||||
|
||||
if (!embedUrl) return ''
|
||||
|
||||
// Check if it's a YouTube URL
|
||||
const ytMatch = embedUrl.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/)
|
||||
if (ytMatch) {
|
||||
const videoId = ytMatch[1]
|
||||
let html = '<div class="url-embed youtube-embed">'
|
||||
html += `<iframe width="560" height="315" src="https://www.youtube.com/embed/${escapeXML(videoId)}" frameborder="0" allowfullscreen></iframe>`
|
||||
if (embedTitle) {
|
||||
html += `<h3><a href="${escapeXML(embedUrl)}">${escapeXML(embedTitle)}</a></h3>`
|
||||
}
|
||||
if (embedDescription) {
|
||||
html += `<p>${escapeXML(embedDescription)}</p>`
|
||||
}
|
||||
html += '</div>'
|
||||
return html
|
||||
}
|
||||
|
||||
// Check if it's a Twitter/X URL
|
||||
const twitterMatch = embedUrl.match(/(?:twitter\.com|x\.com)\/\w+\/status\/(\d+)/)
|
||||
if (twitterMatch) {
|
||||
// For Twitter/X, we can't embed the actual tweet in RSS, but we can provide a nice preview
|
||||
let html = '<div class="url-embed twitter-embed">'
|
||||
if (embedImage) {
|
||||
html += `<img src="${escapeXML(embedImage)}" alt="Tweet preview" />`
|
||||
}
|
||||
html += '<div class="embed-content">'
|
||||
html += `<span class="site-name">𝕏 (Twitter)</span>`
|
||||
if (embedTitle || embedDescription) {
|
||||
html += `<p>${escapeXML(embedDescription || embedTitle || '')}</p>`
|
||||
}
|
||||
html += `<p><a href="${escapeXML(embedUrl)}">View on 𝕏</a></p>`
|
||||
html += '</div></div>'
|
||||
return html
|
||||
}
|
||||
|
||||
// For other URL embeds, create a rich preview
|
||||
let html = '<div class="url-embed">'
|
||||
if (embedImage) {
|
||||
html += `<img src="${escapeXML(embedImage)}" alt="${escapeXML(embedTitle || '')}" />`
|
||||
}
|
||||
html += '<div class="embed-content">'
|
||||
if (embedSiteName) {
|
||||
html += `<span class="site-name">${escapeXML(embedSiteName)}</span>`
|
||||
}
|
||||
if (embedTitle) {
|
||||
html += `<h3><a href="${escapeXML(embedUrl)}">${escapeXML(embedTitle)}</a></h3>`
|
||||
}
|
||||
if (embedDescription) {
|
||||
html += `<p>${escapeXML(embedDescription)}</p>`
|
||||
}
|
||||
html += '</div></div>'
|
||||
return html
|
||||
case 'iframe':
|
||||
const iframeSrc = node.attrs?.src || ''
|
||||
const iframeWidth = node.attrs?.width || 560
|
||||
const iframeHeight = node.attrs?.height || 315
|
||||
if (!iframeSrc) return ''
|
||||
return `<iframe src="${escapeXML(iframeSrc)}" width="${iframeWidth}" height="${iframeHeight}" frameborder="0" allowfullscreen></iframe>`
|
||||
default:
|
||||
const defaultText = extractTextFromNode(node)
|
||||
return defaultText ? `<p>${defaultText}</p>` : ''
|
||||
}
|
||||
})
|
||||
.filter((html: string) => html)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
// Helper to extract text from TipTap nodes
|
||||
function extractTextFromNode(node: any): string {
|
||||
if (!node) return ''
|
||||
|
||||
// Direct text content
|
||||
if (node.text) return escapeXML(node.text)
|
||||
|
||||
// Nested content
|
||||
if (node.content && Array.isArray(node.content)) {
|
||||
return node.content
|
||||
.map((child: any) => {
|
||||
if (child.type === 'text') {
|
||||
return escapeXML(child.text || '')
|
||||
}
|
||||
return extractTextFromNode(child)
|
||||
})
|
||||
.join('')
|
||||
}
|
||||
|
||||
return ''
|
||||
// Use the existing renderEdraContent function which properly handles TipTap marks
|
||||
// including links, bold, italic, etc.
|
||||
return renderEdraContent(content)
|
||||
}
|
||||
|
||||
// Helper function to extract text summary from content
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { RequestHandler } from './$types'
|
||||
import { prisma } from '$lib/server/database'
|
||||
import { logger } from '$lib/server/logger'
|
||||
import { renderEdraContent } from '$lib/utils/content'
|
||||
|
||||
// Helper function to escape XML special characters
|
||||
function escapeXML(str: string): string {
|
||||
|
|
@ -14,27 +15,13 @@ function escapeXML(str: string): string {
|
|||
}
|
||||
|
||||
// Helper function to convert content to HTML for full content
|
||||
// Uses the same rendering logic as the website for consistency
|
||||
function convertContentToHTML(content: any): string {
|
||||
if (!content || !content.blocks) return ''
|
||||
if (!content) return ''
|
||||
|
||||
return content.blocks
|
||||
.map((block: any) => {
|
||||
switch (block.type) {
|
||||
case 'paragraph':
|
||||
return `<p>${escapeXML(block.content || '')}</p>`
|
||||
case 'heading':
|
||||
const level = block.level || 2
|
||||
return `<h${level}>${escapeXML(block.content || '')}</h${level}>`
|
||||
case 'list':
|
||||
const items = (block.content || [])
|
||||
.map((item: any) => `<li>${escapeXML(item)}</li>`)
|
||||
.join('')
|
||||
return block.listType === 'ordered' ? `<ol>${items}</ol>` : `<ul>${items}</ul>`
|
||||
default:
|
||||
return `<p>${escapeXML(block.content || '')}</p>`
|
||||
}
|
||||
})
|
||||
.join('\n')
|
||||
// Use the existing renderEdraContent function which properly handles TipTap marks
|
||||
// including links, bold, italic, etc.
|
||||
return renderEdraContent(content)
|
||||
}
|
||||
|
||||
// Helper function to extract text summary from content
|
||||
|
|
|
|||
Loading…
Reference in a new issue