Skip to content

fix(web): render project images in markdown previews - #7857

Draft
flamboh wants to merge 3 commits into
pingdotgg:mainfrom
flamboh:t3code/fix-readme-image-path
Draft

fix(web): render project images in markdown previews#7857
flamboh wants to merge 3 commits into
pingdotgg:mainfrom
flamboh:t3code/fix-readme-image-path

Conversation

@flamboh

@flamboh flamboh commented Aug 22, 2026

Copy link
Copy Markdown

The problem

  • Images embedded in HTML or markdown syntax within markdown previews that are sourced from the project workspace do not render
  • This is most commonly an issue with README.md that contain the project's favicon

The solution

  • Add a new code path to resolve workspace-local images

UI Changes

Below is the relevant HTML snippet for these screenshots, with the favicon at public/icon-512.png:

<div>
  <p align="center">
    <img src="public/icon-512.png" alt="" width="128" height="128" />
  </p>

  <h1 align="center">Tagium</h1>

  <p align="center">
    Local, lossless metadata editing for MP3, FLAC, M4A, and Opus audio files. <br>
    Bring your favorite tracks anywhere you listen.
    <br />
    <a href="https://tagium.app">tagium.app</a>
  </p>
</div>

Before

SCR-20260821-psun

After

SCR-20260821-pndv

Built with Fable 5 and GPT 5.6 Sol in T3 Code

Fix markdown previews to render workspace-relative images

  • Adds resolveMarkdownImagePath in markdown-links.ts to normalize image src values and resolve them against a base directory, returning an absolute workspace path or null
  • Adds rehypePreserveLocalImageSrc plugin that copies local image srcs (Windows drive paths, file: URLs) into data-local-src before sanitization so they survive the sanitizer
  • Adds MarkdownWorkspaceImage component in ChatMarkdown.tsx that requests a signed asset URL for the resolved path and falls back to the original src on failure
  • ChatMarkdown accepts an optional imageBaseDir prop; RenderedMarkdownSurface in FilePreviewPanel.tsx passes the document's own directory so relative image paths resolve correctly
  • Behavioral Change: the CHAT_MARKDOWN_SANITIZE_SCHEMA now permits data-local-src on img elements; external URLs and non-workspace paths render unchanged
📊 Macroscope summarized 9112ccf. 10 files reviewed, 4 issues evaluated, 3 issues filtered, 1 comment posted

🗂️ Filtered Issues

apps/mobile/src/features/files/FileMarkdownPreview.tsx — 0 comments posted, 1 evaluated, 1 filtered
  • line 215: renderImage is passed only to SelectableMarkdownText, but hasNativeSelectableMarkdownText() is false on the default/Android implementation, so Android renders the fallback Markdown component without any image override. Consequently workspace-relative images remain broken on Android, including the README/favicon case this code path is intended to support. [ Cross-file consolidated ]
apps/mobile/src/features/threads/ThreadFeed.tsx — 0 comments posted, 2 evaluated, 2 filtered
  • line 708: renderImage is stored only on the MarkdownStyleSet for SelectableMarkdownText, but it is not added to the Nitro Markdown renderers. On Android, hasNativeSelectableMarkdownText() is always false, so messages use <Markdown renderers={styles.renderers}> and never invoke this workspace image renderer; workspace-relative images therefore remain broken on Android. Add an image renderer to createMarkdownRenderers (or otherwise pass the override into the fallback renderer). [ Cross-file consolidated ]
  • line 1436: renderWorkspaceImage is passed only into useMarkdownStyles, where it is stored as styles.renderImage for SelectableMarkdownText; the active fallback at lines 969–977 renders Markdown without an image renderer. Since hasNativeSelectableMarkdownText() currently always returns false, workspace images in thread messages still use the default renderer and the newly added signed-asset flow is never reached. [ Cross-file consolidated ]

@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bcbe207d-bf01-4723-8406-c7c1c04aad72

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 22, 2026
@flamboh flamboh changed the title fix(web): render workspace-relative images in markdown previews fix(web): render project images in markdown previews Aug 22, 2026
<FileMarkdownPreview
markdown={props.fileContents}
onRefresh={props.onRefresh}
renderImage={props.renderMarkdownImage}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High files/ThreadFilesRouteScreen.tsx:154

On Android, workspace-relative images remain broken because FileMarkdownPreview passes renderImage only to SelectableMarkdownText, while hasNativeSelectableMarkdownText() selects the fallback Markdown renderer that ignores this prop. Pass the image renderer into the fallback Markdown renderers as well (for example via createMarkdownRenderers).

Also found in 3 other location(s)

apps/mobile/src/features/files/FileMarkdownPreview.tsx:215

renderImage is passed only to SelectableMarkdownText, but hasNativeSelectableMarkdownText() is false on the default/Android implementation, so Android renders the fallback Markdown component without any image override. Consequently workspace-relative images remain broken on Android, including the README/favicon case this code path is intended to support.

apps/mobile/src/features/threads/ThreadFeed.tsx:1436

renderWorkspaceImage is passed only into useMarkdownStyles, where it is stored as styles.renderImage for SelectableMarkdownText; the active fallback at lines 969–977 renders Markdown without an image renderer. Since hasNativeSelectableMarkdownText() currently always returns false, workspace images in thread messages still use the default renderer and the newly added signed-asset flow is never reached.

