lens-blocks
componentlink-previewsource: ok · hit
Link Preview
A component to display a link preview for URLs included in Lens posts, showing the title, description, and image.
npx shadcn add "https://lensblocks.com/r/link-preview.json"free
Where it goes
testimonial
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightnode-html-parser@7.0.1
Registry dependencies
https://lensblocks.com/r/utils.json
Files4 · source cached 10/14/2026, 2:18:35 PM
registry/new-york/components/feed/link-preview.tsx
import { useEffect, useState } from "react";
import { UrlMetadata } from "@/registry/new-york/lib/get-meta-from-url";
import getFavicon from "@/registry/new-york/lib/get-favicon";
import { truncateUrl } from "@/registry/new-york/lib/lens-utils";
type Props = {
url: string;
};
export const LinkPreview = (props: Props) => {
const { url } = props;
const [urlMetadata, setUrlMetadata] = useState<UrlMetadata | null>(null);
const favicon = getFavicon(url);
useEffect(() => {
if (!url) return;
fetch(`/api/url-meta?url=${encodeURIComponent(url)}`)
.then(r => r.json())
.then(setUrlMetadata);
}, [url]);
if (!urlMetadata) {
return null;
}
return (
<div onClick={event => event.stopPropagation()}>
<a
href={url}
className="w-full max-w-sm flex flex-col bg-card text-card-foreground border rounded-lg mb-4 cursor-pointer hover:!no-underline"
target="_blank"
rel="noopener noreferrer"
>
<div className="flex flex-col p-2 flex-1 min-w-0 gap-0.5 overflow-hidden">
<div className="flex gap-1 items-center text-xs font-normal">
<img
src={urlMetadata.icon ?? favicon}
width={24}
height={24}
alt={`${url} favicon`}
className="w-4 h-4 p-0.5"
/>
{urlMetadata.siteName && <div className="line-clamp-1 opacity-60">{urlMetadata.siteName + " • "}</div>}
<div className="flex-none opacity-60">{truncateUrl(urlMetadata.url)}</div>
</div>
{urlMetadata.title && <div className="line-clamp-1 !font-bold text-sm">{urlMetadata.title}</div>}
{urlMetadata.description && (
<div className="line-clamp-2 text-xs !font-normal">{urlMetadata.description}</div>
)}
</div>
</a>
</div>
);
};
registry/new-york/lib/get-meta-from-url.ts
import { parse } from "node-html-parser";
export type UrlMetadata = {
url: string;
title: string | null;
description: string | null;
siteName: string | null;
icon: string | null;
};
type Props = {
url: string;
userAgent?: string;
};
export default async function getMetaFromUrl({ url, userAgent = "LensBot/0.1 (like TwitterBot)" }: Props) {
const res = await fetch(url, { headers: { "User-Agent": userAgent }, redirect: "follow" });
if (!res.ok) return { url, title: null, description: null, siteName: null, icon: null };
const html = await res.text();
const root = parse(html);
const pick = (names: string[]) => {
for (const name of names) {
const tag = root.querySelector(`meta[name="${name}"]`) || root.querySelector(`meta[property="${name}"]`);
const val = tag?.getAttribute("content");
if (val) return val;
}
return null;
};
const title = pick(["og:title", "twitter:title"]) || root.querySelector("title")?.text.trim() || null;
const description = pick(["og:description", "twitter:description", "description"]) || null;
const siteName = pick(["og:site_name"]) || null;
const iconTags = root.querySelectorAll(
'link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"], link[rel="mask-icon"]',
);
let icon: string | null = null;
let bestScore = -1;
for (const el of iconTags) {
const href = el.getAttribute("href");
if (!href) continue;
let score = href.toLowerCase().endsWith(".svg") ? 1000 : 0;
const sizes = el.getAttribute("sizes");
if (sizes) {
const m = sizes.match(/(\d+)x(\d+)/);
if (m) score += Math.max(parseInt(m[1], 10), parseInt(m[2], 10));
}
if (score > bestScore) {
bestScore = score;
icon = new URL(href, url).toString();
}
}
if (!icon) icon = new URL("/favicon.ico", url).toString();
return { url, title, description, siteName, icon };
}
registry/new-york/lib/get-favicon.ts
const FAVICON_BASE_URL = "https://external-content.duckduckgo.com/ip3";
const UNKNOWN_DOMAIN = "unknowndomain";
const getFavicon = (url: string): string => {
const domain = (() => {
try {
return new URL(url).hostname;
} catch {
return UNKNOWN_DOMAIN;
}
})();
return `${FAVICON_BASE_URL}/${domain || UNKNOWN_DOMAIN}.ico`;
};
export default getFavicon;
registry/new-york/files/api/url-meta/route.ts
import { NextResponse } from "next/server";
import getMetaFromUrl from "@/registry/new-york/lib/get-meta-from-url";
export const dynamic = "force-dynamic";
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const url = searchParams.get("url");
if (!url) return NextResponse.json({ error: "url required" }, { status: 400 });
const meta = await getMetaFromUrl({ url });
return NextResponse.json(meta, { headers: { "Cache-Control": "max-age=0, s-maxage=3600" } });
}