forked from
standard.site/standard.site
Standard.site landing page built in Next.js
1
2interface LinkCardProps {
3 title?: string
4 description?: string
5 url: string
6 image?: string
7}
8
9async function fetchOGMetadata(url: string) {
10 try {
11 const response = await fetch(url, {
12 next: { revalidate: 86400 }, // Cache for 24 hours
13 headers: {
14 'User-Agent': 'Mozilla/5.0 (compatible; Standard.site/1.0; +https://standard.site)'
15 }
16 })
17
18 if (!response.ok) {
19 console.warn(`Failed to fetch ${url}: ${response.status}`)
20 return null
21 }
22
23 const html = await response.text()
24 const urlObj = new URL(url)
25
26 // Extract OG metadata - try both property and name attributes
27 let ogImage =
28 html.match(/<meta[^>]*property=["']og:image["'][^>]*content=["']([^"']*)["']/i)?.[1] ||
29 html.match(/<meta[^>]*content=["']([^"']*)["'][^>]*property=["']og:image["']/i)?.[1]
30
31 const ogTitle =
32 html.match(/<meta[^>]*property=["']og:title["'][^>]*content=["']([^"']*)["']/i)?.[1] ||
33 html.match(/<meta[^>]*content=["']([^"']*)["'][^>]*property=["']og:title["']/i)?.[1]
34
35 const ogDescription =
36 html.match(/<meta[^>]*property=["']og:description["'][^>]*content=["']([^"']*)["']/i)?.[1] ||
37 html.match(/<meta[^>]*content=["']([^"']*)["'][^>]*property=["']og:description["']/i)?.[1]
38
39 // Resolve relative image URLs to absolute
40 if (ogImage && !ogImage.startsWith('http')) {
41 if (ogImage.startsWith('//')) {
42 ogImage = `${urlObj.protocol}${ogImage}`
43 } else if (ogImage.startsWith('/')) {
44 ogImage = `${urlObj.protocol}//${urlObj.host}${ogImage}`
45 } else {
46 ogImage = `${urlObj.protocol}//${urlObj.host}/${ogImage}`
47 }
48 }
49
50 return {
51 image: ogImage || null,
52 title: ogTitle || null,
53 description: ogDescription || null
54 }
55 } catch (error) {
56 console.error('Failed to fetch OG metadata from', url, error)
57 return null
58 }
59}
60
61export async function LinkCard({ title, description, url, image }: LinkCardProps) {
62 const urlObj = new URL(url)
63 const hostname = urlObj.hostname.replace('www.', '')
64
65 // Fetch OG metadata if not provided
66 const metadata = await fetchOGMetadata(url)
67
68 const finalTitle = title || metadata?.title || hostname
69 const finalDescription = description || metadata?.description || ''
70 const finalImage = image || metadata?.image
71
72 urlObj.searchParams.set('utm_source', 'standard.site')
73 urlObj.searchParams.set('utm_medium', 'docs')
74 urlObj.searchParams.set('utm_campaign', 'implementations')
75
76 return (
77 <a
78 href={urlObj.toString()}
79 target="_blank"
80 rel="noopener noreferrer"
81 className="group flex rounded-2xl border border-border bg-linear-to-br from-base-200/50 to-base-200 transition-all hover:border-border/60 hover:shadow-lg not-prose mb-4"
82 >
83 <div className="flex min-w-0 flex-1 flex-col gap-2 px-3 py-2">
84 <hgroup className="flex flex-col gap-0">
85 <h3 className="font-bold tracking-tight text-base-content group-hover:underline line-clamp-2">
86 {finalTitle}
87 </h3>
88 {finalDescription && (
89 <p className="text-sm tracking-tight text-muted-content line-clamp-2">
90 {finalDescription}
91 </p>
92 )}
93 </hgroup>
94 <div className="mt-auto text-xs @md/content:pt-2 @md/content:border-t border-base-300/50 font-medium text-muted-content">
95 <span className="font-mono">{hostname}</span>
96 </div>
97 </div>
98 {finalImage && (
99 <div className="shrink-0">
100 <div className="size-26 @md/content:h-29 @md/content:aspect-[1.91/1] @md/content:w-auto overflow-hidden rounded-r-2xl bg-base-100">
101 <img
102 src={finalImage}
103 alt={finalTitle}
104 className="h-full w-full object-cover"
105 />
106 </div>
107 </div>
108 )}
109 </a>
110 )
111}