apps/mobile/src/features/threads/ThreadFeed.tsx:708

renderImage is stored only on the MarkdownStyleSet for SelectableMarkdownText, but it is not added to the Nitro Markdown renderers. On Android, hasNativeSelectableMarkdownText() is always false, so messages use &lt;Markdown renderers={styles.renderers}&gt; and never invoke this workspace image renderer; workspace-relative images therefore remain broken on Android. Add an image renderer to createMarkdownRenderers (or otherwise pass the override into the fallback renderer).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx around line 154:

On Android, workspace-relative images remain broken because `FileMarkdownPreview` passes `renderImage` only to `SelectableMarkdownText`, while `hasNativeSelectableMarkdownText()` selects the fallback `Markdown` renderer that ignores this prop. Pass the image renderer into the fallback `Markdown` renderers as well (for example via `createMarkdownRenderers`).

Also found in 3 other location(s):
- apps/mobile/src/features/files/FileMarkdownPreview.tsx:215 -- `renderImage` is passed only to `SelectableMarkdownText`, but `hasNativeSelectableMarkdownText()` is false on the default/Android implementation, so Android renders the fallback `Markdown` component without any image override. Consequently workspace-relative images remain broken on Android, including the README/favicon case this code path is intended to support.
- apps/mobile/src/features/threads/ThreadFeed.tsx:1436 -- `renderWorkspaceImage` is passed only into `useMarkdownStyles`, where it is stored as `styles.renderImage` for `SelectableMarkdownText`; the active fallback at lines 969–977 renders `Markdown` without an image renderer. Since `hasNativeSelectableMarkdownText()` currently always returns `false`, workspace images in thread messages still use the default renderer and the newly added signed-asset flow is never reached.
- apps/mobile/src/features/threads/ThreadFeed.tsx:708 -- `renderImage` is stored only on the `MarkdownStyleSet` for `SelectableMarkdownText`, but it is not added to the Nitro Markdown `renderers`. On Android, `hasNativeSelectableMarkdownText()` is always false, so messages use `<Markdown renderers={styles.renderers}>` and never invoke this workspace image renderer; workspace-relative images therefore remain broken on Android. Add an image renderer to `createMarkdownRenderers` (or otherwise pass the override into the fallback renderer).


/** `C:/...` and `C:\...` hrefs read as a "c:" scheme, so URL filters drop them. */
export function isWindowsDrivePathHref(href: string): boolean {
return WINDOWS_DRIVE_PATH_PATTERN.test(href);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/markdown-links.ts:239

isWindowsDrivePathHref returns false for backslash paths encoded as C:%5Crepo%5Cfile.ts, so markdownUrlTransform passes them to defaultUrlTransform, which treats c: as a disallowed scheme and clears the href. Decode the href before applying WINDOWS_DRIVE_PATH_PATTERN so Windows file links are preserved.

Suggested change
return WINDOWS_DRIVE_PATH_PATTERN.test(href);
return WINDOWS_DRIVE_PATH_PATTERN.test(safeDecode(href));
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/markdown-links.ts around line 239:

`isWindowsDrivePathHref` returns `false` for backslash paths encoded as `C:%5Crepo%5Cfile.ts`, so `markdownUrlTransform` passes them to `defaultUrlTransform`, which treats `c:` as a disallowed scheme and clears the href. Decode the href before applying `WINDOWS_DRIVE_PATH_PATTERN` so Windows file links are preserved.

Comment on lines +248 to +251
function isSanitizerStrippedImageSrc(src: string): boolean {
const decoded = safeDecode(src);
return WINDOWS_DRIVE_PATH_PATTERN.test(decoded) || decoded.toLowerCase().startsWith("file:");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/markdown-links.ts:248

isSanitizerStrippedImageSrc preserves file://server/share/logo.png, so raw HTML sends it through parseFileUrlHref, which drops the hostname and resolves it as the local path /share/logo.png; the signed workspace flow can therefore display an unrelated local file instead of the referenced network file. Restrict preserved file: URLs to empty or localhost hosts, or retain UNC-host semantics before resolving them.

-function isSanitizerStrippedImageSrc(src: string): boolean {
-  const decoded = safeDecode(src);
-  return WINDOWS_DRIVE_PATH_PATTERN.test(decoded) || decoded.toLowerCase().startsWith("file:");
-}
+function isSanitizerStrippedImageSrc(src: string): boolean {
+  const decoded = safeDecode(src);
+  if (WINDOWS_DRIVE_PATH_PATTERN.test(decoded)) return true;
+  if (!decoded.toLowerCase().startsWith("file:")) return false;
+  try {
+    const parsed = new URL(decoded);
+    return parsed.hostname === "" || parsed.hostname.toLowerCase() === "localhost";
+  } catch {
+    return false;
+  }
+}
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/markdown-links.ts around lines 248-251:

`isSanitizerStrippedImageSrc` preserves `file://server/share/logo.png`, so raw HTML sends it through `parseFileUrlHref`, which drops the hostname and resolves it as the local path `/share/logo.png`; the signed workspace flow can therefore display an unrelated local file instead of the referenced network file. Restrict preserved `file:` URLs to empty or `localhost` hosts, or retain UNC-host semantics before resolving them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant