Skip to main content

Link preview pipeline β€” 6 small fixes to make external URL previews work reliably

link previews on posts are inconsistent, especially for social-media URLs and for posts where users paste raw URLs (the most common case). I traced the root causes through the source (v2.5.0) and validated fixes via a userland snippet. Sharing the findings below so they can flow into a future release.

Every item is small + surgical. None require new features β€” they're mostly one-line config changes or filter additions.


1. Bare URLs don't become previewable (high impact, one-line fix)

File:Β app/Services/FeedsHelper.php, line 71
Current:

->setUrlsLinked(false)

Issue:Β Parsedown's auto-linker is explicitly off, so any user who pastesΒ https://example.comΒ as plain text gets a non-clickable URL AND no preview card, because the downstreamΒ findFirstUrl()Β only matchesΒ <a href>Β tags. This is the most common user flow on every other community platform.

Proposed fix:Β Flip toΒ ->setUrlsLinked(true). If keeping it off is intentional for a specific reason, expose it as a filter:

->setUrlsLinked(apply_filters('fluent_community/markdown_auto_link_urls', true))

2.Β findFirstUrl()Β regex has a greedy lookahead bug

File:Β app/Services/FeedsHelper.php, line 222
Current:

$pattern = '/<a\s+(?:[^>]*?\s+)?href=([\'"])(?!.*\/u\/)(.*?)\1/';

Issue:Β TheΒ (?!.*\/u\/)Β lookahead uses greedyΒ .*Β against theΒ whole remaining string, so if any mention or profile link appearsΒ anywhere laterΒ in the message, the regex discardsΒ everyΒ preceding URL. Posts that mix a URL with anΒ @mentionΒ get no preview.

Proposed fix:Β UseΒ preg_match_all, then filter results per-match:

preg_match_all('/<a\s+[^>]*href=([\'"])(.*?)\1/i', $html, $matches);

foreach ($matches[2] as $href) {

if (strpos($href, '/u/') === false) {

return $href;

}

}

return '';


3. Outbound User-Agent gets rejected by half the modern web

File:Β app/Services/RemoteUrlParser.php, line 124
Current:

$modified_user_agent = 'WP-URLDetails/' . get_bloginfo('version') . ' (+' . get_bloginfo('url') . ')';

Issue:Β Twitter/X, Meta properties, LinkedIn, and most Cloudflare-fronted sites return 401/403 to this UA. So fetches for the most-shared URL types silently fail and no preview ever gets generated.

Proposed fix:Β Default to a real browser UA, expose a filter for overrides:

$modified_user_agent = apply_filters(

'fluent_community/url_fetch_user_agent',

'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'

);


4. Response size cap is too tight for modern sites

File:Β app/Services/RemoteUrlParser.php, line 127
Current:Β 'limit_response_size' => 300 * KB_IN_BYTES
Issue:Β Many modern sites push OG tags below 300KB of inline JSON-LD / preload tags, so theΒ <head>Β section is never reached and metadata extraction fails.

Proposed fix:Β Bump to 600KB and add an explicit timeout so slow news sites don't fail:

$args = [

'limit_response_size' => 600 * KB_IN_BYTES,

'timeout' => 15,

'redirection' => 5,

'user-agent' => $modified_user_agent,

];


5. Eye-icon preview button silently fails for OG cards

File:Β assets/app.jsΒ (composer preview component)
Current behavior:

this.$get("feeds/oembed", {url}).then(e => {

this.media.html = e.oembed.html // only renders if HTML exists

})

Issue:Β The composer's preview button only renders embeddable iframe HTML (YouTube, Vimeo, etc.). Generic OG cards (image + title + description, the format most websites return) come back without anΒ htmlΒ field, so the preview button silently shows nothing. Users perceive the button as broken.

Proposed fix:Β When the response isΒ type: 'meta_data', render a fallback OG card client-side usingΒ oembed.image,Β oembed.title,Β oembed.description. Same component you already render under a posted feed β€” just reuse it in the composer.


6. No proxy hook for blocked hosts

Issue:Β Twitter/X, Instagram, TikTok, Facebook actively block all unauthenticated server-side fetches regardless of UA. No CSS / config fix bypasses this β€” the only solution is routing through a third-party metadata service (Iframely, Microlink, LinkPreview). The plugin has no documented extension point for this.

Proposed fix:Β Add an early-exit filter insideΒ RemoteUrlParser::parse():

public static function parse($url)

{

// Allow third-party services to handle blocked hosts.

$preempted = apply_filters('fluent_community/preview_metadata_pre_fetch', null, $url);

if ($preempted) {

return $preempted;

}

// ... existing logic

}

That single filter lets agencies and community admins plug in Iframely (paid, rich) or Microlink (free tier) for hosts that block direct fetches, without forking the plugin.


Cache backend (nice-to-have, item 7)

File:Β app/Services/RemoteUrlParser.php, lines 88, 117
UsesΒ wp_cache_get/wp_cache_set, which is non-persistent unless an object cache plugin is installed. Consider falling back toΒ get_transient/set_transientΒ so first-page-load previews aren't re-fetched on every request on stock installs.


Priority recommendation

If you can only ship one thing:Β item #1Β (setUrlsLinked(true)). That alone solves the majority of "no preview" complaints because it makes raw pasted URLs work the way users expect on every other platform.

If you can ship two: addΒ #3Β (browser UA). That unblocks LinkedIn + most CDN-protected sites.

Items #5 and #6 are bigger UX wins but require more work.

We will look into these, thank you.

Manoj Sharma

Tawsif Ahmed RiyadΒ These things are creating real impact, its hard to get a community going, and if we have these sort of roadblocks, then motivation just flys away...

Manoj Sharma, we have identified two improvements among your 6 suggestions. We are working on this. We are also investigating these areas; it will take some time. Thank you.

Manoj Sharma

Tawsif Ahmed RiyadΒ yes that will help all the community owners

Manoj Sharma, your point no. 02 and 06 has been fixed.

For 06, we have added a filter hook,
file: fluent-community/app/Services/RemoteUrlParser.php

method: getInfoFromRemoteUrl (from code line 127-131)

        $preempted = apply_filters('fluent_community/preview_metadata_pre_fetch', null, $url);
        if ($preempted && is_array($preempted)) {
            wp_cache_set($cacheKey, $preempted, 'fluent-community', apply_filters('rest_url_details_cache_expiration', HOUR_IN_SECONDS)); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
            return $preempted;
        }

For the rest of the points, some of your findings are false positives, and some still need to be checked. Thank you.