Static Link Previews Using Site Metadata
Link previews are a great way to give your users more context about content you link to in articles and posts.
Wikipedia uses link previews to show short snippets of other Wikipedia articles, so you can get quick access to essential information without loading another page or breaking your reading flow.
Using NextJS, we can design and dynamically generate link previews during build time. This means, for all the added image and text content, adding link previews won't affect page load times by much, if at all.
Set up metadata fetching
To display a link preview, we first need information from the linked webpage. What information do we need? Luckily, we already have a standard for such structured information - metadata, in the form of meta
tags in the HTML header
. Modern webpages set various meta
tags to hold, well... metadata about the page. And search engines, messaging apps, and social media services all look to these meta
tags to retrieve and display link previews.
We could use a headless browser such as Puppeteer to render the page, then extract the information we like from the HTML header. We could also take a screenshot of the webpage to show as an image preview. However, in most cases, the added bulk of a headless browser in a build, plus the overhead of rendering each linked webpage, is simply not worth it.
Instead, we can use node-html-parser
alongside our good friend fetch
to load and parse through the HTML of webpages.
import { parse } from 'node-html-parser'
export const getLinksMeta = async (urls) => {
if (urls.length === 0) return []
let linksMeta = []
for (const url of urls) {
const response = await fetch(url)
const body = await response.text()
const rootElement = await parse(body)
const metaTags = rootElement.getElementsByTagName('meta')
const imgUrl = metaTags
.filter((meta) => meta.attributes?.name?.toLowerCase() === 'twitter:image' || meta.attributes?.property?.toLowerCase() === 'og:image')
.map((meta) => meta.attributes.content)[0]
const title = metaTags
.filter((meta) => meta.attributes?.name?.toLowerCase() === 'twitter:title' || meta.attributes?.property?.toLowerCase() === 'og:title')
.map((meta) => meta.attributes.content)[0]
linksMeta.push({ href: url, title, imgUrl })
}
return linksMeta
}
To handle edge cases, we also check for og:image
and og:title
Open Graph meta properties in addition to the twitter:
variants above. Some sites also use a property
key instead of name
in the <meta>
tag, so we check for those as well. To make things easier, you may even want to use an out of the box solution like metascraper.js, which takes in HTML and returns normalized metadata as JSON.
As is, this function can be used anywhere to get link metadata. Within a React component, you could use this within useEffect
or similar hook. However, this approach would mean metadata is fetched every time the component mounts.
Fetch on build
Since link previews aren't intended to be dynamically refreshed, and since webpage metadata doesn't change frequently, we can take advantage of server-side rendering (SSR). NextJS makes this easy with getStaticProps
. Here, we pass in the list of URLs that we want metadata for, and the result will be cached as static assets that will be served alongside SSR HTML. We can use this in a NextJS dynamic route, which will generate static assets for all blog posts, for example.
We also need to associate each link's metadata result with the URL itself.
import { getLinksMeta} from 'lib/meta'
export default function Home({ content, linksMetadata }) {
const components = {
a: (props) => {
const metadata = linksMetadata.find((link) => link.href === props.href)
return <LinkWithPreview metadata={metadata} />
}
}
return (
<article>
<MDXContent components={components} />
</article>
)
}
export const getStaticProps = async ({ params }) => {
const content = fetchContent(params.id)
const links = fetchLinksFromContent(content)
return {
props: {
content: content,
linksMetadata: await getLinksMeta(links)
},
}
}
Customize
To show link previews, we need to iterate over the linksMetadata
array to find the corresponding link to show next to its anchor tag in the content body. Styling the preview itself also depends on your individual setup. Using Tailwind makes designing things a breeze:
const LinkWithPreview = ({ metadata, linkText }) => {
const { title, imgUrl, href } = metadata
return (
<div className='relative inline-block'>
<a className='relative peer' href={href}>
{linkText}
</a>
<div className='absolute w-48 bg-slate-200 rounded-2xl shadow-xl p-4 peer-hover:visible invisible'>
<img src={imgUrl} className='rounded-xl h-36 w-full object-cover' loading='lazy' />
<span className='text-sm font-semibold leading-tight mt-2'>{title}</span>
</div>
</div>
)
}
And with that, we have a beautiful link preview component, with images and text all rendered server-side, without using client-side fetching or headless browsers, and without increasing page load time!
You can find all of the code for this project in this repo, including a working demo.
No spam, no sharing to third party. Only you and me.