diff --git a/frontend/src/components/layout/NewsMarkdown.tsx b/frontend/src/components/layout/NewsMarkdown.tsx index 686b3afa..289eab99 100644 --- a/frontend/src/components/layout/NewsMarkdown.tsx +++ b/frontend/src/components/layout/NewsMarkdown.tsx @@ -12,6 +12,9 @@ * news posts embed screenshots/demos, so viewers' browsers DO fetch * remote media from the posts — documented in the OSS README next * to the VELXIO_NEWS opt-out. + * - A bare image URL (autolinked by GFM, link text == href) renders + * as the image itself — authors paste screenshot URLs without + * knowing the ![](…) syntax. An explicit [label](…png) stays a link. * - All links open in a new tab (the modal sits on top of the editor; * navigating away would lose workspace state). */ @@ -20,6 +23,13 @@ import { useState } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; +function isImageHref(href: string): boolean { + return ( + /^https?:\/\//i.test(href) && + /\.(png|jpe?g|gif|webp|avif|svg)(?=$|[?#])/i.test(href) + ); +} + function youTubeId(href: string): string | null { const m = href.match( /^https?:\/\/(?:www\.|m\.)?(?:youtube\.com\/(?:watch\?(?:.*&)?v=|shorts\/|embed\/)|youtu\.be\/)([\w-]{11})/, @@ -60,9 +70,9 @@ export function NewsMarkdown({ children }: { children: string }) { remarkPlugins={[remarkGfm]} components={{ a: ({ href, children: kids }) => { + const text = Array.isArray(kids) ? kids.join('') : String(kids ?? ''); const id = href ? youTubeId(href) : null; if (id) { - const text = Array.isArray(kids) ? kids.join('') : String(kids ?? ''); return ( <> @@ -72,10 +82,16 @@ export function NewsMarkdown({ children }: { children: string }) { ); } + if (href && isImageHref(href) && (!text || text === href)) { + return ; + } return ( {kids} ); }, + img: ({ src, alt }) => ( + {alt + ), }} > {children}