DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

Building an Open Graph / meta-tag previewer: one set of tags, three social cards, and the fallback chain behind them

When someone shares your link, the platform's crawler fetches the page and reads a handful of <meta> tags out of the <head> to build a preview card. There are two overlapping vocabularies — Open Graph (og:*) for Facebook, LinkedIn, Slack and most others, and Twitter Cards (twitter:*) for X — and each platform reads what it wants, falls back when a tag is missing, and truncates differently. I built a previewer that resolves that whole mess live, renders the three real-world cards, and writes the exact tag block to paste. There's no library; it's all string work.

The whole state is six base fields

You don't store a tag per platform — you store the source of truth once and derive every tag from it: title, description, URL, image, site name, and the Twitter card type. That flat object is your document; the previews are just views of it.

let state = {
  title:       "How to Build a Meta-Tag Previewer",
  description: "A hands-on guide to Open Graph & Twitter Cards…",
  url:         "https://solvefromzero.dev/blog/og-previewer",
  image:       "https://solvefromzero.dev/img/og-cover.png",
  siteName:    "SolveFromZero",
  card:        "summary_large_image"   // or "summary"
};
Enter fullscreen mode Exit fullscreen mode

The fallback rule is "first non-empty wins"

This one helper is the heart of the tool. Give it candidates in priority order and it returns the first with a real value. Every fallback chain in Open Graph and Twitter Cards is a call to this.

function pick(...vals){
  for (const v of vals)
    if (v != null && String(v).trim() !== "") return String(v);
  return "";
}
Enter fullscreen mode Exit fullscreen mode

Now the chains: Open Graph reads the base fields; Twitter reads its own tag, then Open Graph, then the base — so a page that only sets og:* still gets a full X card. That is why writing the Open Graph set well covers most of the internet at once.

tw: {
  card:  pick(s.card, "summary_large_image"),
  title: pick(s.twTitle, s.ogTitle, title),          // twitter:title ← og:title ← <title>
  image: pick(s.twImage, s.ogImage, image)           // twitter:image ← og:image
}
Enter fullscreen mode Exit fullscreen mode

Truncation is one function and a table

Each surface clips text at a different length, so keep the limits as data and share one truncate. There is no single "correct" length — write for the shortest cutoff you care about.

const LIMITS = {
  google:  { title: 60, desc: 160 },
  twitter: { title: 70, desc: 200 },
  facebook:{ title: 88, desc: 200 }
};
function truncate(str, n){
  str = str || "";
  return str.length <= n ? str : str.slice(0, n - 1).trimEnd() + "";
}
Enter fullscreen mode Exit fullscreen mode

Parse a pasted head with no DOM

To read an existing page, sweep every <meta> tag and key it by its property or name. A small attribute-reader plus a regex loop is enough, and it runs anywhere — even under Node — because it never touches the DOM.

for (const m of html.matchAll(/<meta\b[^>]*>/gi)){
  const key = (attr(m[0],"property") || attr(m[0],"name") || "").toLowerCase();
  const content = attr(m[0], "content");
  if (key === "og:image") out.image = content;   // …and the rest
}
Enter fullscreen mode Exit fullscreen mode

Let the browser verify the image ratio

For the 1200×630 (1.91:1) image check, don't compute anything — let the browser load it and read naturalWidth/naturalHeight. A token guards against a stale image finishing after the field changed.

let imgToken = 0;
function checkImage(src){
  const token = ++imgToken;
  const im = new Image();
  im.onload = () => {
    if (token !== imgToken) return;            // a newer check started — ignore
    const ok = Math.abs(im.naturalWidth / im.naturalHeight - 1.905) < 0.06;
    warnImage(ok, `${im.naturalWidth}×${im.naturalHeight}`);
  };
  im.src = src;
}
Enter fullscreen mode Exit fullscreen mode

Five things that surprise people

Building this surfaced the gotchas that cause most "why is my card blank?" bugs. One: og: uses property=, twitter: uses name= — swap them and the tag is silently ignored. Two: missing tags fall back, they don't fail. Three: every platform truncates at a different length. Four: the image must be an absolute URL, roughly 1200×630, and reachable by an anonymous crawler. Five: crawlers don't run your JavaScript and they cache — the tags must be in the HTML the server returns, and you use each platform's debugger to force a re-scrape.

The lesson underneath: a social preview isn't something you design per platform. It's one honest set of tags, and each platform resolves, truncates and lays it out its own way. Your job is to write the tags well and know how far each one gets clipped.

Fill the fields or paste a head, watch the three cards, copy the tags:
https://dev48v.infy.uk/solve/day39-og-previewer.html

Top comments (0)