From 7c638af3229132e9223105fafdf70ce49e9b44cf Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 1 Nov 2025 21:10:31 +0100 Subject: [PATCH] fixed discussions rendering --- PROXY_SETUP.md | 183 +++++++++++++++++- .../Note/MarkdownArticle/MarkdownArticle.tsx | 81 ++++++-- .../MarkdownArticle/preprocessMediaLinks.ts | 78 ++++++++ src/services/web.service.ts | 5 + 4 files changed, 325 insertions(+), 22 deletions(-) create mode 100644 src/components/Note/MarkdownArticle/preprocessMediaLinks.ts diff --git a/PROXY_SETUP.md b/PROXY_SETUP.md index c08779b..550ad8e 100644 --- a/PROXY_SETUP.md +++ b/PROXY_SETUP.md @@ -157,15 +157,23 @@ sudo systemctl restart apache2 ServerAlias www.jumble.imwald.eu # Reverse Proxy Configuration - ProxyPreserveHost On # Proxy for the jumble-proxy-server (must come BEFORE the catch-all / rule) # The code constructs: ${proxyServer}/sites/${encodeURIComponent(url)} # So /proxy/sites/... needs to be forwarded to http://127.0.0.1:8090/sites/... - ProxyPass /proxy/ http://127.0.0.1:8090/ - ProxyPassReverse /proxy/ http://127.0.0.1:8090/ + # IMPORTANT: Use Location block to scope headers properly for /proxy/ path only + + ProxyPreserveHost Off + ProxyPass http://127.0.0.1:8090/ + ProxyPassReverse http://127.0.0.1:8090/ + # Unset forwarded headers that might make the proxy server use Host header instead of URL path + RequestHeader unset X-Forwarded-Host + RequestHeader unset X-Forwarded-Server + RequestHeader set Host "127.0.0.1:8090" + - # Reverse Proxy for the main Jumble app + # Reverse Proxy for the main Jumble app (needs Host header preserved) + ProxyPreserveHost On ProxyPass / http://127.0.0.1:32768/ ProxyPassReverse / http://127.0.0.1:32768/ @@ -196,10 +204,32 @@ sudo systemctl reload apache2 5. **Test the proxy route:** ```bash # Test with a real URL - the code constructs /proxy/sites/{encoded-url} -curl -I https://jumble.imwald.eu/proxy/sites/https%3A%2F%2Fexample.com -# Should return 200 OK if working correctly +curl https://jumble.imwald.eu/proxy/sites/https%3A%2F%2Fexample.com +# Should return example.com's HTML, NOT jumble.imwald.eu's HTML +# If you see Jumble HTML, the proxy server is using the Host header instead of the URL path +``` + +**If the test returns Jumble HTML instead of the requested site's HTML:** + +The proxy server is using the `Host` header (`jumble.imwald.eu`) to determine what to fetch. Update your Apache config to use `ProxyPreserveHost Off` for the `/proxy/` path: + +```apache +# In your Apache config, change from: +ProxyPreserveHost On +ProxyPass /proxy/ http://127.0.0.1:8090/ + +# To: +ProxyPreserveHost Off +ProxyPass /proxy/ http://127.0.0.1:8090/ +ProxyPassReverse /proxy/ http://127.0.0.1:8090/ + +# Then set it back to On for the main app: +ProxyPreserveHost On +ProxyPass / http://127.0.0.1:32768/ ``` +Then reload Apache and test again. + 6. **Build with the proxy URL:** ```bash docker build \ @@ -220,6 +250,147 @@ docker build \ ## Troubleshooting +### If Proxy Returns Jumble HTML Instead of Requested Site + +If you've set `ProxyPreserveHost Off` but still get Jumble HTML, test the proxy server directly: + +**1. Test the proxy server directly (bypassing Apache):** +```bash +# Test direct connection to proxy on port 8090 +curl http://127.0.0.1:8090/sites/https%3A%2F%2Fexample.com +# Should return example.com's HTML, NOT jumble.imwald.eu's HTML +``` + +**2. Check proxy server logs:** +```bash +docker logs imwald-jumble-proxy --tail 50 +# Look for what URL the proxy is trying to fetch +# This will show if the proxy server is receiving the correct path or if it's using Host header +``` + +**2b. Check what request the proxy server is actually receiving:** +```bash +# Enable verbose logging in Apache to see what it's forwarding +# Or check what the proxy receives by making a test request and watching logs: +docker logs -f imwald-jumble-proxy & +# Then in another terminal: +curl -v https://jumble.imwald.eu/proxy/sites/https%3A%2F%2Fexample.com +# Look at the proxy logs to see what URL it tried to fetch +``` + +**3. Check if Apache is receiving `/proxy/` requests:** +```bash +# Watch Apache access log to see if /proxy/ requests reach Apache +sudo tail -f /var/log/apache2/access.log | grep proxy +# Then in another terminal, make a request: +curl https://jumble.imwald.eu/proxy/sites/https%3A%2F%2Fexample.com +# Check if the request appears in Apache logs +``` + +**3b. Check what Apache is forwarding:** +```bash +# Check Apache error log for proxy-related entries +sudo tail -f /var/log/apache2/error.log +# Then make a request and see what Apache is doing +``` + +**3c. Verify Apache config is being used:** +```bash +# Check that your Location block syntax is correct: +sudo apache2ctl -S | grep jumble.imwald.eu +# Make sure the site is enabled and config syntax is correct: +sudo apache2ctl configtest +``` + +**4. Possible Issues:** + +**IMPORTANT: If you see `Server: nginx` in response headers, nginx is in front of Apache!** + +If you see `Server: nginx/1.29.3` or `X-Powered-By: PleskLin` in the response headers, nginx reverse proxy is handling requests before Apache. You need to configure nginx directly (bypassing Plesk interface) to pass `/proxy/` to Apache, or route it directly to the proxy server. + +**If nginx is in front of Apache (bypassing Plesk interface):** + +Since nginx is handling requests before Apache, configure nginx directly by editing nginx config files. + +**Option A: Configure nginx to pass `/proxy/` through to Apache:** + +1. Find nginx config for your domain: +```bash +# Plesk usually stores configs here: +/etc/nginx/conf.d/vhost.conf +# or +/etc/nginx/conf.d/jumble.imwald.eu.conf +# or check Plesk's nginx vhosts: +/etc/nginx/plesk.conf.d/vhosts/jumble.imwald.eu.conf +``` + +2. Edit the nginx config file for `jumble.imwald.eu` + +3. Add this location block BEFORE any catch-all location: +```nginx +location /proxy/ { + # Forward to Apache (check what port Apache is listening on) + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +} +``` + +**Note:** Replace `8080` with the port Apache is actually listening on. Check with: +```bash +sudo netstat -tlnp | grep apache +# or +sudo ss -tlnp | grep apache +``` + +4. Test nginx config: `sudo nginx -t` +5. Reload nginx: `sudo systemctl reload nginx` + +**Then Apache will handle it with your existing Apache config.** + +**Option B: Configure nginx to route `/proxy/` directly to the proxy server (simpler):** + +1. Edit nginx config for `jumble.imwald.eu` (same location as above) +2. Add this location block BEFORE any catch-all location: +```nginx +location /proxy/ { + proxy_pass http://127.0.0.1:8090/; + proxy_set_header Host 127.0.0.1:8090; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # Don't send X-Forwarded-Host which might confuse the proxy + proxy_set_header X-Forwarded-Host ""; +} +``` + +3. Test nginx config: `sudo nginx -t` +4. Reload nginx: `sudo systemctl reload nginx` + +This bypasses Apache entirely for `/proxy/` requests and goes directly to the proxy server. + +**Other possible issues:** +- The proxy server might be using the `X-Forwarded-Host` header instead of the URL path +- The proxy server might need a specific Host header value +- The proxy server might not be correctly parsing `/sites/...` path + +**5. Unset forwarded headers that might confuse the proxy server:** +```apache +# The proxy server might be using X-Forwarded-Host instead of the URL path +# Unset or modify these headers for the /proxy/ path: +ProxyPass /proxy/ http://127.0.0.1:8090/ +ProxyPassReverse /proxy/ http://127.0.0.1:8090/ +ProxyPreserveHost Off +# Unset forwarded headers that might interfere +RequestHeader unset X-Forwarded-Host +RequestHeader unset X-Forwarded-Server +RequestHeader set Host "127.0.0.1:8090" +``` + +### Other Console Errors + If you see errors in the console: - `[WebService] No proxy server configured` - `VITE_PROXY_SERVER` is undefined or empty - `[WebService] CORS/Network error` - The proxy URL might be wrong, or CORS isn't configured diff --git a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx index 3b174a9..4890699 100644 --- a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx +++ b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx @@ -6,7 +6,7 @@ import WebPreview from '@/components/WebPreview' import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata' import { toNote, toNoteList, toProfile } from '@/lib/link' import { useMediaExtraction } from '@/hooks' -import { cleanUrl, isImage, isMedia } from '@/lib/url' +import { cleanUrl, isImage, isMedia, isVideo, isAudio } from '@/lib/url' import { ExternalLink } from 'lucide-react' import { Event, kinds } from 'nostr-tools' import { ExtendedKind } from '@/constants' @@ -22,6 +22,7 @@ import NostrNode from './NostrNode' import { remarkNostr } from './remarkNostr' import { remarkHashtags } from './remarkHashtags' import { remarkUnwrapImages } from './remarkUnwrapImages' +import { preprocessMediaLinks } from './preprocessMediaLinks' import { Components } from './types' export default function MarkdownArticle({ @@ -40,6 +41,11 @@ export default function MarkdownArticle({ const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event]) const contentRef = useRef(null) + // Preprocess content to convert plain media URLs to markdown syntax + const processedContent = useMemo(() => { + return preprocessMediaLinks(event.content) + }, [event.content]) + // Use unified media extraction service const extractedMedia = useMediaExtraction(event, event.content) @@ -55,6 +61,17 @@ export default function MarkdownArticle({ return hashtags }, [event.content]) + // Extract media URLs that are in the content (so we don't render them twice) + const mediaUrlsInContent = useMemo(() => { + const urls = new Set() + const mediaUrlRegex = /(https?:\/\/[^\s<>"']+\.(jpg|jpeg|png|gif|webp|svg|heic|mp4|webm|ogg|mov|avi|wmv|flv|mkv|m4v|mp3|wav|flac|aac|m4a|opus|wma)(\?[^\s<>"']*)?)/gi + let match + while ((match = mediaUrlRegex.exec(event.content)) !== null) { + urls.add(cleanUrl(match[0])) + } + return urls + }, [event.content]) + // All images from useMediaExtraction are already cleaned and deduplicated // This includes images from content, tags, imeta, r tags, etc. const allImages = extractedMedia.images @@ -205,9 +222,22 @@ export default function MarkdownArticle({ child => React.isValidElement(child) && child.type === Image ) - // If link contains an image, let the image handle the click for lightbox - // Just wrap it in an anchor that won't interfere with image clicks + // If link contains only an image, render just the image without the link wrapper + // This prevents the image from opening as a file - clicking opens lightbox instead if (hasImage) { + // Check if this is just an image with no other content + const childrenArray = React.Children.toArray(children) + const onlyImage = childrenArray.length === 1 && + React.isValidElement(childrenArray[0]) && + childrenArray[0].type === Image + + if (onlyImage) { + // Just render the image directly, no link wrapper + return <>{children} + } + + // If there's text along with the image, keep the link wrapper + // but prevent navigation when clicking the image itself return ( - - - ) + return } return ( @@ -381,8 +407,20 @@ export default function MarkdownArticle({ img: ({ src }) => { if (!src) return null - // Find the index of this image in allImages (includes content and tags, already deduplicated) const cleanedSrc = cleanUrl(src) + + // Check if this is actually a video or audio URL (converted by remarkMedia) + if (cleanedSrc && (isVideo(cleanedSrc) || isAudio(cleanedSrc))) { + return ( + + ) + } + + // Find the index of this image in allImages (includes content and tags, already deduplicated) const imageIndex = cleanedSrc ? allImages.findIndex(img => cleanUrl(img.url) === cleanedSrc) : -1 @@ -394,7 +432,7 @@ export default function MarkdownArticle({ image={{ url: src, pubkey: event.pubkey }} className="max-w-[400px] rounded-lg my-2 cursor-zoom-in" classNames={{ - wrapper: 'rounded-lg', + wrapper: 'rounded-lg inline-block', errorPlaceholder: 'aspect-square h-[30vh]' }} data-markdown-image="true" @@ -565,6 +603,16 @@ export default function MarkdownArticle({ color: #86efac !important; /* Tailwind green-300 */ text-decoration: underline !important; } + /* Make images display inline-block so they can wrap horizontally */ + .prose span[data-markdown-image] { + display: inline-block !important; + margin: 0.5rem !important; + } + /* When images are in paragraphs, make those paragraphs inline or flex */ + .prose p:has(span[data-markdown-image]:only-child) { + display: inline-block; + width: 100%; + } `}
- {event.content} + {processedContent} {/* Inline Media - Show for non-article content (kinds 1, 11, 1111) */} - {!showImageGallery && extractedMedia.videos.length > 0 && ( + {/* Only render media that's not already in the content (from tags, imeta, etc.) */} + {!showImageGallery && extractedMedia.videos.filter(v => !mediaUrlsInContent.has(v.url)).length > 0 && (
- {extractedMedia.videos.map((video) => ( + {extractedMedia.videos.filter(v => !mediaUrlsInContent.has(v.url)).map((video) => ( ))}
)} - {!showImageGallery && extractedMedia.audio.length > 0 && ( + {!showImageGallery && extractedMedia.audio.filter(a => !mediaUrlsInContent.has(a.url)).length > 0 && (
- {extractedMedia.audio.map((audio) => ( + {extractedMedia.audio.filter(a => !mediaUrlsInContent.has(a.url)).map((audio) => ( ))}
diff --git a/src/components/Note/MarkdownArticle/preprocessMediaLinks.ts b/src/components/Note/MarkdownArticle/preprocessMediaLinks.ts new file mode 100644 index 0000000..216492b --- /dev/null +++ b/src/components/Note/MarkdownArticle/preprocessMediaLinks.ts @@ -0,0 +1,78 @@ +import { isImage, isVideo, isAudio } from '@/lib/url' + +/** + * Preprocess markdown content to convert plain media URLs to proper markdown syntax + * - Images: `https://example.com/image.png` -> `![](https://example.com/image.png)` + * - Videos: `https://example.com/video.mp4` -> `![](https://example.com/video.mp4)` + * - Audio: `https://example.com/audio.mp3` -> `![](https://example.com/audio.mp3)` + */ +export function preprocessMediaLinks(content: string): string { + let processed = content + + // Find all matches but process them manually to avoid complex regex lookbehind + const allMatches: Array<{ url: string; index: number }> = [] + let match + + // Find all candidate URLs + const tempRegex = /https?:\/\/[^\s<>"']+/gi + while ((match = tempRegex.exec(content)) !== null) { + const index = match.index + const url = match[0] + const before = content.substring(Math.max(0, index - 10), index) + + // Check if this URL is already part of markdown syntax + // Skip if preceded by: [text](url, ![text](url, or ](url + if (before.match(/\[[^\]]*$/) || before.match(/\]\([^)]*$/) || before.match(/!\[[^\]]*$/)) { + continue + } + + allMatches.push({ url, index }) + } + + // Process in reverse order to preserve indices + for (let i = allMatches.length - 1; i >= 0; i--) { + const { url, index } = allMatches[i] + + // Check if URL is in code block + const beforeUrl = content.substring(0, index) + const backticksCount = (beforeUrl.match(/```/g) || []).length + if (backticksCount % 2 === 1) { + continue // In code block + } + + // Check if URL is in inline code + const lastBacktick = beforeUrl.lastIndexOf('`') + if (lastBacktick !== -1) { + const afterUrl = content.substring(index + url.length) + const nextBacktick = afterUrl.indexOf('`') + if (nextBacktick !== -1) { + const codeBefore = beforeUrl.substring(lastBacktick + 1) + const codeAfter = afterUrl.substring(0, nextBacktick) + // If no newlines between backticks, it's inline code + if (!codeBefore.includes('\n') && !codeAfter.includes('\n')) { + continue + } + } + } + + // Check if it's a media URL + const isImageUrl = isImage(url) + const isVideoUrl = isVideo(url) + const isAudioUrl = isAudio(url) + + let replacement: string + if (isImageUrl || isVideoUrl || isAudioUrl) { + // Media URLs: convert to ![](url) + replacement = `![](${url})` + } else { + // Don't convert non-media URLs - let autolink handle them + continue + } + + // Replace the URL + processed = processed.substring(0, index) + replacement + processed.substring(index + url.length) + } + + return processed +} + diff --git a/src/services/web.service.ts b/src/services/web.service.ts index 701aed5..f034dab 100644 --- a/src/services/web.service.ts +++ b/src/services/web.service.ts @@ -56,6 +56,11 @@ class WebService { } const html = await res.text() + + // Debug: Log a snippet of the HTML to see what we're getting + const htmlSnippet = html.substring(0, 500) + console.log(`[WebService] Received HTML snippet for ${url} (via ${fetchUrl}):`, htmlSnippet) + const parser = new DOMParser() const doc = parser.parseFromString(html, 'text/html')