Claude
Skills
Sign in
Back

og-inspect

Included with Lifetime
$97 forever

Inspect a URL's Open Graph and Twitter Card metadata and render a styled HTML page showing how the link unfurls on Facebook, X/Twitter, LinkedIn, Slack/Discord, and Tumblr. Also prints a JSON summary to stdout. Use when the user asks to "preview a link", "check OG tags", "inspect Open Graph", "see how a URL looks when shared", or invokes `/og-inspect <url>`.

Image & Video

What this skill does


# /og-inspect — Open Graph / share-card inspector

Given a URL, fetch the page, extract Open Graph + Twitter Card metadata, render a styled HTML preview showing how the link unfurls on Facebook, X/Twitter, LinkedIn, Slack/Discord, and Tumblr, and print a JSON summary. Like https://www.opengraph.xyz/ but local.

## Workflow

1. **Get the URL.** The user invokes `/og-inspect <url>`. If no URL is provided, ask: "Which URL should I inspect?"

2. **Run the inspector.** Execute the bash block below with the URL as `$1`. It fetches the page with `curl`, parses meta tags with Python stdlib (`html.parser`), writes a styled HTML preview to `~/claude.nosync/og-previews/YYYY-MM-DD-<host>-<slug>.html`, prints the absolute path of the HTML file to stderr, and prints the JSON summary to stdout.

3. **Open the HTML file** in the default browser with `open "<path>"`.

4. **Print the JSON** result to the terminal (it is already on stdout from step 2 — relay it to the user).

## The script

Run this with the URL as the first argument. Treat the URL as untrusted input — the script already does shell-safe quoting and no `eval`.

