The problem with plain URLs
Before this feature, any URL dropped into a Devit post rendered as raw text. Users posting GitHub repos, blog articles, or project demos had no way to give context at a glance. The feed felt flat compared to what developers expect from modern platforms — Discord, Slack, and Twitter/X all unfurl links automatically.
The challenge with building this ourselves was twofold: scraping external URLs is slow and unreliable, and several high-traffic domains actively block bots. We needed a fetch pipeline that was fast, cacheable, and had a graceful fallback story for the domains that would never cooperate.
The fetch pipeline
Link preview generation runs in a Supabase Edge Function rather than on the client. This keeps API secrets server-side, allows caching in the database, and avoids CORS issues when fetching third-party pages.
URL detection on post save
When a post is saved, the frontend extracts the first URL from the body text using a regex and fires a request to the link-preview edge function with that URL.
Cache check
The edge function first queries the link_previews table for an existing row matching the URL. If a cached entry exists and is less than 7 days old, it's returned immediately — no fetch required.
Domain routing
Known blocked domains (YouTube, Twitter/X) skip the fetch entirely and receive a pre-built static fallback payload. All other URLs proceed to the live fetch step.
OG meta parse
The function fetches the URL with a browser-like user agent and parses og:title, og:description, og:image, and the favicon from the raw HTML. Falls back to <title> and <meta name="description"> if OG tags are missing.
Cache write & return
The parsed payload is inserted into link_previews with a fetched_at timestamp, then returned to the frontend to render the card.
// Edge function: domain routing (simplified) const STATIC_FALLBACKS = { 'youtube.com': { type: 'youtube', title: 'YouTube' }, 'youtu.be': { type: 'youtube', title: 'YouTube' }, 'twitter.com': { type: 'twitter', title: 'View on X' }, 'x.com': { type: 'twitter', title: 'View on X' }, }; const domain = new URL(url).hostname.replace('www.', ''); if (STATIC_FALLBACKS[domain]) { return buildFallback(url, STATIC_FALLBACKS[domain]); }
Rendering in the feed
On the frontend, posts with a saved preview payload render a LinkPreviewCard component below the post body. The card layout varies by preview type:
| Type | Layout | Status |
|---|---|---|
| Generic OG | Thumbnail image + title + description + domain/favicon | Live fetch |
| No OG tags | Title + description from <title> + meta fallback |
Live fetch |
| YouTube | Inline <iframe> embed using the video ID extracted from the URL |
Static fallback |
| Twitter / X | Branded card with X logo and "View on X" CTA | Static fallback |
| Fetch error | Plain URL chip — no card rendered | Silent fail |
Description text is clamped to two lines with CSS -webkit-line-clamp. OG images that fail to load are hidden rather than showing a broken image placeholder.
YouTube inline embeds
YouTube links get special treatment: instead of a static card, they render directly as an embedded player. The video ID is extracted from both youtube.com/watch?v= and youtu.be/ URL formats, then dropped into a standard iframe with the youtube-nocookie.com embed domain to keep things privacy-clean.
// Extract YouTube video ID from either URL format function getYouTubeId(url) { const patterns = [ /youtube\.com\/watch\?v=([^&]+)/, /youtu\.be\/([^?]+)/, ]; for (const re of patterns) { const m = url.match(re); if (m) return m[1]; } return null; }
Why not embed Twitter? Twitter's oEmbed endpoint requires authentication since the API pricing changes in 2023. The branded fallback card is the cleanest option without paying for API access.
Caching strategy
Fetching OG data on every page load would be slow and would hammer third-party servers. The link_previews table acts as a persistent cache keyed on the full URL. The 7-day TTL is a balance — fresh enough that OG tags that change (updated article titles, new thumbnails) eventually reflect, but old enough that the vast majority of requests are served from the database with zero latency.
RLS note: The link_previews table is public-read, authenticated-write. Any logged-in user can trigger a new cache entry, but unauthenticated visitors can still see previews on public posts.
Performance impact
Before the feature, post cards were simple text blocks — fast to render. The concern was that adding image fetches and iframes would slow the feed. In practice, the impact is minimal because:
- Cached previews are a single extra column on the posts query — no additional round trip once the cache is warm
- OG images are loaded lazily with
loading="lazy"— they don't block the initial render - YouTube iframes are only inserted when the post is scrolled into view, using an
IntersectionObserver - Failed fetches store a null payload so the same URL is never re-fetched within the TTL window
What's next
The current implementation handles one URL per post. A natural extension is multi-URL support — parsing all URLs in the body and letting the user pin the one they want to feature. GitHub repo cards are also planned: rather than a generic OG card, a GitHub URL would render a structured card showing stars, language, and last commit date pulled from the GitHub API.
Live on Devit: Paste any URL into a post at devit-six.vercel.app and the preview card will generate automatically. YouTube links will embed inline.