Skip to content

Add web link preview unfurling#367

Merged
appflowy merged 6 commits into
mainfrom
fix/link-preview-unfurl-sidebar-reveal
May 25, 2026
Merged

Add web link preview unfurling#367
appflowy merged 6 commits into
mainfrom
fix/link-preview-unfurl-sidebar-reveal

Conversation

@appflowy

@appflowy appflowy commented May 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a server-side link preview endpoint with redirect target validation for SSRF-sensitive hosts
  • register the AppFlowy unfurl provider ahead of Microlink/GitHub while preserving fallback behavior on server fetch failures
  • add hover previews for external-link mentions and reveal selected sidebar ancestors on load
  • keep slow external image loads out of the upload-pending state

Review

  • reviewed the current diff and found no blocking issues
  • verified redirect-based SSRF handling and fallback-provider behavior with focused tests

Tests

  • pnpm exec jest src/utils/tests/link-preview.test.ts src/utils/tests/link-preview-remote.test.ts api/_lib/tests/unfurl.test.ts src/components/app/hooks/tests/resolveAncestorViewIds.test.ts --no-coverage --runInBand
  • pnpm exec tsc --noEmit --project tsconfig.web.json

Summary by Sourcery

Add a server-side link unfurling endpoint and wire it into web link previews while improving sidebar reveal behavior and upload status handling.

New Features:

  • Introduce a server-side /api/link-preview endpoint that unfurls external URLs with basic SSRF-safe redirect handling.
  • Add a high-priority AppFlowy link preview provider that uses the new unfurl endpoint before third-party providers.
  • Enable rich hover cards for external-link mentions in the editor, including delayed fetching based on viewport visibility.

Enhancements:

  • Automatically expand sidebar ancestors to reveal the currently selected view when navigating into the app.
  • Prevent slow or failing external images from entering the upload-pending state, keeping them in a generic loading state instead.

Tests:

  • Add unit tests for the URL ancestor resolution helper used to reveal sidebar views.
  • Add unit tests for the server-side unfurl logic, including redirect and failure handling.
  • Add unit tests for the AppFlowy remote link preview provider integration.

@sourcery-ai

sourcery-ai Bot commented May 25, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a server-side link unfurling pipeline (with SSRF-aware URL validation) and wires it into the web app as the primary link preview provider, introduces hover preview cards for external-link mentions while deferring fetches until in-view, ensures the sidebar auto-expands to reveal the selected view on load, and refines image loading logic so slow external images do not appear as upload-pending.

Sequence diagram for server-side link preview unfurling with SSRF-aware redirects

sequenceDiagram
  participant UserBrowser
  participant MentionExternalLink
  participant link_preview as fetchLinkPreviewData
  participant provider as appflowyLinkPreviewProvider
  participant ApiRoute as api_link_preview_handler
  participant Unfurl as unfurl
  participant FetchHtml as fetchHtmlFollowingAllowedRedirects
  participant UrlSafety as isAllowedHttpUrl
  participant ExternalSite

  UserBrowser->>MentionExternalLink: hover external-link mention
  MentionExternalLink->>link_preview: fetchLinkPreviewData(url)
  link_preview->>provider: appflowyLinkPreviewProvider.fetch(context)
  provider->>ApiRoute: GET /api/link-preview?url=normalizedUrl
  ApiRoute->>UrlSafety: isAllowedHttpUrl(parsedUrl)
  UrlSafety-->>ApiRoute: allowed / blocked
  alt url blocked
    ApiRoute-->>provider: 400 Blocked host
    provider-->>link_preview: undefined (fallback to other providers)
  else url allowed
    ApiRoute->>Unfurl: unfurl(url)
    Unfurl->>FetchHtml: fetchHtmlFollowingAllowedRedirects(initialUrl, signal)
    loop redirects <= MAX_REDIRECTS
      FetchHtml->>UrlSafety: isAllowedHttpUrl(currentUrl)
      UrlSafety-->>FetchHtml: allow / block
      alt redirect response
        FetchHtml->>ExternalSite: fetch(nextUrl, redirect=manual)
      else final response
        FetchHtml-->>Unfurl: {response, url}
      end
    end
    Unfurl-->>ApiRoute: UnfurlResult (title, description, image, logo)
    ApiRoute-->>provider: 200 JSON
    provider-->>link_preview: LinkPreviewData
    link_preview-->>MentionExternalLink: LinkPreviewData
    MentionExternalLink-->>UserBrowser: Show MentionLinkPreviewCard in Popover
  end
Loading

File-Level Changes

