🗂

Next.jsで作ったブログにリンクカードを実装する

2022/05/26に公開

リンクカードとは?

これです。外部リンクをMarkdownに記載したときに、自動的にリンク先の<meta>からtitle, description, imageの情報を取ってきて、カード形式でリンク先の情報を表示するものです。
↓こんな感じのものです。

References

こちらの記事を参考に実装しました。
https://zenn.dev/tomi/articles/2021-03-22-blog-card

環境

  • Next.js
  • Tailwind(css)

コード

/pages/Post.tsx

記事を表示するページです。SSGにしているので、getStaticPropsでmarkdownから、URLを正規表現で抽出し、getMetaDataでURLにアクセスして、<meta>の情報を取得して、NextPageに渡しています。getMetaDataでは、JSDOMを使っています。

pages/Post.tsx
import Markdown from '@/hogehoge'

export type Meta = {
  url: string
  title: string
  description: string
  image: string
}

const Post: NextPage<PostProps> = ({ post, metas }) => {
  ...
  return (
    <div>
      ...
      <Markdown source={post.content} metas={metas} />
    </div>
  )
}

async function getMetaData(url: string): Promise<Meta> {
  const metaData = {
    url,
    title: '',
    description: '',
    image: '',
  }
  try {
    const res = await fetch(url)
    const text = await res.text()
    const doms = new JSDOM(text)
    const metas = doms.window.document.getElementsByTagName('meta')

    for (const meta of metas) {
      const np = meta.getAttribute('name') || meta.getAttribute('property')
      if (typeof np !== 'string') continue
      if (np.match(/title/)) {
        metaData.title = meta.getAttribute('content')
      }
      if (np.match(/description/)) {
        metaData.description = meta.getAttribute('content').slice(0, 100)
      }
      if (np.match(/image/)) {
        metaData.image = meta.getAttribute('content')
      }
    }
  } catch (e) {
    console.error(e)
  }
  return metaData
}

function getUrlList(content: string): Array<string> {
  return content.match(/https?:\/\/[^\n\]]*/g) ?? []
}

export const getStaticProps: GetStaticProps<PostProps, Params> = async ({ params }) => {
  const post = getPostByPath() // mdが記載されているファイルを取得
  const urls = getUrlList(post.content) // mdの中から、外部リンク(URL)を配列で取得
  const metas = await Promise.all(urls.map(async (url) => await getMetaData(url))) // metaタグの情報を取得
  const filteredMetas = metas.filter((m) => m !== undefined) // undefinedのものをfilter
  return {
    props: {
      ...
      metas: filteredMetas, // propsを使って、Postに渡す
    },
  }
}



/components/Markdown.tsx

Markdownを表示するコンポーネントです。ReactMarkdownを使っており、以下のようにしています。

Markdown.tsx
...
import { Meta } from '@/pages/posts/[year]/[month]/[slug]'

export interface MarkdownInterface {
  source: string
  metas: Meta[]
}

const Markdown: React.FC<MarkdownInterface> = ({ source, metas }) => {
  ...

  const a: Components['a'] = (props) => {
    ...
    return (
      <LinkCard href={href} metas={metas}>
        {children}
      </LinkCard>
    )
  }

  return (
    <div className="markdown">
      <ReactMarkdown
        components={{
          ...
          a,
        }}
      >
        {source}
      </ReactMarkdown>
    </div>
  )
}

export default Markdown

/components/LinkCard.tsx

リンクカードのコンポーネントです。CSSは、tailwindを使っています。

LinkCard.tsx
import { Meta } from '@/hogehoge'

interface P {
  href: string
  children: any
  metas: Meta[]
}

const LinkCard: React.FC<P> = ({ href, children, metas }) => {
  const target = metas.find((meta) => meta.url == href)

  if (target) {
    return (
      <a href={href} target="_blank" rel="noreferrer">
        <div className="w-full flex justify-around bg-white rounded-md p-3 border lg:w-1/2">
          <div className="w-1/2">
            <img src={target.image} alt={target.title} className="max-h-20 m-auto" />
          </div>
          <div className="flex flex-col justify-start px-1 ml-3">
            <div className="text-sm font-bold text-black whitespace-pre-wrap">{target.title}</div>
            <div className="text-gray-400 text-xs whitespace-pre-wrap">{target.description}</div>
          </div>
        </div>
      </a>
    )
  }
  return (
    <a href={href} target="_brank">
      {children}
    </a>
  )
}

export default LinkCard

Discussion