【Next.js】Markdownブログで個別にカスタムサムネイルを設定する
はじめに
みなさんは自身のMarkdownブログで「今回は特別にサムネイルを設定したい!」な時がありませんか?ありますよね!
Markdownブログの管理方法としてGitHubのリポジトリがあると思います.今回はそのリポジトリの画像フォルダにサムネイルを入れている場合を想定して実装し,実際にこの記事のサムネイルとして適用しました!
是非,投稿一覧やOGPが得られるサイト/サービスから確認してみてください!
↓ 実際のサムネイル
↓ 何も設定されていない場合に表示される動的生成のOGP画像
作り方
もし,何らかの方法でGitHubのリポジトリから記事内に画像を貼る仕様があるなら簡単に実装可能です!
画像を取得する
記事内へ画像を貼る/貼らないにしてもGitHub APIを通じてファイル内容を取得する必要があります.以下のソースコードのgitContentPath
がapiへアクセスするためのURLです.今回は.env(.local)
ファイルに以下の値を登録しています.
GIT_USERNAME
=GitHubのユーザー名GIT_REPO
=GitHubでのリポジトリ名
const gitContentPath = `https://api.github.com/repos/${process.env.GIT_USERNAME!}/${process.env.GIT_REPO!}/contents`;
export const getImage = cache(async (path: string) => {
const fileJson = await fetch(`${gitContentPath}${path}`, {
...getHeaders(),
...getNext(3600 * 24 * 30),
})
.then((res) => res.json())
.catch((err) => console.error(err));
const imageJson = await fetch(fileJson.git_url, {
...getHeaders(),
...getNext(3600 * 24),
})
.then((res) => res.json())
.catch((err) => console.error(err));
return imageJson.content as string;
});
ここで,fetch
関数には2つのJSON形式の値を入れています.
export function getHeaders() {
return {
headers: {
Authorization: `token ${process.env.GIT_TOKEN!}`,
'Content-Type': 'application/json',
},
};
}
GIT_TOKEN
=GitHub PATのトークン
このheader
オプションと,
export function getNext(revalidate: number): FetchOptions {
if (revalidate === 0) {
return { cache: 'no-store' };
} else {
return { next: { revalidate } };
}
}
このcache/next
オプションを入れます.特にgetNext
はrevalidate
の指定でキャッシュ期間(再びfetchするまでの時間)を秒数で設定できます.画像そのものが変化することは中々ないので 3600*24*30(秒)=30(日) のキャッシュ期間を設けました.
取得したJSON形式の値からgit_url
を参照します.ここに画像の全容へアクセスできるためのリンクがあります.取得してもう一度fetch
します.すると,JSONのcontent
にBase64エンコーディングの画像が得られます!
これを返り値として次のステップに進みます.
Base64からデコードしてAPIとしてレスポンスする
ここからはBase64エンコーディングで取得する方法ができたので実際にリクエストに対してレスポンスを行います.新しくsrc/app/api/get-thumbnail/route.ts
として作ってみます.
URLパラメータからpath
の値として受け取り,文字列が/GIT_IMAGES_DIR/
から始まるか確認します.
GIT_IMAGES_DIR
=リポジトリの画像を格納するディレクトリ
したがって,上述が満たされない場合は400エラーを返します.
import { getImage } from '@/lib/getPosts';
import { getImageMimeType } from '@/lib/mime-getter';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(req: NextRequest) {
const targetPath = new URL(req.url).searchParams.get('path');
if (!targetPath) return NextResponse.json({ message: 'Fatal Error: no "path" query' }, { status: 400 });
// 任意のディレクトリ内かチェック
if (!targetPath.startsWith(`/${process.env.GIT_IMAGES_DIR}/`)) {
return NextResponse.json({ message: 'Fatal Error: don\'t match "path" query' }, { status: 400 });
}
try {
// Base64で取得
const base64Image = await getImage(targetPath);
const mimeType = getImageMimeType(targetPath);
const imageBuffer = Buffer.from(base64Image, 'base64');
return new NextResponse(imageBuffer, {
status: 200,
headers: {
'Content-Type': mimeType,
'Content-Length': imageBuffer.length.toString(),
},
});
} catch (error) {
console.error(error);
return NextResponse.json({ message: 'Error fetching image' }, { status: 500 });
}
}
imageBuffer
にてBase64エンコーディングから画像のバッファを作成します.あとは,Content-Type
をimage/サブタイプ
にして返すだけです.
getImageMimeType
関数は渡されたpathから拡張子を切り取り判別し,対応したMIMEタイプを返します.
export function getImageMimeType(path: string) {
const ext = path.split('.').pop()?.toLowerCase();
switch (ext) {
case 'jpg':
case 'jpeg':
return 'image/jpeg';
case 'png':
return 'image/png';
case 'gif':
return 'image/gif';
case 'webp':
return 'image/webp';
case 'bmp':
return 'image/bmp';
case 'svg':
return 'image/svg+xml';
default:
return 'application/octet-stream';
}
}
他のMIMEタイプは以下より確認してください.
呼び出し方
既に動的OGPを呼んでいた引数の部分に下のように書けば実装完了です.
// dataはYAMLヘッダ部のJSON形式
data.thumbnail
? `/api/get-thumbnail?path=${encodeURIComponent(data.thumbnail)}`
: `/api/ogp-posts/${slug}` /* 本来の動的OGP画像を生成するエンドポイント */
おわりに
今回は外部からはサムネイルを参照しないようにし,自身のリポジトリだけで完結させ問題が起こりにくいようにしています.ブログサイトにサムネイル表示機能があると,カスタムサムネイルがあるだけで全体的な印象が変わるのではないかと思います.
↓ 実際に変更したコミット内容