Change Details Files
Add SSRF-aware server-side link unfurling endpoint and URL safety utilities, plus tests.
  • Implement unfurl() to fetch HTML, follow only allowed redirects with a manual redirect mode, and extract Open Graph / title / favicon metadata without external dependencies
  • Introduce isBlockedHost() and isAllowedHttpUrl() helpers to block private/loopback/link-local hosts and enforce http/https-only URLs, and apply them to redirects and endpoint input validation
  • Expose a /api/link-preview handler that validates input, calls unfurl, sets cache headers, and returns structured preview data or appropriate HTTP errors
  • Add unit tests to validate unfurl behaviour on network errors, non-OK responses, safe vs blocked redirects, and metadata extraction, and to validate link-preview remote provider behaviour and URL filtering
api/_lib/unfurl.ts
api/_lib/url-safety.ts
api/_lib/__tests__/unfurl.test.ts
api/link-preview.ts
src/utils/link-preview-remote.ts
src/utils/__tests__/link-preview-remote.test.ts
Register an AppFlowy-hosted link preview provider and integrate it ahead of Microlink/GitHub while preserving graceful fallback.
  • Define appflowyLinkPreviewProvider with higher priority, http/https URL gating, and mapping of /api/link-preview JSON responses into LinkPreviewData
  • Handle endpoint failures or invalid responses by returning undefined so lower-priority providers (Microlink/GitHub) can run
  • Provide registerAppflowyLinkPreviewProvider() to register the provider once at app startup, avoiding test interference
src/utils/link-preview-remote.ts
src/utils/__tests__/link-preview-remote.test.ts
src/main.tsx
Add hover-based rich preview cards for inline external-link mentions, with deferred fetching using IntersectionObserver and debounced popover behaviour.
  • Wrap MentionExternalLink in React.memo and refactor to use hooks from React/lodash-es instead of React namespace imports
  • Use IntersectionObserver on the mention span anchor to delay remote preview fetches until the chip is near the viewport, falling back to eager fetching when unsupported
  • Introduce hover-driven Popover around the mention chip with debounced open/close, pointer-guarded mouse enter/leave, and click handler delegating to openUrl
  • Render a new MentionLinkPreviewCard component for the popover content that mirrors desktop layout, including banner image, truncated description, and favicon+host footer, and stop event bubbling to avoid interfering with editor selection
src/components/editor/components/leaf/mention/MentionExternalLink.tsx
src/components/editor/components/leaf/mention/MentionLinkPreviewCard.tsx
Automatically reveal the selected view in the sidebar outline by expanding its ancestor chain, using a mix of local and remote resolution with tests.
  • Track the currently selected sidebar view id and a revealedViewIdRef to ensure each view is auto-revealed at most once per workspace reset
  • Maintain an outlineRef to provide stable access to the latest outline tree for async ancestor resolution without constantly restarting the walk
  • Implement resolveAncestorViewIds() to resolve ancestor ids via findAncestors/findView in the loaded outline (fast path) or via parent_view_id + fetchView (slow path), stopping at the workspace root and guarding against cycles or missing views
  • Wire resolveAncestorViewIds into Outline to expand ancestor ids via expandAncestors, routing them through expandViewIds/pendingAutoLoadIds so lazy-loading still works, and add unit tests covering local-only, remote, mixed, failure, and cycle scenarios
src/components/app/outline/Outline.tsx
src/components/app/hooks/resolveAncestorViewIds.ts
src/components/app/hooks/__tests__/resolveAncestorViewIds.test.ts
Refine image loading and validation to avoid misclassifying slow external image loads as upload-pending and to surface pending state only when the upload pipeline is not ready.
  • Track the latest CheckImageResult in useImageWithRetry so the pending-promotion timer only moves loading → pending when errorKind is 'not-ready' (server reports upload still in-flight)
  • On retries, when a 'not-ready' result arrives after the pending threshold elapsed, immediately transition loading → pending to surface the correct upload-waiting state
  • Change validateImageLoad() timeout classification from errorKind 'not-ready' to 'network' so external image timeouts do not trigger the upload-pending UI
src/components/editor/components/blocks/image/useImageWithRetry.ts
src/utils/image.ts

Possibly linked issues

  • #[Bug] link previews in table cells are too big: PR reworks external link mentions into hover popovers portaled outside table cells, fixing oversized in-cell previews.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • In appflowyLinkPreviewProvider.fetch, consider wrapping the fetch call in a try/catch and returning undefined on network errors so this provider consistently fails open and allows lower-priority providers to run, matching the behavior you already implemented for non-OK responses and invalid JSON.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `appflowyLinkPreviewProvider.fetch`, consider wrapping the `fetch` call in a try/catch and returning `undefined` on network errors so this provider consistently fails open and allows lower-priority providers to run, matching the behavior you already implemented for non-OK responses and invalid JSON.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@appflowy appflowy merged commit 0dbeca7 into main May 25, 2026
12 of 13 checks passed
@appflowy appflowy deleted the fix/link-preview-unfurl-sidebar-reveal branch May 25, 2026 07:05
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