1. ホーム
  2. >
  3. Blog
  4. >
  5. Nuxt.jsでリンクカードを簡単に実装する方法(改訂版)
2024年9月19日 2025年6月7日
Nuxt.jsでリンクカードを簡単に実装する方法(改訂版)

Nuxt.jsでリンクカードを簡単に実装する方法(改訂版)

Nuxt.jsを利用して、ブログ記事にリンクカードを埋め込む完全ガイド。open-graph-scraperの導入からLinkCardコンポーネントの作成、nuxt.config.tsとpackage.jsonの設定まで、すべての手順をカバーします。


はじめに

これまでの記事では、Nuxt.jsでTwitterやYouTubeの埋め込み方法を解説しました。今回はそれに加え、多くのブログで必要とされる「リンクカード」の実装方法を掘り下げます。

この記事では、Nuxt.js@nuxt/content を利用して、ブログ記事にリッチなリンクカードを埋め込む手順を網羅的に解説します。リンクカードは、外部リンクを視覚的に分かりやすく読者に提示するための重要なUIコンポーネントです。open-graph-scraper の導入からコンポーネント作成、各種設定ファイルの更新まで、ステップバイステップで説明します。

修正版の概要

Nuxt.js@nuxt/content のバージョンアップに伴い、以前の方法ではビルドや静的サイト生成(generate)時にリンクカードが正しく機能しない問題が確認されました。本記事(改訂版)では、この問題に対応するための最新の実装方法を詳しく解説します。

リンクカード参考サイト

上記はそれぞれZennとAstroでリンクカードを実装する方法を解説した記事です。特に画像取得のロジックは、Astro向けの記事を参考にさせていただきました。

これらの記事ではNuxt 3および@nuxt/content v3環境での具体的な解説がなかったため、本記事でその方法を詳しく説明します。

Nuxtでリンクカードを使う方法

Nuxtにリンクカードを埋め込む

Nuxt.jsと@nuxt/contentを使用してブログサイトに美しいリンクカードを埋め込む方法を紹介します。

上記の参考サイトリンクカードは以下の方法で表示させています

1. open-graph-scraperのインストール

はじめに、リンク先のOGP情報を取得するためのパッケージ open-graph-scraper をインストールします。これにより、指定したURLからタイトル、説明、画像といったメタデータを効率的に取得できます。

pnpmbash
    pnpm add open-graph-scraper

    
yarnbash
    yarn add open-graph-scraper

    
npmbash
    npm install open-graph-scraper

    

また、Nuxt.jsのビルドツールとしてViteを利用するため、@vitejs/plugin-vue がインストールされていない場合は以下のコマンドを実行してインストールしてください。

pnpmbash
    pnpm add @vitejs/plugin-vue

    
yarnbash
    yarn add @vitejs/plugin-vue

    
npmbash
    npm install @vitejs/plugin-vue

    

2. LinkCard.vueコンポーネントの作成

次に、app/components/content/LinkCard.vue ファイルを作成し、以下のコードを貼り付けます。このコンポーネントは、指定されたURLからOGPデータを取得し、それを基にリンクカードを表示します。 コード内の109行目から始まる .txt-limit クラスは、取得したdescription(説明文)が長すぎる場合に、表示を2行に制限するためのCSSです。

LinkCard.vuevue
    <script setup lang="ts">
import { onMounted, ref } from "vue";
const props = defineProps({
  propsUrl: String,
  title: String,
  siteUrl: String,
  description: String,
});

// 環境変数を取得
const isDevRun = import.meta.env.DEV; // nuxt dev の場合に true

// リアクティブなデータとしてogpDataを定義
const ogpData = ref(null);

if (isDevRun) {
  // dev時
  onMounted(async () => {
    try {
      const data = await $fetch(`/api/ogp?url=${encodeURIComponent(props.propsUrl)}`);
      console.log("APIレスポンス:", data);
      ogpData.value = data;
    } catch (error) {
      console.error("Fetch error:", error);
    }
  });
} else {
  // generate時
  const { data, error } = await useFetch(
    `/api/ogp?url=${encodeURIComponent(props.propsUrl)}`
  );
  if (error.value) {
    console.error("Fetch error:", error.value);
  } else {
    ogpData.value = data.value;
  }
}

