Published on

Next.js blogで記事ごとにOGP画像を変える

Authors
  • avatar
    Name
    Nomad Dev Life
    Twitter
2024-11-06-ogp-image Unsplashdole777が撮影した写真

OGP画像が変わらない

X等のSNSのポストにリンクを貼ると、その記事のサムネイルが表示されます。この時に表示されるサムネイル画像をOGP画像と呼びます。blog記事にアイキャッチ用の画像を入れたら、それがOGP画像として表示されてほしいところです。

しかし、試しにこのblogの記事のURLをいくつか貼り付けたところ、表示されるOGP画像が常に同じであることに気が付きました😢 OGP画像は、headタグの中のmetaタグで定義されています。どの記事のHTMLも、以下のようになっていました。

<meta property="og:image" content="https://nomad-dev-life.net/static/images/twitter-card.png"/>

そこで、blog記事に画像が入っている場合は、その画像をOGP画像として採用するようにできないか試してみました。

blogのOGP画像を記事ごとに変更する

この記事は、Next.jsのTailwind CSS Blogを前提としています。しかし、基本的な考え方は同じはずです。

今回はコードを見ていきながら、どうしたら記事ごとにOGP画像を設定できるかを探っていきます。最終的な変更内容を先にチェックしたい方は、下の方にpatch形式のファイルがあるので、それを参考にしてください。

コードを見てみる

各blog記事は、app/blog/[...slug]/page.tsxで生成されているようでした。46行目でogImageListを扱っているので、このあたりを見ていけば良さそうです。

ただ、post.imagesが設定されている場合は、その画像がogImageListに設定されるように見えます。実際、この箇所にconsole.logを入れて、yarn devでローカル実行してページを表示させてみると、記事に画像が入っている場合でもpost.imagesは空でした。どうもこのあたりに問題がありそうです。

post.imagescontentlayer.config.tsで定義されています。しかし、このContentlayerのコードを読む限り、post.imagesに値を設定している箇所がありませんでした。

記事ごとにOGP画像を設定できない理由

ここまで調べた結果、記事ごとにOGP画像を設定できない理由は、blog記事のmdxファイルから画像の要素を抜き出していない為であることが分かりました。post.imagesを定義までしているのに、肝心の画像の要素を抜き出していないのは片手落ちな感があります。

そこで、まずmdxファイルの画像の要素を解析する為に、remark-mdx-imagesをプロジェクトに追加しました。

yarn add remark-mdx-images

次に、contentlayer.config.tsremark-mdx-imagesをimportし、computedFieldsの中でimage要素をpost.imagesに追加していくことによって、mdxファイルの画像要素が認識されるようになります。 変更内容は以下のようになっています。

From 2d2f1a5efd7a4c909018debbfd9cf775f6854d98 Mon Sep 17 00:00:00 2001
From: user <user@gmail.com>
Date: Tue, 5 Nov 2024 00:10:17 +0900
Subject: [PATCH] Apply the images in an article to ogp:image

---
 contentlayer.config.ts | 19 +++++++++++++++++++
 package.json           |  1 +
 yarn.lock              | 12 ++++++++++++
 3 files changed, 32 insertions(+)

diff --git a/contentlayer.config.ts b/contentlayer.config.ts
index 63e51dd..0c903e5 100644
--- a/contentlayer.config.ts
+++ b/contentlayer.config.ts
@@ -14,6 +14,10 @@ import {
   remarkImgToJsx,
   extractTocHeadings,
 } from 'pliny/mdx-plugins/index.js'
+import remarkMdxImages from 'remark-mdx-images';
+import { remark } from 'remark';
+import { visit } from 'unist-util-visit';
+
 // Rehype packages
 import rehypeSlug from 'rehype-slug'
 import rehypeAutolinkHeadings from 'rehype-autolink-headings'
@@ -55,8 +59,22 @@ const computedFields: ComputedFields = {
     resolve: (doc) => doc._raw.sourceFilePath,
   },
   toc: { type: 'json', resolve: (doc) => extractTocHeadings(doc.body.raw) },
