<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: bpmcginley</title>
    <description>The latest articles on DEV Community by bpmcginley (@bpmcginley).</description>
    <link>https://dev.to/bpmcginley</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3960074%2F79ba3b05-2471-4844-840a-e39d77704778.png</url>
      <title>DEV Community: bpmcginley</title>
      <link>https://dev.to/bpmcginley</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bpmcginley"/>
    <language>en</language>
    <item>
      <title>How to Generate Link Previews Like Slack (Without the Edge-Case Hell)</title>
      <dc:creator>bpmcginley</dc:creator>
      <pubDate>Sat, 30 May 2026 14:31:38 +0000</pubDate>
      <link>https://dev.to/bpmcginley/how-to-generate-link-previews-like-slack-without-the-edge-case-hell-b0c</link>
      <guid>https://dev.to/bpmcginley/how-to-generate-link-previews-like-slack-without-the-edge-case-hell-b0c</guid>
      <description>&lt;p&gt;You've seen it a thousand times: you paste a URL into Slack, Discord, or iMessage and it blooms into a tidy card with a title, an image, and a description. It's one of the highest-trust little UI elements you can add to a chat app, a comment box, a CMS, or a bookmarking tool. A bare URL looks like spam. An unfurled link looks legit — and gets clicked.&lt;/p&gt;

&lt;p&gt;So you decide to build it. How hard can it be? You fetch the page, grab a few meta tags, done by lunch.&lt;/p&gt;

&lt;p&gt;Then reality shows up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a link preview actually is
&lt;/h2&gt;

&lt;p&gt;The card is built from &lt;strong&gt;Open Graph&lt;/strong&gt; tags in the page's &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;​&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:title"&lt;/span&gt;       &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"The Verge"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:description"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Technology, science and culture."&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;property=&lt;/span&gt;&lt;span class="s"&gt;"og:image"&lt;/span&gt;       &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"https://.../cover.png"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
​```



Read those, render a card. Simple — in the demo. The pain is everything *between* "fetch the page" and "read the tags."

## The five things that break your weekend scraper

1. **Redirects.** `t.co`, `bit.ly`, and `http → https` mean the URL you were handed isn't the page you parse. You have to follow them.
2. **Missing tags.** Tons of sites have no `og:*` at all. You need a fallback chain: Open Graph → Twitter Card → `&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;`/`meta description` → first `&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;`/`&lt;span class="nt"&gt;&amp;lt;img&amp;gt;&lt;/span&gt;`.
3. **Relative image URLs.** `og:image` is often `/img/cover.png`, not a full URL. Resolve it against the final page URL or the image just won't load.
4. **Timeouts and giant pages.** A slow or multi-megabyte page will hang your request or eat your memory. You need a hard timeout and a byte cap.
5. **SSRF — the dangerous one.** If users submit the URLs, an attacker can point you at `http://169.254.169.254` (cloud metadata) or `http://localhost` to reach internal services. You must block private/loopback/link-local IPs — on **every** redirect hop, not just the first.

That last one is why "just scrape it yourself" quietly becomes a security review. Link-preview features are a classic SSRF vector in real apps.

## The shortcut: one request, clean JSON

If you'd rather not own all of that, hand the URL to a service that already has, and get structured data back:

​

```javascript
const res = await fetch(
  "https://link-preview14.p.rapidapi.com/preview?url=" +
    encodeURIComponent(targetUrl),
  { headers: {
      "X-RapidAPI-Key": "YOUR_KEY",
      "X-RapidAPI-Host": "link-preview14.p.rapidapi.com",
  } },
);
const preview = await res.json();
​```



You get back:

​

```json
{
  "resolvedUrl": "https://www.theverge.com/",
  "title": "The Verge",
  "description": "The Verge is about technology and how it makes us feel.",
  "image": "https://www.theverge.com/static-assets/og-image.png",
  "favicon": "https://www.theverge.com/favicon.ico",
  "siteName": "The Verge",
  "themeColor": "#5200ff"
}
​```



Redirects followed, relative URLs resolved, fallbacks applied, private IPs blocked, responses cached. Every missing field comes back as `null`, so the shape never changes and your render code stays boring (the good kind).

## When to build vs. when to buy

Building it yourself is totally reasonable if you control the input — a fixed list of trusted URLs, internal pages, that kind of thing. The moment URLs are **user-submitted**, the edge cases and the SSRF surface make a purpose-built endpoint the faster, safer call.

If you want to skip the weekend, the API above is on RapidAPI with a free tier (no card): **[Link Preview API](https://rapidapi.com/bpmcginley/api/link-preview14)**. Full write-up with the field reference is [here](https://linkpreviewapi.onrender.com/blog/how-to-generate-link-previews.html).

What edge case bit you hardest building link previews? I'll add the bad ones to the article.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
