Static Link Previews Using Site Metadata

by Kartik Chaturvedi

August 3, 2022
Static Link Previews Using Site Metadata image

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.

This tutorial uses NextJS, but you can use almost any other framework, with slight changes to these steps. While you may need to add additional logic based on your individual setup, these steps should still hold in general, regardless of framework, content source, and language.

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.

/lib/meta.js
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.

Metascraper: unified metadata from the webOpen Graph, Microdata, RDFa, Twitter Cards, JSON-LD, HTML, and more.metascraper.js.org

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.

/pages/index.js
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)
    },
  }
}

The code snippet above skips over the implementation of fetchContent and fetchLinksFromContent to make this tutorial as framework-agnostic as possible. If you are starting from scratch, packages such as Contentlayer make this a trivial implementation.

Contentlayer makes content easy for developersContentlayer is a content SDK that validates and transforms your content into type-safe JSON data you can easily import into your application.contentlayer.dev/

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:

/components/LinkWithPreview.js
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!

Article image
Link preview on a NextJS link

You can find all of the code for this project in this repo, including a working demo. Enjoy experimenting, and be sure to share your creations with me on Twitter!

GitHub - kchaturvedi/link-previews: Link Previews – a quick demoLink Previews – a quick demo. Contribute to kchaturvedi/link-previews development by creating an account on GitHub.GitHub

Kartik's Newsletter

Subscribe to get science and tech news, new posts, and the latest updates from me.

Subscribe on Substack