アイコン
スキル

【OGP】Next.js + microCMSを使って、リンクカードを作ってみた

アイキャッチ

こんにちは、さかです。

この記事では、Next.jsとmicroCMSでOGP 情報を使い、リンクカードを作成する方法を解説します。

はじめに

SNSが盛んに利用されている近年では、OGPはとても重要です。

OGPを設定することで、TwitterやFacebookなどのSNSでリンクがシェアされた際に、タイトル、画像、説明文が意図した通りに表示され、ユーザーの目を引くことができます。

webページにおいても、OGP情報を使用して、リンクカードにすることで、リンク先の情報を一目で把握でき、ユーザーの視認性を高める効果があります。

まだ自分のサイトではOGP情報を使用したリンクカード生成をしていなかったため、今回新たにリンクカードに対応するように改修をしました。

OGPとは

OGP(Open Graph Protocol)とは、ウェブページの情報をSNSに適切に伝えるための仕組みです。ウェブサイトのURLをSNSに貼り付けると、そのページのタイトルや説明文、画像が自動的に表示されます。これがOGPの基本的な機能です。

以下は、OGP タグの例です。

index.html
<meta property="og:title" content="ページのタイトル">
<meta property="og:description" content="ページの説明文">
<meta property="og:image" content="画像のURL">
<meta property="og:url" content="ページのURL">

OGPが重要な理由

OGPが重要とされる主な理由は以下の通りです。

  • クリック率の向上:魅力的な画像やタイトルが表示されることで、ユーザーがリンクをクリックする可能性が高まります。
  • ブランディング:サイトのイメージに合った情報が表示されることで、ブランドイメージを統一できます。
  • 情報の正確性:意図しない情報が表示されることを防ぎ、正確な情報をユーザーに届けられます。

作成するリンクカードの全体像

今回はNext.jsで、以下のOGP情報を使用したリンクカードを実装していきます。

  • 外部サイトのOGP情報を取得する。
  • microCMSのリッチエディタに設定した「link-card」カスタムクラスを使って、自動的にリンクカードを生成する。
  • ドメイン判定による、表示方法の最適化。
  • 取得したOGP情報をカード形式で表示する。

技術スタック

今回の実装で使用する主な技術は以下の通りです。

  • Next.js 15.0.3
  • React
  • typescript
  • open-graph-scraper
  • html-react-parser
  • microCMS
  • tailwindcss

microCMSリッチエディタの設定

microCMSのリッチエディタでは、コンテンツにカスタムクラス「link-card」を設定することで、特定のテキストをリンクカードとして表示させ、その部分がLinkCardコンポーネントに置き換わるようにします。

リッチエディタにカスタムclassを追加する

まず、画面右上にあるAPI設定をクリックし、APIスキーマ > リッチエディタに遷移します。
その後、リッチエディタが適用されている箇所の詳細設定を選択します。

その後、サイドバナーが出現し、カスタムclassの設定までスクロールし、リンクカードのカスタムクラスを追加します。

リッチエディタにカスタムクラスを付与する

保存したら、リッチエディタの編集画面に移動し、カスタムを選択すると、先ほど追加したリンクカードが追加されています。

リンクカードにしたいテキストURLをマーカーし、カスタムクラスの「link-card」を付与することができます。

このように設定することで、リンクを解析し、LinkCardコンポーネントに置き換わることができます。

OGP取得機能の実装とリンクカード表示

まず、外部のウェブサイトのOGP情報を取得する機能を実装します。

これには、open-graph-scraperという外部サイトのOGP情報を簡単に取得できるライブラリが便利です。

Server Actionsを使用する

open-graph-scraperはServer Actionsで実行します。
Server Actionsを使用することで、クライアントサイド(ブラウザ)から直接外部サイトにアクセスしようとした際に発生するCORS(Cross-Origin Resource Sharing) の問題を回避できます。

以下は、Server Actionsを使用して、OGP情報を取得する関数を実装します。

app/_action/getOgp.ts
'use server';

import ogs from 'open-graph-scraper';

interface OgpResult {
  title: string;
  image: string;
  domain: string;
  url: string;
  favicon: string;
}

export async function getOgp(url: string): Promise<OgpResult> {
  const domain = new URL(url).hostname; // URLからドメイン名を取得
  const defaultFavicon = new URL('/favicon.ico', url).toString(); // デフォルトのファビコンURL

  try {
    const { result } = await ogs({ url }); // open-graph-scraperでOGP情報を取得

    // 取得した情報からタイトル、画像、ファビコンを抽出
    // 型アサーションを使って型を明示的に指定

    const title = (result.ogTitle as string) ?? '';
    const image = (result.ogImage && result.ogImage[0]?.url as string) ?? ''; // 画像は配列で返ってくることがあるので注意

    const favicon = (result.favicon as string) ?? defaultFavicon; // ファビコンがなければデフォルト画像を適用する
    return { title, image, domain, url, favicon };
  } catch (error) {
    // エラーが発生した場合も、最低限の情報を返す
    console.error(`OGP取得エラー for ${url}:`, error);
    return { title: '', image: '', domain, url, favicon: defaultFavicon };
  }
}

