ブログ内の外部リンクをカード型のコンポーネントにした話
October 5, 2021
はじめに
ブログの外部リンクコンポーネントをカード型のコンポーネントにしました。こういうやつです。
「カード型」というキーワードがなかなか出てこず、ずっと OGP 情報を設定する記事ばかり見つけてましたが、なんとか実装することができました。
アプローチ
marked
によって記事の文字列を HTML タグに変換する際に、カスタムの renderer を噛ませています。URL がそのまま書かれている部分のみを<a>
タグではなくカード型のコンポーネントに置き換えるというアプローチです。
実装のはなし
カード型の外部リンクコンポーネントを作成するに当たり、実装したのは以下です。
content から直書きの URL を抽出する
これは content を入力とし、正規表現にマッチした部分を抜き出し返却するものです。
export function getSlackingUrls(md: string): string[] {
const regSlackingUrl = /(?<!\\()https?:\\/\\/[-_.!~*\\\\'()a-zA-Z0-9;\\\\/?:\\\\@&=+\\\\$,%#]+/g;
const slackingUrls = md.match(regSlackingUrl);
return slackingUrls ?? [];
}
"slacking" とは「怠ける・手を抜く」という意味です。(という指摘がある時点で読みにくいですよね。全くリーダブルコードじゃないです。)rawUrls
とかでよかったですね。
URL から OGP 情報を取得する
OGP 情報の取得には open-graph-scraper
というものを使いました。
上記の関数で取得した各 URL を入力とし、OGP データを返します。
export async function getOGPData(slackingUrls: string[]): Promise<OGPData[]> {
const ogpData: OGPData[] = [];
if (slackingUrls.length === 0) return ogpData;
await Promise.all(
slackingUrls.map(async (url) => {
const options = { url, onlyGetOpenGraphInfo: true };
return openGraphScraper(options)
.then((data) => {
if (!data.result.success) {
// 失敗時の処理
return;
}
ogpData.push(data.result);
})
.catch(() => {
// エラー処理
return;
});
}),
);
return ogpData;
}
OGP 情報をもとにカード型コンポーネントを描画するカスタム renderer
各記事(markdown ファイル)で使用することになる OGP 情報を受け取り、カスタム renderer を返す関数を作成します。返された renderer を marked.use()
でプラグインとして登録しています。
function createLinkRenderer(ogpDatas: OGPData[]) {
const renderer = new marked.Renderer();
renderer.link = (href: string, title: string, text: string) => {
const sanitizedUrl = sanitizeUrl(href ?? undefined);
const ogpData = ogpDatas.find((data) => href === data.ogUrl || `${text}/` === data.ogUrl);
if ((text !== href && `${text}/` !== href) || !ogpData) {
return `
<a href="${sanitizedUrl}" target="_blanck" rel="noreferrer" class="text-blue-700 dark:text-blue-500 hover:underline">${text}${title}</a>`;
}
const { ogImage } = ogpData;
const image = Array.isArray(ogImage) ? ogImage[0] : ogImage;
const domain = getDomainFromUrl(ogpData?.ogUrl);
return `
<div>
<a href=${ogpData?.ogUrl} target="_blanck" class="og-link">
<div class="og-container">
<div class="og-thumbnail-container">
<img src="${image?.url}" alt="${ogpData?.ogTitle}" class="og-thumbnail"/>
</div>
<div class="og-text-container">
<p class="og-title">${ogpData?.ogTitle}</p>
<p class="og-description">${ogpData?.ogDescription}</p>
<div class="og-domain-container">
<img src="<https://www.google.com/s2/u/0/favicons?domain=${domain}>" alt="${domain}"/>
<div class="og-domain-name">${domain}</div>
</div>
</div>
</div>
</a>
</div>`;
};
return { renderer };
}
スタイリングは css をいい感じに書いて,_app.tsx
で読み込ませます。
仕上げ
以上のものを getStaticProps
内で呼び出し、各記事を生成します。
export async function getStaticProps({ params }: Params) {
const post = getPostBySlug(params.slug, ["some", "params"]);
// 直書きのURL抽出
const slackingUrls = getSlackingUrls(post.content);
// OGP データの取得
const ogpData = await getOGPData(slackingUrls);
// markdown の内容を html タグに変換
const content = await markdownToHtml(post.content || "", ogpData);
return {
props: {
post: {
...post,
content,
},
},
};
}
以上で完成です。
参考
(追記) リンク先ページの URL が更新されていたので修正しました。
さいごに
次はコードブロックのシンタックスハイライトですね。