Next.jsでリンクカードを実装する
Zennと同様の記述ができるようにした。単純な見栄えの問題もあるが、何より画像を挿入する手間が省けるのが嬉しい。
尚、Amazonリンクは他のサイトのようにOGPを取得できない仕様となっている。別個で対応する必要がある。
概要
リンクカード化にあたり、次のような挙動の実現を目指した。
- ベタ貼りのリンクはリンクカード化
- 通常のリンクこれはリンクカード化しない
具体的な実装の方法についてはこの記事が参考になった。
大まかな流れは次の通り。
- markdownからベタ貼りされたリンクを配列で抽出
- 抽出したリンクのmeta情報(opg情報)を一括で取得
- propsに渡す
- 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内で次のように行うことにした。
-
href
とtext
が同じか否かで判定 -
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情報の配列はグローバルステートに保存することになる。少々周りくどい。
そのほかの機能との兼ね合いもあるのでお好みで選ぶと良いと思う。
Discussion