• #nuxt
  • #vue
  • #composable
  • #ssg
  • #debugging
  • #todo
開発完了

useOgMetaヘルパーのビルドエラー調査

概要

OGP画像署名生成の重複コードを削減するためuseOgMetaヘルパーコンポーザブルを作成したが、SSGビルド時に500エラーが発生した。

背景

現状のパターン(動作する)

各Vueページで以下のパターンを使用:

// about.vue など
const { data: ogImageUrl } = await useAsyncData(
  'og-about',
  () => {
    if (!import.meta.server) return null
    const { generatePageOgImageUrl } = usePageOgSignature()
    return generatePageOgImageUrl({
      type: 'general',
      id: 'about',
      title: 'About'
    })
  },
  { server: true, lazy: false }
)

useHead({
  meta: computed(() => ogImageUrl.value ? [
    { property: 'og:image', content: ogImageUrl.value },
    { property: 'og:image:width', content: '1200' },
    { property: 'og:image:height', content: '630' },
    { name: 'twitter:card', content: 'summary_large_image' },
    { name: 'twitter:image', content: ogImageUrl.value },
  ] : [])
})

このパターンは44ファイルで重複している。

作成したヘルパー(ビルドエラー)

// useOgMeta.ts
export async function useOgMeta(params: OgMetaParams) {
  const { generatePageOgImageUrl } = usePageOgSignature()

  const { data: ogImageUrl } = await useAsyncData(
    `og-${params.type}-${params.id}`,
    () => {
      if (!import.meta.server) return null
      if (params.type === 'jleague') {
        return generatePageOgImageUrl({
          type: 'jleague',
          id: params.id,
          clubName: params.clubName,
          clubColor: params.clubColor
        })
      } else {
        return generatePageOgImageUrl({
          type: params.type,
          id: params.id,
          title: params.title
        })
      }
    },
    { server: true, lazy: false }
  )

  useHead({
    meta: computed(() => ogImageUrl.value ? [
      { property: 'og:image', content: ogImageUrl.value },
      { property: 'og:image:width', content: '1200' },
      { property: 'og:image:height', content: '630' },
      { name: 'twitter:card', content: 'summary_large_image' },
      { name: 'twitter:image', content: ogImageUrl.value },
    ] : [])
  })

  return { ogImageUrl }
}

使用方法:

await useOgMeta({ type: 'general', id: 'home', title: 'log.eurekapu.com' })

エラー症状

ビルドログ

[nitro]   ├─ /financial-quiz/semiconductor-revenue (566ms)
  │ ├── [500] Server Error
[nitro]   ├─ /blog/data-value-breakthrough-analysis (513ms)
  │ ├── [500] Server Error
[nitro]   ├─ /financial-data/nvidia-financial-chart (488ms)
  │ ├── [500] Server Error
...

ERROR  [Page OG Signature] OG_SECRET is not set. Returning empty signature. (repeated 11 times)
ERROR  Exiting due to prerender errors.

観察された事実

  1. useOgMetaを使用しているページでのみ500エラーが発生
  2. 旧パターン(直接useAsyncData)を使用しているページは正常にビルド
  3. OG_SECRET未設定の警告は出るが、これは直接の原因ではない(旧パターンでも同じ警告が出るが500エラーにはならない)

試した修正

修正1: コンポーザブル呼び出しをコールバック外に移動

export async function useOgMeta(params: OgMetaParams) {
  // コールバック外で呼び出し
  const { generatePageOgImageUrl } = usePageOgSignature()

  const { data: ogImageUrl } = await useAsyncData(
    `og-${params.type}-${params.id}`,
    () => {
      if (!import.meta.server) return null
      // generatePageOgImageUrlを使用
      ...
    },
    { server: true, lazy: false }
  )
  ...
}

結果: 同じエラーが発生

修正2: コールバック内でコンポーザブル呼び出し(旧パターンと同じ)

const { data: ogImageUrl } = await useAsyncData(
  `og-${params.type}-${params.id}`,
  () => {
    if (!import.meta.server) return null
    const { generatePageOgImageUrl } = usePageOgSignature()
    // ...
  },
  { server: true, lazy: false }
)

結果: 同じエラーが発生

考えられる原因

1. Nuxtコンポーザブルのコンテキスト問題

Nuxtのコンポーザブル(useAsyncDatauseRuntimeConfigなど)は特定のコンテキストで呼び出す必要がある。

  • 直接<script setup>で呼び出す: 正常動作
  • async関数内で呼び出す: 問題が発生する可能性

useOgMetaasync functionとして定義されており、その内部でuseAsyncDataを呼び出している。この構造がSSGプリレンダリング時に問題を引き起こす可能性がある。

2. useRuntimeConfigの呼び出しタイミング

usePageOgSignature内でuseRuntimeConfig()が呼び出される:

export function usePageOgSignature() {
  const config = useRuntimeConfig() // ここ
  ...
}

SSGビルド時のプリレンダリングコンテキストで、この呼び出しが正しく動作していない可能性がある。

3. 非同期コンポーザブルの制限

Nuxtドキュメントによると、コンポーザブルは同期的なセットアップコンテキストで呼び出す必要がある。async function内での呼び出しはコンテキストが失われる可能性がある。

未解決の疑問

  1. なぜ旧パターンは動作するのか?
    • 旧パターンもuseAsyncDataのコールバック内でusePageOgSignature()を呼び出している
    • 違いは「直接<script setup>で実行」vs「async関数経由で実行」
  2. どのコンポーザブルが問題なのか?
    • useAsyncData?
    • useRuntimeConfig?
    • useHead?
  3. エラーの詳細は?
    • 500エラーの具体的なスタックトレースが見えない
    • OG_SECRET警告は赤字だが、実際のエラー原因ではない

今後の調査方向

1. 開発サーバーでのデバッグ

pnpm dev

ブラウザで該当ページにアクセスし、詳細なエラーメッセージを確認する。

2. 非同期コンポーザブルの代替実装

async functionを避け、同期的なコンポーザブルとして再実装:

// 案: useAsyncDataを呼び出し側に任せる
export function useOgMetaSync(params: OgMetaParams) {
  const { generatePageOgImageUrl } = usePageOgSignature()

  // useAsyncDataのコールバックを返すのみ
  return {
    handler: () => {
      if (!import.meta.server) return null
      return generatePageOgImageUrl(...)
    },
    key: `og-${params.type}-${params.id}`
  }
}

// 使用側
const { handler, key } = useOgMetaSync({ type: 'general', id: 'home', title: 'Home' })
const { data: ogImageUrl } = await useAsyncData(key, handler, { server: true, lazy: false })

3. Nuxtプラグインとしての実装

サーバープラグインとして実装し、コンテキスト問題を回避する。

4. defineNuxtComponent使用

Options APIベースのコンポーネントラッパーを作成し、setup内でコンポーザブルを呼び出す。

現状の対応

  • useOgMetaヘルパーの導入は見送り
  • 既存の44ファイルは旧パターンを維持
  • discriminated union型のみ追加(usePageOgSignature.tsの型安全性向上)

関連ファイル

  • apps/web/app/composables/usePageOgSignature.ts - 署名生成コンポーザブル
  • apps/web/app/pages/about.vue - 旧パターンの参照実装
  • apps/web/tests/og-signature.test.ts - 統合テスト

参考リンク