
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からタイトル、説明、画像といったメタデータを効率的に取得できます。
pnpm add open-graph-scraper
また、Nuxt.jsのビルドツールとしてViteを利用するため、@vitejs/plugin-vue がインストールされていない場合は以下のコマンドを実行してインストールしてください。
pnpm add @vitejs/plugin-vue
2. LinkCard.vueコンポーネントの作成
次に、app/components/content/LinkCard.vue ファイルを作成し、以下のコードを貼り付けます。このコンポーネントは、指定されたURLからOGPデータを取得し、それを基にリンクカードを表示します。 コード内の109行目から始まる .txt-limit クラスは、取得したdescription(説明文)が長すぎる場合に、表示を2行に制限するためのCSSです。
<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エンドポイントを作成します。このファイルは以前の記事では触れていませんでしたが、リンクカードを動作させるために不可欠な要素です。
// 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内に設定を移すことが推奨されます。
import vue from '@vitejs/plugin-vue'
export default defineNuxtConfig({
// ... 他の設定
nitro: {
rollupConfig: {
plugins: [Vue({
template: {
customElement: true,
},
}),
さらに、experimental オプションの追加も重要です。この設定を有効にすることで、静的サイト生成(SSG)時に各ページのAPIレスポンス(この場合はOGPデータ)が _payload.json という静的ファイルとして書き出され、クライアントサイドでの再取得が不要になります。
experimental: {
payloadExtraction: true,
renderJsonPayloads: true,
}
以上の手順で、Nuxt.jsプロジェクトにリンクカードを埋め込む準備が整いました。LinkCard コンポーネントを使用することで、美しいリンクカードをブログ記事に簡単に追加できます。
使用例:
- mdファイルに埋め込む場合
::link-card{propsUrl="https://exanple.com"}
::
或いは、Block Components形式で、
::link-card
---
propsUrl: "https://exanple.com"
---
::
- vueファイルに埋め込む場合
<template>
<LinkCard
propsUrl="https://example.com"
/>
</template>
このコンポーネントを使用することで、指定したURLのOGPデータを自動的に取得し、スタイリングされたリンクカードとして表示します。
5.環境変数の設定
最後に、環境変数の設定を見直します。以前は .env ファイルでVITE_APP_ENVを設定する方法を案内していましたが、現在のNuxtのバージョンではビルドプロセスで変数が正しく渡されないケースがあります。より確実な方法として、package.jsonのスクリプト内で直接変数を指定することを推奨します。
"build": "VITE_APP_ENV=production nuxi build",
"generate": "VITE_APP_ENV=production nuxi generate",
まとめ
本記事では、Nuxt.jsと@nuxt/content環境で、OGP情報を活用したリッチなリンクカードを実装する一連の手順を解説しました。サーバーAPIの設置、コンポーネントの作成、そしてビルド設定の最適化により、開発時も静的生成時も安定して動作するリンクカードが実現できます。