• TOP
  • ブログ一覧
  • microCMSの投稿からOGPを取得して画像とリンクカードを作る

2025年06月14日

microCMSの投稿からOGPを取得して画像とリンクカードを作る

JamstackWEB制作Next.js
microCMSの投稿からOGPを取得して画像とリンクカードを作る

Webサイトやブログ記事で外部サイトへのリンクを貼る際、単なるURLだけでは情報が伝わりにくく、ユーザーのクリックを促しにくいことがあります。そこで活用したいのがOGP (Open Graph Protocol) です。OGPは、Webページの内容をSNSなどで魅力的に表示させるためのメタデータで、これを利用することで、リンクを画像付きのリンクカードとして表示できるようになります。
この記事では、OGPデータを取得して記事内のリンクを自動的にリッチなリンクカードに変換するJavaScriptコードとその仕組み、そしてその見た目を整えるCSSについて解説します。
この記事ではNext.jsのApp routerを使って下記のようなリンクカードを作る方法をご紹介します!


OGPをリンクカードにすることの効能

OGP(Open Graph Protocol)を使ってブログ記事のリンクをカード形式で表示することには、以下のような効能があります。

1. 視認性・訴求力の向上

リンクカードはタイトル・説明・画像がまとまって表示されるため、テキストリンクよりも目立ち、ユーザーの興味を引きやすくなります。

2. クリック率の向上

画像や説明文があることで、リンク先の内容が直感的に伝わり、クリックされやすくなります。

3. SNSシェア時の最適化

OGP情報が正しく設定されていれば、TwitterやFacebookなどSNSでシェアされた際にも美しいカード形式で表示され、拡散効果が高まります。

4. サイトの信頼性向上

リンクカードはデザイン的にも整って見えるため、サイト全体の信頼感やプロフェッショナル感がアップします。
ブラキオではこのようにヘッドレスCMSから取得した記事をカスタマイズすることも可能

リンクカード自動生成の仕組みを支える2つの主要なコード

今回ご紹介するリンクカードの自動生成は、主に以下の2つのJavaScriptコードと、その見た目を定義するCSSによって実現されます。

  1. getOGPData: 指定されたURLからOGPデータを取得するサーバーサイドの関数
  2. useParseBody: 記事本文からリンクを抽出し、getOGPDataで取得した情報をもとにリンクカードに変換するReactフック
  3. CSS: 生成されたリンクカードの見た目を整えるスタイルシート

getOGPData: OGPデータを取得する

まず、外部サイトのOGP情報を取得するための関数getOGPDataから見ていきましょう。この関数は、指定されたURLにアクセスし、そのページのHTMLからOGP関連のメタデータを抽出します。

'use server'
import * as cheerio from 'cheerio'

export type OGPData = {
  title: string
  description: string
  image: string
}

export async function getOGPData(url: string): Promise<OGPData> {
  try {
    // 1. 指定されたURLからHTMLを取得
    const response = await fetch(url)
    const html = await response.text()
    // 2. cheerioを使ってHTMLをパース
    const $ = cheerio.load(html)

    // 3. メタタグからOGP情報を抽出するヘルパー関数
    const getMetaTag = (name: string) => {
      return (
        $(`meta[name=${name}]`).attr('content') || // 通常のmetaタグ (name属性)
        $(`meta[property="og:${name}"]`).attr('content') || // Open Graphプロパティ
        $(`meta[property="twitter:${name}"]`).attr('content') // Twitter Cardsプロパティ
      )
    }

    // 4. 抽出した情報をOGPDataオブジェクトとして返す
    return {
      title: getMetaTag('title') || 'No title', // タイトル
      description: getMetaTag('description') || 'No description', // 説明
      image: getMetaTag('image') || '' // サムネイル画像
    }
  } catch (error) {
    console.error('Error fetching OGP data:', error)
    throw new Error('Failed to fetch OGP data')
  }
}

