【Next.js】markdown-itでリンクカードを作成する
2024-04-15に公開
当ブログにリンクカードを実装しました!
GitHubGitHub - markdown-it/markdown-it: Markdown pars...
直書きのURLに対してリンクカードを生成するようにしております。
なかなか調べても情報が少なかったので、
catnoseさんのzenn-editorのコードで勉強させていただきました。
GitHubzenn-editor/packages/zenn-markdown-html/src/uti...
誰かの参考にしていただけたら嬉しいです!
使用するもの
- markdown-it
- markdownからhtmlにパースする
- open-graph-scraper
- OGP情報を簡単にスクレイピングして取得できるライブラリです
ロジック(このサイト)
- マークダウンデータから直書きリンクを探す
- OGP情報を取得
- markdown-it用のプラグインを自作する
- markdown-itでのパース時に、自作したプラグインを読みこむ
- 1,2,4をサーバー側で読み込む
1. マークダウンデータから直書きリンクを探す
このブログはマークダウンデータをDBに保存しています。
データから直書きリンクを正規表現で抽出し配列に入れます。
export function extractLinks(md: string) {
const regFloatLink =
/(?<!\()https?:\/\/[-_.!~*\\'()a-zA-Z0-9;\\/?:\\@&=+\\$,%#]+/g;
const floatLinks = md.match(regFloatLink);
return floatLinks ?? [];
}
2. OGP情報を取得
1で取得したリンクを元に、open-graph-scraperを使用し直書きリンクからOGP情報を取得します。
これは、リンクカードを表示するときのテキストやfavicon, ogp画像を使用するためです。
export async function getOgpData(
autolink: string[],
): Promise<OgpDataRes[] | undefined> {
let res: any = [];
if (autolink.length === 0) {
return res;
}
if (!autolink) return;
await Promise.all(
autolink.map(async (link) => {
const { error, html, result, response } = await ogs({
url: link,
timeout: 5000,
});
// エラー時とデータ取得失敗の際
if (error || !result.success) {
if (error) {
console.error('ogp取得エラー1', error);
} else if (!result.success) {
console.error('ogp取得エラー2', result.success);
}
return response;
}
// OGPデータ取得に成功;
res.push(result);
}),
);
return res;
}
無事、OGPデータを取得できると下記のように情報が返ってきます。
いくつか情報が返ってきますが、
このサイトで使用しているのは下記になります。
- ogSiteName
- ogTitle
- favicon
- ogImageのurl, alt
3. markdown-it用のプラグインを自作する
プラグインの作成に関しては、下記の記事を参考にさせていただきました。
export function linkCard(md: MarkdownIt, ogpDatas: OgpDataRes[]) {
md.core.ruler.after('replacements', 'link_card', ({ tokens }) => {
// https://から始まるリンクのみが対象
const linkRegex = /^https:\/\//;
tokens.forEach((token, i) => {
// 直書きリンクはinlineのchildrenにのみ存在するのでそれ以外は除外
if (token.type !== 'inline') return;
if (!token.children) return;
// リンクカードの生成
token.children.forEach((child) => {
if (
child.content &&
child.type === 'text' &&
child.markup !== 'linkify' &&
linkRegex.test(child.content)
) {
// ogpDataのリンクと同じ場合htmlを生成する
const ogpData = ogpDatas.find((data) => {
return data.ogUrl === child.content;
});
console.log(ogpData);
if (ogpData) {
child.content = `
<section class="md-link-card">
<div class="og-text">
<h1>${ogpData.ogTitle}</h1>
<div class="og-text__icon">
${
ogpData.favicon
? `<img src="${ogpData.favicon ?? 'リンクカードのfaviconです'}" />`
: ``
}
<p>${ogpData.ogSiteName}</p>
</div>
</div>
<div class="og-img">
<img src="${ogpData.ogImage[0].url}" alt="${ogpData.ogImage[0].alt ?? 'リンクカードのイメージです'}">
</div>
</section>
`;
child.type = 'html_block';
}
}
});
});
});
return true;
}
4.markdown-itでのパース時に、自作したプラグインを読みこむ
3で作成したlinkCard
プラグインをパース時に動くように読み込みます。
import MarkdownIt from 'markdown-it';
import { linkCard } from './linkCard';
export function changeHtml(markdown: string, ogpDatas?: any) {
// オプションが別途必要な場合はlinkifyの後に書けばOK
const md: MarkdownIt = MarkdownIt({
html: true,
linkify: true,
});
// 自作プラグインを読み込み取得したOGP情報を渡す
if (ogpDatas) {
md.use(linkCard, ogpDatas);
}
return md.render(markdown);
}
5. 1,2,4をサーバー側で読み込む
これで準備ができたので、Next.jsのgetStaticProps
で実行します。
SSR, ISRでも同様で大丈夫だと思います。
export const getStaticProps: GetStaticProps = async () => {
// response.notice.contentの中にマークダウンデータが入っています。
--- 省略 ---
// リンクカード生成
const floatLink = extractLinks(response.notice.content ?? '');
const ogpDatas = await getOgpData(floatLink);
// マークダウンをhtmlに変換
const htmlContent = changeHtml(response.notice.content ?? '', ogpDatas);
--- 省略 ---
}
あとはCSSで装飾すればリンクカードが完成します!
当ブログはGithubで公開しておりますので、
コードの全般は下記のリンクカードからご覧いただければと思います。