use serverでは、このファイル内の関数がサーバーサイドでのみ実行されることを示し、ogs({ url })で、open-graph-scraperライブラリを使用して、指定されたURLのOGP情報を取得します。

result.ogTitleなどは、取得したOGP情報からタイトル、画像、ファビコンのURLを取り出す処理です。?? '' は、情報が取得できなかった場合に空文字列を返します。
エラーハンドリング は、OGP情報の取得に失敗した場合でも、エラーを捕捉して最低限の情報を返すようにしています。

LinkCardコンポーネントの実装とmicroCMS連携

OGP情報を表示するためのLinkCardコンポーネントと、microCMSのリッチエディタと連携する部分を実装します。

まずは、OGP情報を表示するLinkCardコンポーネントを実装します。

LinkCard.tsx
import Link from 'next/link';
import Image from 'next/image';
import { getOgp } from '@/app/_action/getOgp'; // サーバーアクションをインポート

interface LinkCardProps {
  url: string;
}

const LinkCard = async ({ url }: LinkCardProps) => {
  // サーバーアクションを呼び出してOGP情報を取得
  const ogp = await getOgp(url);
  const serverDomain = process.env.SERVER_DOMAIN; // 環境変数から自分のサイトのドメインを取得
  let isDomainFlag = false; // 自分のサイト内のリンクかどうかを判定するフラグ

  // --- 同ドメイン判定ロジック ---
  if (serverDomain) {
    // www. の有無を無視してドメインを比較するため、正規化します
    const normalizedDomain = ogp.domain.replace(/^www\./, '');
    const normalizedServerDomain = new URL(serverDomain).hostname.replace(/^www\./, '');

    if (normalizedDomain === normalizedServerDomain) {
      // 自分のサイト内のリンクの場合、パスだけにする
      const parsed = new URL(ogp.url);
      ogp.url = parsed.pathname + parsed.search + parsed.hash;
      isDomainFlag = true;
    }
  }

  return (
   // Linkコンポーネントでリンク全体を囲む
   <Link
     href={ogp.url}
     target={isDomainFlag ? '_self' : '_blank'} // 自分のサイトなら同じタブで、外部サイトなら新しいタブで開く
     rel={isDomainFlag ? '' : 'noopener noreferrer'} // セキュリティ対策
     className="block border border-gray-200 rounded-lg overflow-hidden shadow-md hover:shadow-lg transition-shadow duration-300 mb-4"
   >
     <div className="flex items-center p-4">
       {ogp.image ? (
         <div className="w-24 h-24 mr-4 flex-shrink-0">
           <Image
             src={ogp.image}
             alt={ogp.title || 'OGP Image'}
             width={96}
             height={96}
             className="object-cover rounded-md"
           />
         </div>
       ) : (
         <div className="w-24 h-24 mr-4 flex-shrink-0 bg-gray-100 flex items-center justify-    center rounded-md text-gray-500">
           {/* 画像がない場合のプレースホルダー */}
           No Image
         </div>
       )}
       <div className="flex-grow">
         <h3 className="text-lg font-semibold text-gray-800 line-clamp-2">
           {ogp.title || ogp.url}
         </h3>
         <p className="text-sm text-gray-600 line-clamp-1 mt-1">
           {ogp.domain}
         </p>
       </div>
     </div>
   </Link>
  );
};

export default LinkCard;

await getOgp(url)で先ほど作成したサーバーアクションを呼び出して、非同期でOGP情報を取得します。
また、process.env.SERVER_DOMAINで環境変数から自分のサイトのドメインを取得します。
Next.jsでは、.envファイルに設定した環境変数をprocess.env.変数名で参照できます。ここに自分のサイトのドメインを設定しておきます。

同ドメイン判定ロジックについては、自分のサイト内のリンク(同ドメイン)であれば、ogp.urlを相対パス(/aboutのような形式)に変換しています。これにより、Next.jsのLinkコンポーネントが内部リンクとして最適に扱えるようになります。

isDomainFlagを使用して、自分のサイト内のリンクであればtarget="_self"(同じタブで開く)、外部サイトへのリンクであればtarget="_blank"(新しいタブで開く)を設定しています。

rel="noopener noreferrer"は、target="_blank"を使用する際のセキュリティ対策です。
Linkコンポーネントの中に、OGPで取得した画像、タイトル、ドメイン名を表示するUIをtailwindcssで作成しています。また、Imageコンポーネントを使用することで、画像の最適化が行われます。

ArticleRichEditorコンポーネントでparse処理する