コードのポイント

  • 'use server': Next.jsのサーバーコンポーネントまたはサーバーアクションであることを示します。これにより、クライアントサイドではなくサーバーサイドでデータのフェッチが行われ、APIキーなどの機密情報を安全に扱うことができます。
  • cheerio: Node.jsでHTMLをパースし、jQueryのようなシンタックスでDOM操作を可能にするライブラリです。cheerio.load(html)で取得したHTMLを読み込み、DOM要素にアクセスできるようになります。
  • fetch(url): 指定されたURLにHTTPリクエストを送信し、ページのHTMLコンテンツを取得します。
  • getMetaTag関数:
    • OGP情報は<meta>タグに記述されています。このヘルパー関数は、name属性、property="og:"property="twitter:"のいずれかで指定されたメタタグのcontent属性値を取得しようとします。
    • これは、サイトによってOGP情報の記述方法が異なる場合があるため、汎用的に情報を取得できるようにするための工夫です。
  • 返り値: 取得したtitledescriptionimageの各情報をOGPData型として返します。情報が見つからなかった場合はデフォルト値が設定されます。

useParseBody: 記事本文のリンクをリンクカードに変換する

次に、実際の記事本文を解析し、OGPデータを利用してリンクカードを生成するReactフックuseParseBodyを見ていきましょう。

import { useEffect, useState } from 'react'
import * as cheerio from 'cheerio'
import { getOGPData, OGPData } from '../actions/getOGPDataActions'

const urlPattern = /^(https?:\\\\/\\\\/[^\\\\s$.?#].[^\\\\s]*)$/ // URLの正規表現パターン

export function useParseBody(body: string) {
  const [parsedBody, setParsedBody] = useState<string>(body || '') // パース後のHTMLを保持

  useEffect(() => {
    const parseAndFetchOGP = async () => {
      const $ = cheerio.load(body || '') // 記事本文をcheerioで読み込み
      const links: string[] = [] // 処理対象となるリンクを格納する配列

      // リンクを収集する関数
      const collectLinks = () => {
        $('p > a').each((_, link) => { // <p>タグの子要素の<a>タグを走査
          if ($(link).find('img').length === 0) { // <img>タグを含まないリンクのみを対象
            const href = $(link).attr('href')
            const linkText = $(link).text()

            // hrefが存在し、かつリンクのテキストがURLパターンに一致する場合
            if (href && urlPattern.test(linkText)) {
              links.push(href) // リンクを収集リストに追加
            }
          }
        })
      }

      // OGPデータを取得する関数
      const fetchOGPData = async () => {
        // 収集した各リンクに対してgetOGPDataを並行して実行
        const ogpDataPromises = links.map((href) => getOGPData(href))
        return await Promise.all(ogpDataPromises) // 全てのOGPデータ取得を待つ
      }

      // リンクカードを生成する関数
      const generateLinkCards = (hrefToOgpData: Map<string, OGPData>) => {
        $('p > a').each((_, link) => {
          if ($(link).find('img').length === 0) {
            const href = $(link).attr('href')
            const linkText = $(link).text()

            if (!href || href.startsWith('#')) return // リンクがない、またはページ内リンクの場合はスキップ

            const meta: OGPData | undefined = hrefToOgpData.get(href) // 対応するOGPデータを取得
            if (!meta || !urlPattern.test(linkText)) return // OGPデータがない、またはテキストがURLでない場合はスキップ

            // リンクカードのHTML構造を生成
            const linkCardHTML = `
  <div class="link-card">
    <a href="${href}" target="_blank" rel="noopener noreferrer">
      <div class="link-card-body">
        <div class="link-card-info">
          <div class="link-card-head">
            <div class="link-card-title">${meta.title}</div>
            <div class="link-card-description">${meta.description}</div>
          </div>
          <div class="link-card-url">${href}</div>
        </div>
        ${meta.image ? `<div class="link-card-thumbnail"><img src="${meta.image}" alt="thumbnail" /></div>` : ''}
      </div>
    </a>
  </div>
`
            $(link).replaceWith(linkCardHTML) // 元のリンクを生成したリンクカードHTMLに置き換える
          }
        })
      }

      // 実行フロー
      collectLinks() // 記事内のリンクを収集
      const ogpDataResults = await fetchOGPData() // 収集したリンクのOGPデータを一括取得

      // リンクのURLとOGPデータをマッピング
      const hrefToOgpData = new Map<string, OGPData>()
      links.forEach((href, index) => {
        hrefToOgpData.set(href, ogpDataResults[index])
      })

      generateLinkCards(hrefToOgpData) // OGPデータを使ってリンクカードを生成

      setParsedBody($.html()) // 変換後のHTMLを状態として設定
    }

    parseAndFetchOGP() // useEffectの初回マウント時とbodyが変更された時に実行
  }, [body])

  return { parsedBody } // パース済みのHTMLを返す
}

コードのポイント

  • useEffect: bodyプロパティが変更されたときに、記事のパースとOGPデータのフェッチ処理を実行します。
  • useState: パースおよびOGPデータ適用後のHTMLコンテンツをparsedBodyとして保持します。
  • cheerio.load(body): 渡された記事のHTML文字列をCheerioで解析し、DOM操作を可能にします。
  • collectLinks関数:
    • 記事中の<p>タグの下にある<a>タグを走査します。
    • 特に、画像を含まない(つまり、テキストリンクである)<a>タグのみを対象とし、そのhref属性とリンクテキストがURLパターンに一致するかを確認します。これにより、単なるテキストとしてのURLがリンクカードの対象となります。
  • fetchOGPData関数:
    • Promise.allを使って、収集した複数のリンクに対してgetOGPData関数を並行して実行します。これにより、OGPデータの取得処理が高速化されます。
  • generateLinkCards関数:
    • OGPデータが取得できたリンクに対して、事前に定義されたHTMLテンプレートにOGPのタイトル、説明、画像を埋め込み、リッチなリンクカードのHTMLを生成します。
    • 元の<a>タグをこの生成されたリンクカードのHTMLに置き換えます。
  • urlPattern: リンクテキストが有効なURL形式であるかをチェックするための正規表現です。
  • parsedBodyの更新: 全ての処理が完了した後、最終的に変換されたHTML文字列がsetParsedBodyによって状態として更新され、このフックを利用するコンポーネントで利用できるようになります。

CSS: リンクカードの見た目を整える

最後に、生成されたリンクカードを美しく表示するためのCSSを見ていきましょう。このCSSは、リンクカード全体のレイアウト、画像(サムネイル)、テキストの表示方法などを定義しています。

:global(.link-card) {
    display: block; // ブロック要素として表示
    height: fit-content; // コンテンツに合わせて高さを調整
    margin: 0 0 1.5rem; // 下に余白
    overflow: hidden; // はみ出たコンテンツを隠す
    background-color: white; // 背景色
    border:
      1px solid theme.$color-border-default, // 枠線
      rgb(46 37 3 / 13%);
    border-radius: theme.$radius-sm; // 角の丸み

    p {
      margin: 0; // 内部のpタグの余白をリセット
    }

    a {
      display: block;
      height: 100%;
      text-decoration: none; // デフォルトのアンダーラインを削除

      &:hover {
        text-decoration: none;

        :global(.link-card-title) {
          color: theme.$color-text-brand; // ホバー時にタイトル色を変更
        }

        :global(.link-card-thumbnail) {
          img {
            transform: scale(1.2); // ホバー時に画像を拡大
          }
        }
      }
    }
  }

  :global(.link-card-body) {
    display: flex; // flexboxで内部要素を配置
    gap: 1.5rem; // 要素間の隙間
    height: 100%;
  }

  :global(.link-card-thumbnail) {
    display: flex;
    flex-shrink: 0; // 縮小しない
    align-items: center; // 垂直方向中央揃え
    justify-content: center; // 水平方向中央揃え
    width: auto;
    height: 128px; // 高さ固定
    aspect-ratio: 1 / 1; // アスペクト比1:1
    overflow: hidden;

    @include theme.breakpoints(md) { // ミディアム以上の画面サイズの場合
      height: 150px;
      aspect-ratio: 1.91 / 1; // アスペクト比を変更
    }

    img {
      width: 100%;
      height: auto;
      object-fit: cover; // 領域に合わせて画像を切り取る
      transition: transform 0.2s ease; // ホバー時のアニメーション
    }
  }

  :global(.link-card-info) {
    display: flex;
    flex-direction: column; // 垂直方向に要素を配置
    flex-grow: 1; // 可能な限り幅を広げる
    justify-content: space-between; // 要素間のスペースを均等にする
    padding: 1.5rem; // 内側の余白
  }

  :global(.link-card-title) {
    @include theme.font-size(14); // フォントサイズ

    display: -webkit-box; // 複数行のテキストを制御
    width: 100%;
    max-height: 1.55em; // 最大高さ
    margin: 0 0 0.5rem;
    overflow: hidden;
    color: theme.$color-text-default;
    -webkit-line-clamp: 1; // 1行に制限
    -webkit-box-orient: vertical;
  }

  :global(.link-card-description) {
    @include theme.font-size(12);

    display: -webkit-box;
    width: 100%;
    margin: 0 0 0.5rem;
    overflow: hidden;
    color: theme.$color-text-subtlest;
    -webkit-line-clamp: 2; // 2行に制限
    -webkit-box-orient: vertical;
  }

  :global(.link-card-url) {
    @include theme.font-size(12);

    display: -webkit-box;
    width: 100%;
    margin: 0;
    overflow: hidden;
    color: theme.$color-text-subtle;
    -webkit-line-clamp: 1; // 1行に制限
    -webkit-box-orient: vertical;
  }

CSSのポイント

  • :global(): CSSモジュールやScoped CSSを使用している場合でも、グローバルなスタイルを適用するために使用されます。
  • .link-card: リンクカード全体のスタイル(背景色、枠線、角丸、余白など)を定義します。
  • ホバー時のエフェクト: リンクカードにカーソルを合わせた際、タイトル色が変わったり、サムネイル画像が拡大するアニメーションが設定されており、ユーザーエクスペリエンスを高めます。
  • .link-card-body: カードのメインコンテンツ部分をFlexboxでレイアウトし、情報とサムネイル画像を横並びに配置します。
  • .link-card-thumbnail: サムネイル画像のサイズ、アスペクト比、画像の表示方法(object-fit: cover)を調整します。レスポンシブ対応として、メディアクエリ (@include theme.breakpoints(md)) でブレークポイントごとに異なるアスペクト比を設定しています。
  • .link-card-info: タイトル、説明、URLのテキスト情報を格納する部分のレイアウトを定義します。
  • .link-card-title, .link-card-description, .link-card-url: 各テキスト要素のフォントサイズ、色、および**webkit-line-clamp**プロパティを使って表示行数を制限し、テキストがはみ出ないように制御しています。これにより、デザインが崩れるのを防ぎ、見た目をきれいに保ちます。

まとめ

この記事で紹介したgetOGPDatauseParseBody、そして関連するCSSを組み合わせることで、手軽に記事内のURLをリッチなOGPリンクカードに自動変換することができます。これにより、ブログ記事やWebサイトの視認性が向上し、ユーザーが外部サイトのコンテンツを理解しやすくなり、クリック率の向上にも繋がるでしょう。
ご自身のサイトにこの機能を組み込んで、より魅力的なコンテンツ表示を実現してみてはいかがでしょうか。

この記事をシェアする

お気軽にご相談ください

会社のホームページが欲しい!名刺がわりになるコーポレートサイトが欲しい!などお気軽にご相談ください。
またパートナー企業をお探しの制作会社様や、フロントエンド開発もご連絡お待ちしております。

  • GitHub
  • X
  • Instagram
  • zenn

最近の投稿

  • TOP
  • ブログ一覧
  • microCMSの投稿からOGPを取得して画像とリンクカードを作る

ホームページ制作なら
ブラキオにお任せください

フロントエンドエンジニアとして10年のキャリアを活かし
高セキュリティ、高パフォーマンスのホームページを制作いたします。
新しくホームページが欲しい、すでにあるホームページをリニューアルしたい!
ホームページのことでお悩みならブラキオにお任せください。