```bash
set -uo pipefail

URL="${1:-}"
if [ -z "$URL" ]; then
  echo "usage: og-inspect <url>" >&2
  exit 1
fi
# Normalize: prepend https:// if no scheme
if ! printf '%s' "$URL" | grep -qE '^https?://'; then
  URL="https://$URL"
fi

OUTDIR="$HOME/claude.nosync/og-previews"
mkdir -p "$OUTDIR"
TMP_HTML="$(mktemp -t og-inspect.XXXXXX).html"
TMP_META="$(mktemp -t og-inspect.XXXXXX).meta"
TMP_JSON="$(mktemp -t og-inspect.XXXXXX).json"
TMP_PATH="$(mktemp -t og-inspect.XXXXXX).path"
trap 'rm -f "$TMP_HTML" "$TMP_META" "$TMP_JSON" "$TMP_PATH"' EXIT

# Fetch. Always emit the meta file even on failure so Python can render a stub.
# On TLS verification failure (curl exit 60), retry with -k and flag a warning.
TLS_INSECURE=0
CURL_COMMON=( -sSL
  -A "Mozilla/5.0 (compatible; OGInspectBot/1.0)"
  -H "Accept: text/html,application/xhtml+xml"
  --max-time 15
  --max-filesize 5000000
  -w '%{http_code}\n%{url_effective}\n%{content_type}\n'
  -o "$TMP_HTML" )
curl "${CURL_COMMON[@]}" "$URL" > "$TMP_META" 2>/dev/null
RC=$?
if [ "$RC" -eq 60 ]; then
  TLS_INSECURE=1
  curl -k "${CURL_COMMON[@]}" "$URL" > "$TMP_META" 2>/dev/null
  RC=$?
fi
if [ "$RC" -ne 0 ]; then
  printf '0\n%s\n\n' "$URL" > "$TMP_META"
fi

# Parse + render. Python writes JSON to TMP_JSON and the output filepath to TMP_PATH.
python3 - "$URL" "$TMP_HTML" "$TMP_META" "$OUTDIR" "$TLS_INSECURE" >"$TMP_JSON" 2>"$TMP_PATH" <<'PY'
import sys, os, json, html, re
from datetime import datetime, timezone
from html.parser import HTMLParser
from urllib.parse import urljoin, urlparse

orig_url, tmp_html, tmp_meta, outdir, tls_insecure = sys.argv[1:6]
tls_insecure = tls_insecure == "1"

with open(tmp_meta) as f:
    lines = f.read().splitlines()
status_str = lines[0] if len(lines) > 0 else "0"
final_url  = lines[1] if len(lines) > 1 and lines[1] else orig_url
ctype      = lines[2] if len(lines) > 2 else ""

# Decode body with charset detection (file may be missing/empty if curl failed)
try:
    with open(tmp_html, "rb") as f:
        raw = f.read()
except FileNotFoundError:
    raw = b""
charset = None
m = re.search(r"charset=([\w\-]+)", ctype, re.I)
if m: charset = m.group(1)
if not charset:
    mm = re.search(rb'<meta[^>]+charset=["\']?([\w\-]+)', raw[:4096], re.I)
    if mm:
        try: charset = mm.group(1).decode("ascii")
        except Exception: pass
charset = charset or "utf-8"
try:
    text = raw.decode(charset, errors="replace")
except LookupError:
    text = raw.decode("utf-8", errors="replace")
text = text.replace("\x00", "")

class MetaParser(HTMLParser):
    def __init__(self):
        super().__init__()
        self.metas, self.links = [], []
        self.title = None
        self._in_title = False
        self._title_parts = []
    def handle_starttag(self, tag, attrs):
        d = {k.lower(): v for k, v in attrs}
        if tag == "meta":
            prop = d.get("property") or d.get("name") or d.get("itemprop")
            content = d.get("content")
            if prop and content is not None:
                self.metas.append((prop.lower(), content))
        elif tag == "link":
            rel, href = d.get("rel"), d.get("href")
            if rel and href: self.links.append((rel.lower(), href))
        elif tag == "title":
            self._in_title = True
    def handle_endtag(self, tag):
        if tag == "title":
            self._in_title = False
            if self.title is None:
                self.title = "".join(self._title_parts).strip()
    def handle_data(self, data):
        if self._in_title: self._title_parts.append(data)

p = MetaParser()
try: p.feed(text)
except Exception: pass

def collect(prefix):
    out = {}
    for k, v in p.metas:
        if k == prefix or k.startswith(prefix + ":"):
            sub = k[len(prefix):].lstrip(":") or "_"
            out.setdefault(sub, v.strip())
    return out

og = collect("og")
tw = collect("twitter")

def by_name(n):
    for k, v in p.metas:
        if k == n: return v.strip()
    return None

def resolve(u):
    if not u: return None
    try: return urljoin(final_url, u)
    except Exception: return u

for d in (og, tw):
    if d.get("image"): d["image"] = resolve(d["image"])
    if d.get("url"):   d["url"]   = resolve(d["url"])

canonical = favicon = apple_icon = None
for rel, href in p.links:
    rels = rel.split()
    if "canonical"        in rels and not canonical:  canonical  = resolve(href)
    if "icon"             in rels and not favicon:    favicon    = resolve(href)
    if "apple-touch-icon" in rels and not apple_icon: apple_icon = resolve(href)

other = {
    "title":       p.title,
    "description": by_name("description"),
    "author":      by_name("author"),
    "theme_color": by_name("theme-color"),
    "canonical":   canonical,
    "favicon":     favicon or apple_icon,
}

warnings = []
if not og.get("title"):       warnings.append("missing og:title")
if not og.get("description"): warnings.append("missing og:description")
if not og.get("image"):       warnings.append("missing og:image")
if not tw.get("card"):        warnings.append("missing twitter:card")
if status_str == "0":
    warnings.append("fetch failed (network, DNS, timeout, or size cap)")
elif status_str != "200":
    warnings.append(f"HTTP status {status_str}")
if ctype and "html" not in ctype.lower():
    warnings.append(f"non-HTML content-type: {ctype}")
if tls_insecure:
    warnings.append("TLS verification failed — refetched with -k (cert mismatch or self-signed)")

status_int = int(status_str) if status_str.isdigit() else 0

result = {
    "url":          orig_url,
    "final_url":    final_url,
    "fetched_at":   datetime.now(timezone.utc).isoformat(),
    "status":       status_int,
    "content_type": ctype,
    "tls_insecure": tls_insecure,
    "warnings":     warnings,
    "og":           og,
    "twitter":      tw,
    "other":        other,
}

# Filename
parsed = urlparse(final_url)
host = (parsed.hostname or "unknown").lower()
if host.startswith("www."): host = host[4:]
host_slug = re.sub(r"[^a-z0-9]+", "-", host).strip("-") or "unknown"
seg = (parsed.path.strip("/").split("/", 1)[0] or "root")[:30]
path_slug = re.sub(r"[^a-z0-9]+", "-", seg.lower()).strip("-") or "root"
fname = f"{datetime.now().strftime('%Y-%m-%d')}-{host_slug}-{path_slug}.html"
fpath = os.path.join(outdir, fname)

# HTML rendering
def esc(s):
    return html.escape("" if s is None else str(s), quote=True)

def img_or_placeholder(url, alt=""):
    if url:
        return f'<img src="{esc(url)}" alt="{esc(alt)}" loading="lazy" referrerpolicy="no-referrer">'
    return '<div class="img-placeholder">no image</div>'

def table_rows(d):
    if not d:
        return '<tr><td colspan="2" class="missing">— no tags found —</td></tr>'
    rows = []
    for k, v in sorted(d.items()):
        rows.append(f'<tr><th>{esc(k)}</th><td title="{esc(v)}">{esc(v)}</td></tr>')
    return "".jo
Files: 1
Size: 20.1 KB
Complexity: 31/100
Category: Image & Video

Related in Image & Video