// 取得文字列の調整-タイトルの文字数を20文字で切り捨てる
const maxLength = 20

const limitedTitle = computed(() => {
  const base = ogpData.value.ogTitle || props.title || ''
  return base.length > maxLength ? base.substr(0, maxLength) + '...' : base
})
</script>

<template>
  <div class="link-card" v-if="ogpData">
    <a :href="propsUrl" target="_blank" rel="noopener">
      <div class="link-card-content">
        <div self-stretch>
          <img :src="ogpData.ogImage?.[0]?.url || '/img/ogp.png'" alt="OG Image"  tb:mt-10 />
        </div>
        <div class="at-sm:flex-grow-2 tb:pl-2">
          <h3 class="m-0 p-0 text-24px">{{ limitedTitle }}</h3>
          <!-- p class="mt-1 mb-2 p-0 underline text-sm">{{ ogpData.ogUrl || props.siteUrl }}</p -->
          <p class="m-0 p-0 txt-limit">{{ ogpData.ogDescription || props.description }}</p>
        </div>
      </div>
    </a>
  </div>
  <div v-else>
    <p>Link Card Loading...</p>
  </div>
</template>
<style scoped lang="scss">
.link-card {
  border: 1px solid #ddd;
  padding: 16px;
  border-radius: 8px;
  transition: box-shadow 0.3s;
  a {
    text-decoration: none;
    &::after {
      content: none;
    }
  }
}
.link-card img {
  display: block;
  max-width: 100%;
  height: auto;
}
.link-card:hover {
  box-shadow: 3px 4px 8px rgb(0 0 0 /0.6);
}
.link-card-content {
  display: grid;
 @screen sm {
    grid-template-rows: 50% 50%;
    gap: 0;
    h3 {
      @apply text-h5_sm line-height-h5;
    }
  }
  @screen tb {
    grid-template-columns: 40% 60%;
    gap: 10px;
    h3 {
      @apply text-h4_sm line-height-h3;
    }
  }
  @screen lg {
    h3 {
      @apply text-1.5rem leading-lg;
    }
  }
  .txt-limit {
    overflow: hidden;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2; /* 任意の行数を指定 */
  }
}
</style>

    

このコンポーネントは、渡されたURLのOGP(Open Graph Protocol)データを非同期で取得し、リンクカードとして描画します。開発サーバー実行時(nuxi dev)と静的サイト生成時(nuxi generate)でデータの取得方法を切り替えるように設計されています。スタイリングにはUnoCSSを使用していますが、お使いの環境に合わせて自由にカスタマイズしてください。

3. server/api/ogp.ts の設置

次に、OGPデータを取得するためのサーバーAPIエンドポイントを作成します。このファイルは以前の記事では触れていませんでしたが、リンクカードを動作させるために不可欠な要素です。

ogp.tsts
    // server/api/ogp.ts
import { defineEventHandler, getQuery, sendError, H3Error } from 'h3'
import ogs from 'open-graph-scraper'

// タイムアウト値を設定 (例: 20秒 = 20000ミリ秒)
// この値は必要に応じて調整してください。
const OGS_REQUEST_TIMEOUT = 20000;

