a link's six facts, read from its page at authoring time or live on a server
npx shadcn add @ag/web-preview-unfurlAlready installed? Preview an update
npx shadcn add @ag/web-preview-unfurl --dry-runYour installed source stays unchanged until you update it. How updates work
Latest changes · 2026-10-16
unfurlfetches throughpublicFetch: a server unfurling what visitors type never asks its own network (http://169.254.169.254/,localhost,10.0.0.5), on the first request or after a redirect. An address with another scheme (file://) is refused rather than read as a host. It now depends on@ag/web-preview-factsand@ag/public-fetch.
also called open graph
shelf social & links
part of web-preview
uses web-preview-facts, public-fetch
used by browser
A live preview fetches what strangers type. unfurl asks the public internet only: these are refused before any request.
| http://169.254.169.254/latest/meta-data/ | publicFetch http://169.254.169.254/latest/meta-data/: 169.254.169.254, not a public address |
| http://127.0.0.1:3000/api/admin | publicFetch http://127.0.0.1:3000/api/admin: 127.0.0.1, not a public address |
| http://[::1]/ | publicFetch http://[::1]/: ::1, not a public address |
| http://10.0.0.5/ | publicFetch http://10.0.0.5/: 10.0.0.5, not a public address |
| http://[::ffff:192.168.1.1]/ | publicFetch http://[::ffff:c0a8:101]/: ::ffff:c0a8:101, not a public address |
| file:///etc/passwd | publicFetch file:///etc/passwd: file: is not http(s) |
the demo above, verbatim
import { Sample } from "@/app/samples"
import { unfurl } from "@/registry/base-nova/lib/web-preview-unfurl"
/** Addresses a stranger could type into a live preview, each pointing inside the
* server's own network. Every one is refused before a request is made, so this
* page asks nothing of anyone. */
// #region inside
const INSIDE = [
"http://169.254.169.254/latest/meta-data/",
"http://127.0.0.1:3000/api/admin",
"http://[::1]/",
"http://10.0.0.5/",
"http://[::ffff:192.168.1.1]/",
"file:///etc/passwd",
]
// #endregion
export default async function Demo() {
const answers = await Promise.all(
INSIDE.map((url) =>
unfurl(url).then(
(facts) => `read: ${facts.title}`,
(error: Error) => error.message,
),
),
)
return (
<Sample
name="refused"
title="addresses inside the network"
description="A live preview fetches what strangers type. unfurl asks the public internet only: these are refused before any request."
with="inside"
>
<table className="w-full font-mono text-xs">
<tbody>
{INSIDE.map((url, i) => (
<tr key={url} className="border-border border-t first:border-t-0">
<td className="py-2 pr-4">{url}</td>
<td className="py-2 text-muted-foreground">{answers[i]}</td>
</tr>
))}
</tbody>
</table>
</Sample>
)
}the source, every file an install writes
// THE ONE FETCH THAT READS A LINK'S FACTS (`Unfurl`, web-preview-facts) from its
// page; nothing else in the family fetches, and the card is a pure function of them.
//
// WHERE THE FETCH RUNS is a decision, and it is: at authoring time, on the machine that
// has the network, the way a portrait's sidecar is written once by `mise portrait`. A
// browser cannot unfurl a foreign page (CORS), and a build that unfurls on every deploy
// makes the site depend on YouTube answering at 3 a.m. So a site runs `mise unfurl
// <url>` once, pastes the JSON beside the link, and ships facts as data; a server that
// wants to unfurl live imports this same function. A live server fetches what
// strangers type, so every request goes through `publicFetch`: nothing but the public
// internet is ever asked, never the server's own network.
import { publicFetch } from "@/registry/base-nova/lib/public-fetch"
import {
hostOf,
hrefOf,
type Unfurl,
} from "@/registry/base-nova/lib/web-preview-facts"
const ENTITIES: Record<string, string> = {
amp: "&",
lt: "<",
gt: ">",
quot: '"',
apos: "'",
"#39": "'",
nbsp: " ",
}
function decode(s: string): string {
return s.replace(/&(#\d+|#x[0-9a-f]+|[a-z]+);/gi, (m, code: string) => {
if (code[0] === "#") {
const n =
code[1] === "x" || code[1] === "X"
? Number.parseInt(code.slice(2), 16)
: Number.parseInt(code.slice(1), 10)
return Number.isFinite(n) ? String.fromCodePoint(n) : m
}
return ENTITIES[code.toLowerCase()] ?? m
})
}
/** The `content` of the first <meta> whose property or name is one of `keys`, in
* either attribute order. */
function meta(html: string, keys: string[]): string | undefined {
for (const key of keys) {
const k = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
const re = new RegExp(
`<meta\\b(?:[^>]*?\\b(?:property|name)=["']${k}["'][^>]*?\\bcontent=["']([^"']*)["']|[^>]*?\\bcontent=["']([^"']*)["'][^>]*?\\b(?:property|name)=["']${k}["'])`,
"i",
)
const m = re.exec(html)
const v = m?.[1] ?? m?.[2]
if (v) return decode(v.trim())
}
return undefined
}
function iconOf(html: string, base: string): string | undefined {
const m =
/<link\b[^>]*\brel=["'](?:shortcut )?icon["'][^>]*\bhref=["']([^"']+)["']/i.exec(
html,
) ??
/<link\b[^>]*\bhref=["']([^"']+)["'][^>]*\brel=["'](?:shortcut )?icon["']/i.exec(
html,
)
return m?.[1] ? new URL(decode(m[1]), base).href : undefined
}
/** Fetch a page and read its facts. THROWS on a page with no title: a preview with
* nothing to say is not a preview, and a card drawn from an empty fetch is the bug
* this refuses to hide. */
export async function unfurl(url: string): Promise<Unfurl> {
const href = hrefOf(url)
// YouTube serves a bot a consent shell with no <title>; it publishes oEmbed for
// exactly this, so a video's facts come from there: title, channel, thumbnail.
if (/^(www\.)?(youtube\.com|youtu\.be)$/.test(hostOf(href))) {
const res = await fetch(
`https://www.youtube.com/oembed?format=json&url=${encodeURIComponent(href)}`,
)
if (!res.ok) throw new Error(`unfurl ${href}: oEmbed HTTP ${res.status}`)
const o = (await res.json()) as {
title?: string
author_name?: string
thumbnail_url?: string
}
if (!o.title) throw new Error(`unfurl ${href}: oEmbed has no title`)
return {
url: href,
site: "YouTube",
title: o.title,
description: o.author_name,
image: o.thumbnail_url,
favicon:
"https://www.youtube.com/s/desktop/f8c8418d/img/favicon_32x32.png",
}
}
const res = await publicFetch(href, {
headers: {
// A browser's document request: the page as a reader is served it, in English.
"user-agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36",
accept: "text/html,application/xhtml+xml",
"accept-language": "en",
},
})
if (!res.ok) throw new Error(`unfurl ${href}: HTTP ${res.status}`)
const html = (await res.text()).slice(0, 512 * 1024)
const tag = /<title[^>]*>([^<]*)<\/title>/i.exec(html)?.[1]
const title =
meta(html, ["og:title", "twitter:title"]) ??
(tag ? decode(tag).trim() : undefined)
if (!title) throw new Error(`unfurl ${href}: the page has no title`)
const image = meta(html, ["og:image", "og:image:url", "twitter:image"])
return {
url: href,
site: meta(html, ["og:site_name"]) ?? hostOf(href),
title,
description: meta(html, [
"og:description",
"twitter:description",
"description",
]),
image: image ? new URL(image, res.url || href).href : undefined,
favicon: iconOf(html, res.url || href),
}
}