Skip to content

fix(link-preview): use static api import so previews load under BASE_URL#3541

Merged
Yeraze merged 1 commit into
mainfrom
fix/link-preview-dynamic-import-404
Jun 18, 2026
Merged

fix(link-preview): use static api import so previews load under BASE_URL#3541
Yeraze merged 1 commit into
mainfrom
fix/link-preview-dynamic-import-404

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Summary

Link previews silently failed to render on the MeshCore channel, room server, and DM displays (and on the first messages page visited in a session). Root cause: fetchLinkPreview() used a dynamic import('../services/api'), and Vite computed that lazy chunk's preload URL without the runtime BASE_URL prefix — requesting /assets/… instead of /meshmonitor/assets/…. That 404'd, Vite's preload helper threw Unable to preload CSS, the catch returned null, and no preview card rendered.

Diagnosed from the live instance's browser console:

[LinkPreview] Fetching preview for: https://www.youtube.com/live/...
GET http://…:8081/assets/GeoJsonOverlay-CH8CPbwf.js  404 (Not Found)
Error fetching link preview: Unable to preload CSS for /assets/GeoJsonOverlay-vh-t_kPv.css
[LinkPreview] Received metadata: null

The component, setting (linkPreviewsEnabled), URL extraction, CSS, and the /api/link-preview endpoint all worked — the endpoint returns full metadata for the exact message URLs. It looked MeshCore-specific only because the Meshtastic/dashboard pages had already loaded that chunk's CSS (the map uses GeoJsonOverlay), masking the broken preload there.

Changes

  • src/utils/linkRenderer.ts(x): import the api service statically instead of via await import('../services/api'). api is already in the main bundle and statically imported across the app, and it does not import linkRenderer, so the original "avoid circular dependencies" justification doesn't hold. Removing the dynamic import eliminates the fragile, base-path-sensitive preload entirely.
  • Added src/utils/linkRenderer.test.ts (the file had no tests): fetchLinkPreview delegates to the api service and resolves null (no throw) on failure; extractUrls covers query strings, bare www., and the no-URL case.

Issues Resolved

None filed — reported directly ("link previews not showing on MeshCore").

Note for maintainers

The underlying cause is that dynamic-import (lazy chunk) preload URLs don't pick up the runtime BASE_URL, while the entry bundle and API calls do. This fix removes the one dynamic import in the link-preview path, but other lazy chunks (e.g. emoji picker, GeoJsonOverlay, embed) load via the same mechanism and could 404 on first use under a non-root BASE_URL. Worth a separate look at the Vite base / runtime-base handling for code-split chunks.

Documentation Updates

  • CHANGELOG.md — Unreleased → Bug Fixes entry.

Testing

  • Full unit suite passes (7433 tests, 0 failures)
  • TypeScript compiles cleanly
  • New linkRenderer.test.ts (5 tests) + existing LinkPreview.test.tsx pass
  • Reviewer: under a non-root BASE_URL (e.g. /meshmonitor), open a MeshCore channel/room/DM containing a URL and confirm the preview card renders (no /assets/… 404 in the console)

🤖 Generated with Claude Code

fetchLinkPreview() did `await import('../services/api')`. Vite computed that
lazy chunk's preload URL without the runtime BASE_URL prefix (/assets/… instead
of /meshmonitor/assets/…), so it 404'd and threw "Unable to preload CSS" on any
page where the chunk wasn't already loaded. The error was caught and returned
null, so no preview card rendered — most visibly on the MeshCore channel, room
server, and DM views (the Meshtastic/dashboard pages had already loaded the
chunk's CSS, masking it there).

The api service is already in the main bundle and statically imported across the
app; it does not import this module, so the "avoid circular dependencies"
justification doesn't hold. Import it statically to drop the fragile dynamic
import entirely.

Adds linkRenderer tests (fetchLinkPreview delegate + null-on-error, extractUrls).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVGxkuD4Fwa2JGVim8ZeVj
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: fix(link-preview): use static api import

Tasks:

  • Gather context and read changed files
  • Review code quality and best practices
  • Check test coverage
  • Assess performance and security
  • Post final review

Summary

This is a clean, well-targeted fix. The root cause diagnosis is accurate and the solution is correct. Here's my detailed review:

Code Quality ✅

src/utils/linkRenderer.tsx

The change is minimal and correct — replacing await import('../services/api') with a static top-level import eliminates the fragile Vite preload step that was broken under a non-root BASE_URL. The comment in fetchLinkPreview (lines 106–112) clearly explains the historical context and why there's no circular dependency concern, which is valuable for future maintainers.

One minor point: the fetchLinkPreview function has a redundant assignment:

const metadata = await api.fetchLinkPreview(url);
return metadata;

This could be simplified to return await api.fetchLinkPreview(url) (or even return api.fetchLinkPreview(url) since the try/catch would still work), but this is purely cosmetic and doesn't affect correctness.

Test Coverage ✅

src/utils/linkRenderer.test.ts provides good coverage for the two public functions under test:

  • fetchLinkPreview: success path and error/null fallback path — exactly what's needed to lock in the "delegate-and-null-on-failure" contract
  • extractUrls: https URL with query string, bare www. normalization, and empty case

The mock structure correctly uses vi.mock with a hoisted factory before the import of the module under test, which is the right Vitest pattern for static imports.

One gap: there's no test for renderMessageWithLinks — but that function isn't touched by this PR, so it's acceptable to leave for another time.

Performance ✅

The fix is a strict improvement: eliminating the dynamic import means one fewer lazy chunk, one fewer potential preload race, and slightly faster first-render of any component that calls fetchLinkPreview. The api module is already in the main bundle (statically imported across the app), so this adds no extra weight to the initial bundle.

Security ✅

No security concerns. The URL passed to api.fetchLinkPreview is encoded server-side (visible in api.ts: encodeURIComponent(url)). The rel="noopener noreferrer" attributes on rendered preview links are already present in LinkPreview.tsx. No new attack surface is introduced.

One observation (pre-existing, not introduced by this PR): LinkPreview.tsx:149 renders preview.image as an <img src> from third-party OG metadata without sanitization. This is a common pattern and the risk is bounded by the backend's fetch, but worth noting as a long-term consideration.

Maintainer Note Endorsed

The PR description correctly flags that other lazy chunks (emoji picker, GeoJsonOverlay, embed) loaded via the same dynamic-import mechanism could similarly 404 under a non-root BASE_URL. This fix is surgical and correct for the immediate problem; the broader Vite base/runtime-base handling deserves a follow-up issue.

Verdict

Approve — the fix is correct, minimal, and well-tested. No blocking issues.


@Yeraze Yeraze merged commit c2e786d into main Jun 18, 2026
25 of 28 checks passed
@Yeraze Yeraze deleted the fix/link-preview-dynamic-import-404 branch June 18, 2026 19:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant