Standard.site landing page built in Next.js
at main 111 lines 4.4 kB view raw
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}