export default defineEventHandler(async (event) => {
 const query = getQuery(event)
 const url = query.url as string

 if (!url) {
   // エラーレスポンスを H3Error を使って返すことで、
   // Nuxt (Nitro) が適切なHTTPステータスコードを設定しやすくなります。
   const error = new H3Error('URL is required');
   error.statusCode = 400; // Bad Request
   return sendError(event, error);
 }

 try {
   console.log(`[OGP API] Fetching OGP for: ${url} with timeout: ${OGS_REQUEST_TIMEOUT}ms`);
   const { result, error: ogsInternalError } = await ogs({
     url,
     timeout: OGS_REQUEST_TIMEOUT, // タイムアウト値をミリ秒で指定
     // open-graph-scraper はリダイレクトに追従する 'followRedirect': true がデフォルトです。
     // 他にも 'retry': 2 (デフォルト)などのオプションがあります。
   });

   // ogs はエラーがあっても例外を投げず、結果オブジェクト内の error フラグや
   // success: false で示すことがあります。
   if (ogsInternalError || !result || !result.success) {
     const errorMessage = result?.ogError || 'Failed to scrape OGP data due to an unknown reason from ogs.';
     console.error(`[OGP API] OGS failed for ${url}:`, errorMessage, result);
     const error = new H3Error(errorMessage);
     error.statusCode = 502; // Bad Gateway (外部サービスからのエラーとして)
     // クライアント側で詳細なエラー情報が必要な場合は、result を data に含めることもできます
     // error.data = { ogsResult: result };
     return sendError(event, error);
   }

   // console.log('[OGP API] OGP情報:', result);
   return result; // 成功時は ogs の result オブジェクトを返す

 } catch (error: any) { // try-catch は予期せぬ例外(ライブラリのバグなど)を捕捉
   console.error(`[OGP API] Unhandled exception for ${url}:`, error);
   let errorMessage = 'An unexpected error occurred while fetching OGP data.';
   let statusCode = 500; // Internal Server Error

   // タイムアウト関連のエラーかどうかを判定
   // (error.name や error.message はエラーの種類によって変わるため、複数のキーワードでチェック)
   if (error.name === 'AbortError' || // Node.js の AbortController や undici の fetch でタイムアウト時に発生
       (error.message && error.message.toLowerCase().includes('timeout')) ||
       (error.code && error.code === 'ETIMEDOUT')) { // Node.js の net モジュールなどで発生するタイムアウト
     errorMessage = 'The OGP request timed out while fetching from the external site.';
     statusCode = 504; // Gateway Timeout
   }

   const h3Error = new H3Error(errorMessage);
   h3Error.statusCode = statusCode;
   // error.cause や error.stack など、デバッグに役立つ情報をdataに含めることも検討
   // h3Error.data = { originalError: error.toString(), stack: error.stack };
   return sendError(event, h3Error);
 }
})

    

処理する記事の数が多い場合や、取得先のサイトの応答が遅い場合に、OGPデータの取得に失敗することがあります。これを防ぐため、コード内でタイムアウト値を設定し、安定したデータ取得を目指します。

4. nuxt.config.tsの設定

nuxt.config.ts ファイルに以下の設定を追加します。 以前の記事ではvite.pluginsに設定を記述していましたが、現在の構成ではnitro.rollupConfig内に設定を移すことが推奨されます。

nuxt.config.tsts
    import vue from '@vitejs/plugin-vue'

export default defineNuxtConfig({
  // ... 他の設定

  nitro: {
    rollupConfig: {
      plugins: [Vue({
        template: {
          customElement: true,
          },
      }),

    

さらに、experimental オプションの追加も重要です。この設定を有効にすることで、静的サイト生成(SSG)時に各ページのAPIレスポンス(この場合はOGPデータ)が _payload.json という静的ファイルとして書き出され、クライアントサイドでの再取得が不要になります。

nuxt.config.tsts
      experimental: {
    payloadExtraction: true,
    renderJsonPayloads: true,
}

    

以上の手順で、Nuxt.jsプロジェクトにリンクカードを埋め込む準備が整いました。LinkCard コンポーネントを使用することで、美しいリンクカードをブログ記事に簡単に追加できます。

使用例:

  • mdファイルに埋め込む場合
Inline Componentsmd
    ::link-card{propsUrl="https://exanple.com"}
::

或いは、Block Components形式で、
::link-card
---
propsUrl: "https://exanple.com"
---
::

    
  • vueファイルに埋め込む場合
vue
    <template>
  <LinkCard
    propsUrl="https://example.com"
  />
</template>

    

このコンポーネントを使用することで、指定したURLのOGPデータを自動的に取得し、スタイリングされたリンクカードとして表示します。

5.環境変数の設定

最後に、環境変数の設定を見直します。以前は .env ファイルでVITE_APP_ENVを設定する方法を案内していましたが、現在のNuxtのバージョンではビルドプロセスで変数が正しく渡されないケースがあります。より確実な方法として、package.jsonのスクリプト内で直接変数を指定することを推奨します。

package.jsonbash
        "build": "VITE_APP_ENV=production nuxi build",
    "generate": "VITE_APP_ENV=production nuxi generate",

    

まとめ

本記事では、Nuxt.jsと@nuxt/content環境で、OGP情報を活用したリッチなリンクカードを実装する一連の手順を解説しました。サーバーAPIの設置、コンポーネントの作成、そしてビルド設定の最適化により、開発時も静的生成時も安定して動作するリンクカードが実現できます。