🃏

Next.jsでリンクカードを実装する

2022/09/05に公開

Zennと同様の記述ができるようにした。単純な見栄えの問題もあるが、何より画像を挿入する手間が省けるのが嬉しい。
尚、Amazonリンクは他のサイトのようにOGPを取得できない仕様となっている。別個で対応する必要がある。

概要

リンクカード化にあたり、次のような挙動の実現を目指した。

  • ベタ貼りのリンクはリンクカード化
  • 通常のリンクこれはリンクカード化しない

具体的な実装の方法についてはこの記事が参考になった。
https://silurus.dev/articles/pO0Neonv8xwbuEnZigMNf

大まかな流れは次の通り。

  1. markdownからベタ貼りされたリンクを配列で抽出
  2. 抽出したリンクのmeta情報(opg情報)を一括で取得
  3. propsに渡す
  4. propsに渡されたogp情報を参照し、リンクカードを表示

リンクカードは各サイトのメタ情報に基づいてカードを表示する仕組みになっている。実装の対象となるサイトはSSG(静的なブログサイト)なので、getServerSidePropsは使わずgetStaticPropsで次の処理を行う必要があった。

1と2の工程はコードとしてはこのようになる。

//getStaticProps
//自分の例ではpost.contentでmarkdownを取得している
const floatingUrls = getFloatingUrls(post.content ?? '');
const ogpDatas = await fetchOgpData(floatingUrls);

さらにogpDatasはpropsに渡しておく

//省略
  return {
    props: {
      //省略...
      content: post.content,
      ogpDatas,
    },
  };
};

参考にした記事の通りの実装であるため、詳しいコードは割愛する。

LinkCardコンポーネントの実装

続いて、渡されたpropsからカードを表示する。まずリンクカードの描画に必要なComponentを用意しておく。リンクカード化するか否かの判定はComponent内で次のように行うことにした。

  • hreftextが同じか否かで判定
  • trueの場合ははFloatingURL(ベタ書きのurl)と判定し、リンクカード化
  • falseあるいはogpDataがundefinedの場合は通常のリンクとして早期リターン
const LinkCard = (props: ILinkCardProps) => {
  if (props.text !== props.href || !props.ogpData ) {
    return `
      <a href=${props.href} target="_brank">
        ${props.text}
      </a>`;
  }
  //省略
};
  • propsは最低限、次のように定義しておく。
type ILinkCardProps = {
  href: string | null;
  text: string | null;
  title: string | null;
  ogpData: OgpData | undefined;
};

//...

const { ogImage }: any = props.ogpData;
const image = Array.isArray(ogImage) ? ogImage[0] : ogImage;
const domain = getDomainFromUrl(props.ogpData?.ogUrl);
  

あとは参考にした記事の通りのため、詳しいコードは割愛。

Rendererのカスタマイズ

markdownをhtmlに変換するmarkedはRendererをoverride(カスタマイズ)可能な仕様となっている。リンクカード用に作ったComponentをreturnする際、OGP情報をPropsとして渡しておく。

const DisplayPost = (props: IPostProps) => {
  if (props.ogpDatas) {
    const renderer = new marked.Renderer();
    renderer.link = (href, title, text) => {
      const ogpData = props.ogpDatas.find(
        (data) => data.ogUrl && href?.startsWith(data.ogUrl)
      );
      return LinkCard({ href, title, text, ogpData });
    };
    marked.setOptions({
      renderer,
    });
  }

適当な箇所でRendererのカスタマイズを行なったmarkdedで変換し、あとはreturnする。

  return (
    //...
          <div  
            dangerouslySetInnerHTML={{
              __html: marked(props.content),
            }}
          /> 
    //...

最初は使い慣れたReactMarkdownで同様のことができないか試みた。だがその場合では、用意したComponentに対して任意のPropsを渡すことができないようだった。そのため下の記事のように、取得したogp情報の配列はグローバルステートに保存することになる。少々周りくどい。
そのほかの機能との兼ね合いもあるのでお好みで選ぶと良いと思う。
https://zenn.dev/tomi/articles/2021-03-22-blog-card

Discussion