html-react-parserというライブラリで、カスタムクラスを認識するロジックを`ArticleRichEditor`コンポーネントに組み込みます。

ArticleRichEditor.tsx
// features/article/components/ArticleRichEditor/ArticleRichEditor.tsx
import parse, { DOMNode, Element } from 'html-react-parser';
import React from 'react';
import LinkCard from '../LinkCard/LinkCard'; // 作成したLinkCardコンポーネントをインポート

type ArticleRichEditorProps = {
  richEditor: string; // microCMSから取得したHTML文字列
};

const ArticleRichEditor = async ({
  richEditor,
}: ArticleRichEditorProps): Promise<React.ReactElement> => {
  // microCMSのリッチエディタのカスタムクラス「link-card」に対応するための正規化処理
  // span.link-card を含む様々なパターンを a.link-card に正規化します。
  // これにより、後続のparse処理でaタグとして認識しやすくなります。

  const normalizedHtml = richEditor
   // <p><span class="link-card">URL</span></p> パターンを a.link-card に変換
   .replace(
     /<p[^>]*>\s*<span class="link-card">([^<]+)<\/span>\s*<\/p>/g,
     '<a href="$1" class="link-card">$1</a>',
   )  

   // <span class="link-card">URL</span> 単体パターンを a.link-card に変換
   .replace(
     /<span class="link-card">([^<]+)<\/span>/g,
     '<a href="$1" class="link-card">$1</a>',
   )

   // 既存のaタグ内に span.link-card があるパターンも a.link-card に変換
   .replace(
     /<a([^>]*)href="([^"]+)"([^>]*)>\s*<span class="link-card">(.*?)<\/span>\s*<\/a>/g,
     '<a href="$2" class="link-card">$4</a>',
   );

   const options = {
       // HTMLの各ノードを解析する際のオプション
     replace: (domNode: DOMNode) => {
       // もし現在のノードがHTML要素(例: <a>タグ)で、
       // かつ、そのタグ名が'a'であり、
       // さらに、class属性に'link-card'が含まれている場合
       if (
         domNode instanceof Element &&
         domNode.name === 'a' &&
         domNode.attribs.class?.split(' ').includes('link-card')
       ) {
         // そのaタグのhref属性からURLを取得
         const href = domNode.attribs.href;
         // 取得したURLを使ってLinkCardコンポーネントをレンダリング
         return <LinkCard url={href} />; 
       }
      // それ以外のノードはデフォルトの処理に任せる(そのままレンダリングする)
    },
  };

  return (
    <div className="aricleContents pb-14">{parse(normalizedHtml, options)}</div>
  );
};

export default ArticleRichEditor;

このコンポーネントは、microCMSのリッチエディタで設定されたカスタムクラス「link-card」を解析し、リンクカードを表示する処理をしています。

normalizedHtmlによる正規化処理によって、microCMSのリッチエディタの出力は、カスタムクラスの適用方法によって<span>タグに包まれたり、<p>タグに包まれたりする可能性があります。normalizedHtmlの処理は、これらの様々なパターンを統一的に<a href="URL" class="link-card">URL</a>という形式に変換し、後続のhtml-react-parserが扱いやすいようにします。

html-react-parserでこのライブラリは、HTML文字列をReactコンポーネントのツリーに変換してくれます。

また、parse関数のoptions内でreplaceプロパティを使うことで、特定のHTML要素を独自のReactコンポーネントに置き換えることができます。

ここでは、「aタグ」かつ「link-cardクラスを持つ」要素を見つけたら、そのhref属性の値をLinkCardコンポーネントに渡し、そのLinkCardコンポーネントをレンダリングするように指示しています。

その後、microCMSから取得した記事本文のHTML文字列をArticleRichEditorコンポーネントに渡すことで、自動的にリンクカードが生成され、他のHTMLコンテンツと一緒に表示されます。

ページに表示させる

最後にpage.tsxに、microCMSから取得したリッチエディタのコンテンツをArticleRichEditorコンポーネントに渡します。

page.tsx
import ArticleRichEditor from '@/features/article/components/ArticleRichEditor/ArticleRichEditor';

export default async function ArticleDetailPage({ params }: { params: { id: string } }) {
  const article = await getArticleById(params.id); // microCMSのAPIから記事データを取得する関数
  if (!article) {
    return <div>記事が見つかりません。</div>;
  }

  return (
    <div>
      <h1>{article.title}</h1>
      {/* microCMSのリッチエディタのコンテンツを渡す */}
      <ArticleRichEditor richEditor={article.body} />
    </div>
  );
}

以下のように、ページにOGP情報が表示されていたら、完了です。

まとめ

この記事では、Next.jsとmicroCMS を使ってOGP 情報を取得し、実装、表示する方法を解説しました。
ぜひ、Next.jsで自分だけのリンクカードを作ってみてください。

参考文献

記事をシェア

  • X-icon
  • facebook-icon
  • line-icon