From 64deb204aed00b00df731a3019c69f36bf1812c3 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Wed, 5 Aug 2026 16:25:08 +0200 Subject: [PATCH] feat(news): rich media in posts - inline images/GIFs + YouTube click-to-play previews NewsMarkdown (shared renderer): YouTube links render as a thumbnail + play badge that swaps to the youtube-nocookie iframe on click (nothing loads from the player until the user opts in); images render inline; all links open in a new tab. Product decision supersedes the alt-text image stripping: posts embed screenshots/demos, and the README now documents that embedded media loads in the viewer's browser. --- README.md | 2 +- backend/app/api/routes/news.py | 9 +- .../src/components/layout/NewsMarkdown.tsx | 84 +++++++++++++++++++ frontend/src/components/layout/NewsModal.css | 51 +++++++++++ frontend/src/components/layout/NewsModal.tsx | 21 +---- 5 files changed, 144 insertions(+), 23 deletions(-) create mode 100644 frontend/src/components/layout/NewsMarkdown.tsx diff --git a/README.md b/README.md index 3151bc29..dd2a60b7 100644 --- a/README.md +++ b/README.md @@ -345,7 +345,7 @@ change the CORS origin used during local development. | Variable | Default | Description | | --- | --- | --- | | `FRONTEND_URL` | `http://localhost:5173` | Origin allowed by CORS for local Vite dev | -| `VELXIO_NEWS` | `on` | Product news: the backend fetches release notes and product news from velxio.dev's public feed (cached ~6h, anonymous, no identifiers sent) and the editor shows each item once as a "What's New" modal. Set to `off` to disable entirely; an offline host degrades to no news automatically. | +| `VELXIO_NEWS` | `on` | Product news: the backend fetches release notes and product news from velxio.dev's public feed (cached ~6h, anonymous, no identifiers sent) and the editor shows each item once as a "What's New" modal. Posts may embed remote media (screenshots, GIFs, YouTube previews) which the viewer's browser loads directly from the hosting site; video players only load after an explicit click. Set to `off` to disable entirely; an offline host degrades to no news automatically. | > **Deploying behind a reverse proxy?** The container listens on plain HTTP > on port 80 and accepts any `Host` header — no `server_name` whitelist. diff --git a/backend/app/api/routes/news.py b/backend/app/api/routes/news.py index 5be843f7..11c1f28c 100644 --- a/backend/app/api/routes/news.py +++ b/backend/app/api/routes/news.py @@ -4,12 +4,15 @@ The editor shows a once-per-browser "What's New" modal (see frontend/src/lib/newsSource.ts). Rather than every user's browser fetching velxio.dev directly, the backend proxies the public feed: - - users' browsers only ever talk to this server; - - one cached fetch per instance instead of one per visitor; + - the FEED fetch never leaves this server (one cached fetch per + instance instead of one per visitor, no identifiers sent) — though + media embedded in a post (images/GIFs, YouTube thumbnails) does + load in the viewer's browser from wherever it's hosted, as the + README documents; - a single, documented opt-out: VELXIO_NEWS=off (also the automatic behavior when the host has no internet — errors degrade to []). -The upstream feed is plain public JSON (no auth, no identifiers sent). +The upstream feed is plain public JSON (no auth). """ from __future__ import annotations diff --git a/frontend/src/components/layout/NewsMarkdown.tsx b/frontend/src/components/layout/NewsMarkdown.tsx new file mode 100644 index 00000000..686b3afa --- /dev/null +++ b/frontend/src/components/layout/NewsMarkdown.tsx @@ -0,0 +1,84 @@ +/** + * Markdown renderer for product-news content ("What's New" modal and the + * admin authoring preview — keep both on this one component so what the + * admin previews is exactly what users see). + * + * On top of plain GFM: + * - Any YouTube link (watch/shorts/youtu.be/embed) renders as a + * click-to-play preview: thumbnail + play badge, swapped for the + * youtube-nocookie iframe on click. Nothing loads from YouTube's + * player until the user opts in; the thumbnail is a static image. + * - Images (and GIFs) render inline. Deliberate product decision: + * 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. + * - All links open in a new tab (the modal sits on top of the editor; + * navigating away would lose workspace state). + */ + +import { useState } from 'react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; + +function youTubeId(href: string): string | null { + const m = href.match( + /^https?:\/\/(?:www\.|m\.)?(?:youtube\.com\/(?:watch\?(?:.*&)?v=|shorts\/|embed\/)|youtu\.be\/)([\w-]{11})/, + ); + return m ? m[1] : null; +} + +function YouTubePreview({ id }: { id: string }) { + const [playing, setPlaying] = useState(false); + if (playing) { + return ( + +