OverviewFilesCommitsIssuesDocsReleasesPeers

holepunchto-deps / @holepunchto / keet-link-preview / index.js

81 lines · javascript ref:
import fastMetaTags from 'fast-meta-tags'
import { getFile, getDomain, htmlToText } from './utils.js'
import { fetchOembed } from './oembed.js'

export default async function getLinkPreview(url, opt) {
  if (url.startsWith('http://')) url = 'https' + url.slice(4)

  const [oem, data] = await Promise.all([fetchOembed(url, opt), fastMetaTags(url, opt)])
  if (!oem && !data) {
    return null
  }

  const image = oem?.image || (data ? getImage(data) : null)
  const meta = data ? parse({ ...data, image }) : {}

  return {
    url,
    favicon: data?.favicon || null,
    title: oem?.title || meta.title || null,
    name: oem?.name || meta.name || getDomain(url),
    description: oem?.description || meta.description || null,
    file: await getFile(image, opt)
  }
}

function parse(data) {
  return {
    url: data.url,
    favicon: data.favicon,
    title: getTitle(data),
    name: getSiteName(data),
    description: getDescription(data)
  }
}

function getSiteName(data) {
  return (
    getMetaContent(data.meta, {
      name: 'og:site_name',
      property: 'og:site_name'
    }) || getDomain(data.url)
  )
}

function getTitle(data) {
  return getMetaContent(data.meta, { name: 'og:title', property: 'og:title' }) || data.title
}

function getDescription(data) {
  const description = getMetaContent(data.meta, { name: 'description', property: 'og:description' })
  if (description) {
    const text = htmlToText(description)
    return text && text.length > 500 ? text.slice(0, 500) : text
  }
  if (!data.image && data.html) {
    const paragraphs = data.html.match(/<p(\s[^>]*)?>.*?<\/p>/gis) || []
    for (const p of paragraphs) {
      const text = htmlToText(p)
      if (text && text.length > 100) {
        return text.length > 500 ? text.slice(0, 500) : text
      }
    }
  }
  return null
}

function getImage(data) {
  return getMetaContent(data.meta, { name: 'og:image', property: 'og:image' })
}

function getMetaContent(meta, filter) {
  const entries = Object.entries(filter)
  return (
    meta.find((m) => {
      for (const [k, v] of entries) {
        if (m[k] === v) return true
      }
    })?.content || null
  )
}