+  images: { type: 'list', resolve: (doc) => extractImages(doc.body.raw) },
 }
 
+const extractImages = (markdown: string) => {
+  const images: string[] = [];
+  const tree = remark().parse(markdown);
+
+  visit(tree, 'image', (node) => {
+    if (node.url) {
+      images.push(node.url);
+    }
+  });
+
+  return images;
+};
+
 /**
  * Count the occurrences of all tags across blog posts and write to json file
  */
@@ -155,6 +173,7 @@ export default makeSource({
       remarkMath,
       remarkImgToJsx,
       remarkAlert,
+      remarkMdxImages,
     ],
     rehypePlugins: [
       rehypeSlug,
diff --git a/package.json b/package.json
index 14e703e..a8725d2 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
     "remark-gfm": "^4.0.0",
     "remark-github-blockquote-alert": "^1.2.1",
     "remark-math": "^6.0.0",
+    "remark-mdx-images": "^3.0.0",
     "tailwindcss": "^3.4.3",
     "unist-util-visit": "^5.0.0"
   },
diff --git a/yarn.lock b/yarn.lock
index 0dcc50a..a69dca8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10571,6 +10571,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"remark-mdx-images@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "remark-mdx-images@npm:3.0.0"
+  dependencies:
+    "@types/mdast": ^4.0.0
+    unified: ^11.0.0
+    unist-util-visit: ^5.0.0
+  checksum: 774d912ab87d50eff6400aadfc10cb029adf2850356c66f75336bb6cd64f312cda2c67bf553e7cf5230d2708d4ce70bfc9af860f20cb0730bb9c0f74e6bcbeb2
+  languageName: node
+  linkType: hard
+
 "remark-mdx@npm:^3.0.0":
   version: 3.0.1
   resolution: "remark-mdx@npm:3.0.1"
@@ -11423,6 +11434,7 @@ __metadata:
     remark-gfm: ^4.0.0
     remark-github-blockquote-alert: ^1.2.1
     remark-math: ^6.0.0
+    remark-mdx-images: ^3.0.0
     tailwindcss: ^3.4.3
     typescript: ^5.1.3
     unist-util-visit: ^5.0.0
 

記事の最初の画像をOGP画像に設定する

上記までのupdateだと、1つの記事に複数の画像を入れた際、すべての画像が"og:image"としてheadタグの中に定義されます。このように複数の"og:image"が定義されている場合、実際にどの画像をサムネイルとして表示するかはSNS側が決めるようです。

複数のOGP画像が定義されている場合、Xは最後の画像をOGP画像として採用していました。それだとアイキャッチ画像を記事の一番最後に入れなければならないので、app/blog/[...slug]/page.tsxを修正し、記事の最初の画像だけが"og:image"として定義されるようにしました。

From ad455875a4fa008962a293de79af4341cb39b0fe Mon Sep 17 00:00:00 2001
From: user <user@gmail.com>
Date: Tue, 5 Nov 2024 00:23:42 +0900
Subject: [PATCH] When there are multiple images in an aritcle, apply the first
 image as ogp:image

---
 app/blog/[...slug]/page.tsx | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/app/blog/[...slug]/page.tsx b/app/blog/[...slug]/page.tsx
index 1598622..2169359 100644
--- a/app/blog/[...slug]/page.tsx
+++ b/app/blog/[...slug]/page.tsx
@@ -44,6 +44,9 @@ export async function generateMetadata({
   if (post.images) {
     imageList = typeof post.images === 'string' ? [post.images] : post.images
   }
+  if (imageList.length > 1) {
+    imageList = [imageList[0]]  
+  }
   const ogImages = imageList.map((img) => {
     return {
       url: img.includes('http') ? img : siteMetadata.siteUrl + img,

これで、記事の最初にアイキャッチ画像を入れれば、それがOGP画像として採用されるようになります。

おわりに

SNSで記事をシェアすることが多いので、OGP画像を見てユーザーの好奇心をひくのは重要です。もし自分のNext.jsのblogでOGP画像が出ていなければ、本記事を参考にしてください。