9 min left
0% complete
How to Build Rich Link Previews with Open Graph Metadata
Imagine pasting a URL into a chat and instantly seeing a beautifully formatted card with a title, description, and image — not just a plain text link. This seamless experience is powered by Open Graph
Imagine pasting a URL into a chat and instantly seeing a beautifully formatted card with a title, description, and image — not just a plain text link. This seamless experience is powered by Open Graph (OG) metadata, a protocol that turns web pages into rich, shareable objects across platforms like Facebook, Twitter, Slack, and more.
In this post, we’ll walk through how to build a link preview service that fetches and parses OG metadata, handles edge cases, and delivers fast, reliable previews — all while keeping user experience and performance in mind.
What Is Open Graph Metadata?
Open Graph is a standard introduced by Facebook in 2010 to enable any web page to become a rich object when shared on social platforms. When you share a link, apps use OG metadata to generate previews like this:
🔍 Title
📌 Description
🖼️ Image
🌐 Website name
This metadata lives in the <head> of an HTML document using <meta> tags with the property attribute:
<meta property="og:title" content="GitHub - dotnet/aspnetcore" />
<meta property="og:description" content="ASP.NET Core is a cross-platform .NET framework..." />
<meta property="og:image" content="https://opengraph.githubassets.com/abc123/dotnet/aspnetcore" />
<meta property="og:url" content="https://github.com/dotnet/aspnetcore" />
<meta property="og:site_name" content="GitHub" />These tags tell clients what to display — no guesswork required.
Key OG Tags We Extract
| OG Property | Description | Fallback |
|---|---|---|
og:title | Preview title | <title> tag |
og:description | Short summary | <meta name="description"> |
og:image | Thumbnail image | None |
og:url | Canonical URL | <link rel="canonical"> |
og:site_name | Source site name | None |
If og:title is missing, for example, we fall back to the regular <title> tag. This layered approach ensures previews still work even when OG tags aren't perfectly implemented.
Extracting Data From HTML: The Technical Details
To extract this metadata, we need to fetch a URL, parse its HTML, and pull out relevant tags. Here's how we do it using C# and the popular HtmlAgilityPack library.
Parsing Open Graph Tags
We use XPath queries to locate meta tags by their property or name attributes:
private static string GetMetaContent(HtmlDocument doc, string property)
{
// First, try Open Graph style: <meta property="og:title" content="...">
var node = doc.DocumentNode.SelectSingleNode($"//meta[@property='{property}']");
if (node != null)
return node.GetAttributeValue("content", null);
// Fall back to standard meta tags: <meta name="description" content="...">
node = doc.DocumentNode.SelectSingleNode($"//meta[@name='{property}']");
return node?.GetAttributeValue("content", null);
}This allows us to prioritize OG tags but fall back gracefully to standard SEO metadata.
Grabbing the Page Title
The <title> tag is straightforward but essential:
private static string GetTitleTag(HtmlDocument doc)
{
var titleNode = doc.DocumentNode.SelectSingleNode("//head/title");
return titleNode?.InnerText?.Trim();
}Finding the Favicon
Favicons come in many forms. We check them in order of preference:
<link rel="icon" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />Our parser tries each rel value in sequence:
private static string GetFaviconUrl(HtmlDocument doc, Uri baseUri)
{
var selectors = new[]
{
"//link[@rel='icon']",
"//link[@rel='shortcut icon']",
"//link[@rel='apple-touch-icon']",
"//link[@rel='apple-touch-icon-precomposed']"
};
foreach (var selector in selectors)
{
var node = doc.DocumentNode.SelectSingleNode(selector);
var href = node?.GetAttributeValue("href", null);
if (!string.IsNullOrWhiteSpace(href))
{
return ResolveUrl(href, baseUri); // Handle relative URLs
}
}
// Default fallback
return new Uri(baseUri, "/favicon.ico").ToString();
}This ensures we get the best available icon — even if it's hosted externally or uses a relative path.
Real-World Example: YouTube
Let’s say we’re fetching a YouTube video at https://www.youtube.com/watch?v=dQw4w9WgXcQ. The page includes:
<meta property="og:title" content="Rick Astley - Never Gonna Give You Up (Official Music Video)">
<meta property="og:image" content="https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg">
<meta property="og:site_name" content="YouTube">
<link rel="icon" href="https://www.youtube.com/s/desktop/favicon.ico">Our service parses this and returns structured JSON:
{
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"title": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
"description": "The official video for \"Never Gonna Give You Up\" by Rick Astley...",
"image": "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
"siteName": "YouTube",
"favicon": "https://www.youtube.com/s/desktop/favicon.ico"
}Clean, consistent, and ready for rendering.
How the Link Preview Service Works
The full flow from user action to preview rendering:
- ✅ User pastes a URL
- 🚀 Client sends URL to a GraphQL endpoint
- 🔽 Backend fetches the target page's HTML
- 🧠 Parses and extracts OG tags and favicon
- 💾 Caches the result for future use
- 📤 Returns structured preview data
- 🖼️ Client renders a rich card
graph LR
Client --> GraphQL --> LinkPreviewService --> HTTP[Fetch HTML] --> Parser --> Cache --> Response --> ClientThis pipeline ensures we only do expensive work once — and deliver quickly thereafter.
GraphQL Integration
We expose the preview functionality via a simple GraphQL query:
query LinkPreview($url: String!) {
linkPreview(url: $url) {
url
title
description
image
siteName
favicon
}
}This returns structured data that any frontend can use to build a consistent preview component.
Example Response
{
"data": {
"linkPreview": {
"url": "https://github.com/dotnet/aspnetcore",
"title": "GitHub - dotnet/aspnetcore",
"description": "ASP.NET Core is a cross-platform .NET framework...",
"image": "https://opengraph.githubassets.com/...",
"siteName": "GitHub",
"favicon": "https://github.githubassets.com/favicons/favicon.svg"
}
}
}Performance & Trade-offs
Fetching external URLs introduces several challenges:
| Challenge | Why It Matters |
|---|---|
| Network latency | Slows down preview generation |
| Repeated requests | Wastes bandwidth and time |
| External failures | Sites may be down or block requests |
| Stale data | Content changes but preview doesn’t update |
We address these with smart design decisions.
Caching Strategy: Speed Meets Efficiency
To minimize redundant network calls, we cache results for 24 hours using a normalized URL hash.
Cache Key Generation
We use SHA256 to create consistent, unique keys:
private static string GetCacheKey(string url)
{
using var sha256 = SHA256.Create();
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(url.ToLowerInvariant()));
return $"LinkPreview:{Convert.ToBase64String(hashBytes)}";
}Normalizing the URL (lowercase, trimmed) ensures HTTP://EXAMPLE.COM and http://example.com are treated the same.
Caching Best Practices
- ✅ Cache successful results for 24 hours
- ✅ Cache failed attempts too — prevents hammering broken links
- ✅ Use
IMemoryCachefor fast, in-process access - ✅ Log cache hits/misses for observability
Example logs:
[LinkPreview] CACHE MISS - Fetching URL: https://github.com/dotnet/aspnetcore
[LinkPreview] CACHE HIT for URL: https://github.com/dotnet/aspnetcoreThis keeps your system fast and resilient.
Optimize Further: Embed Previews in Item Data
To reduce round trips, we embed the link preview directly in item responses — if it’s already cached.
How It Works
- When a client requests an item with a URL…
- The server checks if a preview is cached
- If yes → include it in the response
- If no → return
linkPreview: null, and the client can fetch it separately
query Item($id: String!) {
item(id: $id) {
id
name
link
linkPreview {
url
title
description
image
siteName
favicon
}
}
}Case 1: Preview Cached ✅
{
"item": {
"link": "https://github.com/dotnet/aspnetcore",
"linkPreview": { "title": "GitHub - dotnet/aspnetcore", ... }
}
}Case 2: Not Cached ⏳
{
"item": {
"link": "https://example.com/new-link",
"linkPreview": null
}
}This "opportunistic embedding" means users get instant previews when possible — without forcing expensive fetches every time.
Client-Side Implementation Guide
On the frontend, handling previews smoothly requires a two-step strategy.
Pseudocode: Fetch with Fallback
async function fetchItemDetails(itemId: string) {
const itemResponse = await graphqlClient.query({
query: ITEM_QUERY,
variables: { id: itemId }
});
const item = itemResponse.data.item;
if (item.linkPreview) {
// ✅ Already available — render immediately
renderLinkPreview(item.linkPreview);
} else if (item.link) {
// 🔄 Fetch preview separately
const previewResponse = await graphqlClient.query({
query: LINK_PREVIEW_QUERY,
variables: { url: item.link }
});
renderLinkPreview(previewResponse.data.linkPreview);
}
}Client Checklist
| Task | Recommendation |
|---|---|
| ✅ Update queries | Add linkPreview { ... } to item queries |
✅ Handle null | Always check if preview exists before rendering |
| ✅ Fallback fetch | Call linkPreview(url) if not embedded |
| ✅ Show loading | Use skeleton loaders during async fetch |
| ✅ Optional caching | Store previews in local state or localStorage |
This approach balances performance and UX — fast when possible, graceful when not.
Why This Architecture Works
We made several key design decisions to ensure reliability and efficiency:
- SHA256 cache keys → Consistent, secure, and URL-length agnostic
- Fail-fast caching → Even errors are cached to avoid repeated failures
- Fallback extraction → Ensures previews work even with incomplete OG tags
- Relative URL resolution → Correctly resolves
/images/preview.jpgto full URLs - User-Agent header → Reduces chance of being blocked as a bot
- Opportunistic embedding → Minimizes client round trips
- Cache peek →
TryGetCachedPreviewnever triggers a network call — safe for frequent checks
Dependencies You’ll Need
This implementation relies on a few battle-tested tools:
- HtmlAgilityPack – Robust HTML parsing (even for malformed markup)
- IMemoryCache – Fast in-memory caching (part of .NET)
- ILogger – For monitoring cache behavior and errors
- HttpClient – To fetch external URLs with proper timeouts and headers
All are lightweight and integrate easily into modern backends.
Key Takeaways
Building rich link previews isn’t just about pulling metadata — it’s about doing it smartly and efficiently.
✅ Use Open Graph tags to generate accurate, attractive previews
🔁 Cache aggressively to reduce latency and external requests
🛡️ Handle failures gracefully — both in parsing and network calls
⚡ Embed when possible to reduce client-side round trips
🎯 Prioritize UX with loading states and fallback behaviors
With this approach, your application delivers a polished, social-media-like sharing experience — turning plain links into engaging visual content.
Ready to make your links shine? Start with OG tags, add a parser, and layer in caching. The result? A seamless preview system that feels fast, reliable, and modern.