feat(readability): render Markdown when proxy provides it

- Detect markdown blocks from r.jina.ai output
- Add react-markdown + remark-gfm for rendering
- Extend ContentPanel to render markdown or HTML
- Add styles for markdown content
This commit is contained in:
Gigi
2025-10-02 23:46:33 +02:00
parent 80408148fb
commit 719ddf3f0b
4 changed files with 71 additions and 5 deletions

View File

@@ -125,6 +125,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
loading={readerLoading}
title={readerContent?.title}
html={readerContent?.html}
markdown={readerContent?.markdown}
selectedUrl={selectedUrl}
/>
</div>

View File

@@ -1,13 +1,16 @@
import React from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
interface ContentPanelProps {
loading: boolean
title?: string
html?: string
markdown?: string
selectedUrl?: string
}
const ContentPanel: React.FC<ContentPanelProps> = ({ loading, title, html, selectedUrl }) => {
const ContentPanel: React.FC<ContentPanelProps> = ({ loading, title, html, markdown, selectedUrl }) => {
if (!selectedUrl) {
return (
<div className="content-panel empty">
@@ -27,7 +30,13 @@ const ContentPanel: React.FC<ContentPanelProps> = ({ loading, title, html, selec
return (
<div className="content-panel">
{title && <h2 className="content-title">{title}</h2>}
{html ? (
{markdown ? (
<div className="content-markdown">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{markdown}
</ReactMarkdown>
</div>
) : html ? (
<div className="content-html" dangerouslySetInnerHTML={{ __html: html }} />
) : (
<div className="content-panel empty">

View File

@@ -298,6 +298,45 @@ body {
word-break: break-word;
}
.content-markdown {
color: #ddd;
line-height: 1.7;
}
.content-markdown h1,
.content-markdown h2,
.content-markdown h3,
.content-markdown h4 {
margin-top: 1.2rem;
}
.content-markdown p {
margin: 0.5rem 0;
}
.content-markdown a {
color: #8ab4f8;
text-decoration: none;
}
.content-markdown a:hover { text-decoration: underline; }
.content-markdown pre,
.content-markdown code {
background: #111;
border: 1px solid #333;
border-radius: 6px;
}
.content-markdown pre {
padding: 0.75rem;
overflow: auto;
}
.content-markdown code {
padding: 0.1rem 0.3rem;
}
.bookmark-item {
background: #1a1a1a;
padding: 1.5rem;

View File

@@ -4,7 +4,8 @@
export interface ReadableContent {
url: string
title?: string
html: string
html?: string
markdown?: string
}
function toProxyUrl(url: string): string {
@@ -19,8 +20,24 @@ export async function fetchReadableContent(targetUrl: string): Promise<ReadableC
if (!res.ok) {
throw new Error(`Failed to fetch readable content (${res.status})`)
}
const html = await res.text()
// Best-effort title extraction
const text = await res.text()
// Detect if the proxy delivered Markdown or HTML. r.jina.ai often returns a
// block starting with "Title:" and "Markdown Content:". We handle both.
const hasMarkdownBlock = /Markdown Content:\s/i.test(text)
if (hasMarkdownBlock) {
// Try to split out Title and the Markdown payload
const titleMatch = text.match(/Title:\s*(.*?)(?:\s+URL Source:|\s+Markdown Content:)/i)
const mdMatch = text.match(/Markdown Content:\s*([\s\S]*)$/i)
return {
url: targetUrl,
title: titleMatch?.[1]?.trim(),
markdown: mdMatch?.[1]?.trim()
}
}
const html = text
// Best-effort title extraction from HTML
const match = html.match(/<title[^>]*>(.*?)<\/title>/i)
return {
url: targetUrl,