Cloudflare x LIFFで動的OGPを実装した方法

# はじめに

SNSでシェアされた際のリンクプレビューは、ユーザーの目を引き、クリック率を高める重要な要素です。このプレビューはOGP（Open Graph Protocol）によって制御されますが、認証が必要なWebアプリケーションでは実装が難しい課題があります。特にLINEのLIFFアプリケーションではさらに複雑になります。

今回は、以下の技術スタックを使用してLIFFベースのプロダクトに動的なOGPを実装した方法を紹介します。

* LIFF（LINE Front-end Framework）
* Next.js + Supabase
* Cloudflare（Next.jsをWorker and Pagesにデプロイ）

# 技術スタックについて


### LIFF（LINE Front-end Framework）

LIFFはLINEが提供するウェブアプリケーションプラットフォームで、LINEのユーザーインターフェースの中でウェブコンテンツを表示できます。LINEのユーザー認証を活用でき、LINEのAPIにもアクセスできる便利なフレームワークです。
[LIFF公式ドキュメント](https://developers.line.biz/ja/docs/liff/overview/)

### Next.js + Supabase

Reactベースのフレームワークで、サーバーサイドレンダリング（SSR）、静的サイト生成（SSG）など多くの機能を提供します。今回はメタデータ生成機能も活用しています。
[Next.js公式サイト](https://nextjs.org/)
[LIFFとNext.js, Supabaseでのログインレス認証方法](https://qiita.com/yongyong/items/8bf9e2a9da23dc2f148f)


### Cloudflare

Cloudflareは高速でセキュアなコンテンツ配信ネットワークを提供するサービスです。特に「Cloudflare Pages」と「Cloudflare Workers」は低コストで高性能なホスティング環境を実現できます。個人開発において非常にコスパが良く、今回の実装の鍵となりました。
[Cloudflare Pages](https://pages.cloudflare.com/)

# OGPとは？そして認証サービスでの課題

OGP（Open Graph Protocol）はFacebookが開発したプロトコルで、ウェブページがソーシャルグラフオブジェクトとして機能するためのメタデータを提供します。簡単に言えば、SNSでリンクをシェアした際に表示されるプレビュー（タイトル、説明文、画像など）をコントロールするための仕組みです。
[Open Graph Protocol公式サイト](https://ogp.me/)

```mermaid
sequenceDiagram
    actor User
    participant Web as Webページ
    participant SNS as SNS
    
    User->>Web: 1. Webページ作成<br>(OGPメタタグ設定)
    User->>SNS: 2. SNSでリンクをシェア
    SNS->>Web: 3. OGPクローラーがアクセス
    Web-->>SNS: 4. OGPメタデータを返却
    SNS->>User: 5. リンクプレビュー表示
```

通常、Next.jsでは`generateMetadata`関数を使用して動的なOGPメタデータを生成できます。しかし、認証が必要なサービス（今回の[LIFF + Supabaseベースのアプリケーション](https://qiita.com/yongyong/items/8bf9e2a9da23dc2f148f)）では問題が発生します。

認証前提のサービスでは、OGPクローラー（LINEやTwitterなどのボット）がアクセスしたときに、認証がないため正しいメタデータを取得できません。私の場合、Supabaseの認証を使用しており、認証されていないリクエストはログインページにリダイレクトされてしまいます。そのため、単純に`generateMetadata`を使うだけでは動的なOGPを実現できません。

```mermaid
sequenceDiagram
    participant SNS as SNSボット
    participant Auth as 認証ページ
    participant App as Webアプリ(Metadata生成)
    
    SNS->>App: OGPデータ取得リクエスト
    App->>Auth: 認証チェック
    Auth-->>App: 未認証！
    App-->>SNS: ログインページへリダイレクト
    Note over SNS: 正しいOGPデータを<br>取得できない
```

# 試行錯誤：失敗した方法

### 方法1：Cloudflare Functionsを使用する

最初に試したのは、Cloudflare Functionsを使用してOGPクローラーのアクセスを検知し、Next.jsアプリケーションにアクセスが来る前にOGPを生成する方法です。

```mermaid
flowchart TD
    A[OGPクローラー] --> B{Cloudflare Functions}
    B -->|OGPクローラー検出| C[動的OGP生成]
    B -->|通常ユーザー| D[Next.jsアプリ]
    C --> E[OGPメタデータ返却]
```

[参考記事：Cloudflare Pages Functionsで動的OGP画像を生成する方法](https://zenn.dev/funteractiveinc/articles/cloudflare-pages-functions_ogp)

しかし、Next.jsを`next-on-pages`を使用してWorkerに対応するように変換すると、Functionsのコードが消えて動作しなくなってしまいました。

[参考：next-on-pagesの既知の問題](https://zenn.dev/laiso/articles/c086dd115f5d4e#%E6%97%A2%E7%9F%A5%E3%81%AE%E5%95%8F%E9%A1%8C%3A(3)-%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%81%97%E3%81%9Fpages-functions%E3%81%A9%E3%81%93%E2%80%A6%E2%80%A6)

### 方法2：Next.jsのミドルウェアを使用する

次に試したのは、Next.jsのミドルウェアでOGPボットのアクセスを検知し、認証不要のページにリダイレクトさせてそこで`generateMetadata`関数を実行する方法です。

```mermaid
flowchart TD
    A[OGPクローラー] --> C{Next.jsミドルウェア}
    C -->|OGPボット検出| D[認証不要ページへリダイレクト]
    C -->|通常ユーザー| E[認証付きアプリページ]
    D --> F[generateMetadata実行]

    style C fill:#000000,stroke:#333333,color:white
```

ローカル環境では上手く動作しましたが、Cloudflareにデプロイするとうまく動作せず、LINEのOGPボットがアクセスできなくなりました。エラーメッセージは「TypeError: Request with a GET or HEAD method cannot have a body.」でした。

調査の結果、これはNext.jsのミドルウェアに到達する前の段階でCloudflare側でエラーが発生していることが分かりました。

```mermaid
flowchart TD
    A[Line OGPクローラー] --> X["error (ClaudFlare)"]
    X --"エラーによりブロック"--> C{Next.jsミドルウェア}
    C -->|OGPボット検出| D[認証不要ページへリダイレクト]
    C -->|通常ユーザー| E[認証付きアプリページ]
    D --> F[generateMetadata実行]

    style C fill:#000000,stroke:#333333,color:white
    style X fill:#F6821F,stroke:#E67E22,color:white
```

# 解決策：Worker Routesを活用する

最終的に成功した方法は、Cloudflare Worker Routesを使用する方法です。以下が実装手順です。

```mermaid
flowchart TD
    A[OGPクローラー] --> B{Cloudflare Worker Routes}
    Z[通常ユーザー] --> B
    
    B -->|OGPクローラー検出| C[OGPデータ取得API]
    B -->|通常ユーザー| D[Next.jsアプリ]
    
    C --> E[HTML + OGPメタタグ生成]
    E --> F[OGPレスポンス返却]
    
    style B fill:#F6821F,stroke:#E67E22,color:white
    style C fill:#4CAF50,stroke:#388E3C,color:white
    style D fill:#000000,stroke:#333333,color:white
    style F fill:#2196F3,stroke:#1976D2,color:white
```

1. Cloudflareでドメインを購入
2. Next.jsアプリケーションをCloudflare Pages and Workersにデプロイ
3. 2のCloudflare Pages and Workersを1で購入したドメインに接続
4. 新しいWorkerを作成し、OGPクローラー検出用のコードを実装
5. Worker Routesで設定

以下が実装したWorkerのコアコードです：

```js
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request, event))
})

async function handleRequest(request, event) {
  const userAgent = request.headers.get('User-Agent') || '';

  // OGPクローラーかどうかをUser-Agentで判定
  const isOgpCrawler = [
    "facebookexternalhit",
    "line-poker",
    "Facebot",
    "Twitterbot",
    "LineBot",
    "LinkedInBot",
    "Slackbot",
    "TelegramBot",
    "Discordbot",
    "WhatsApp",
    "GoogleImageProxy",
  ].some((bot) => userAgent.includes(bot));

  if (isOgpCrawler) {
    // URLからIDを抽出
    const url = new URL(request.url);
    const id = url.pathname.split('/').pop();

    try {
      // OGPデータを取得
      const ogpApiUrl = `https://your-api-endpoint.com/api/public/ogp/${id}`;
      const res = await fetch(ogpApiUrl);
      
      if (!res.ok) {
        throw new Error(`API returned ${res.status} ${res.statusText}`);
      }
      const ogpData = await res.json();

      // デフォルト値の設定
      const defaultTitle = "アプリケーション名";
      const defaultDescription = "アプリケーションの説明";

      const title = ogpData?.title ? `${ogpData.title} | アプリケーション名` : defaultTitle;
      const description = ogpData?.description || defaultDescription;
      const fullImageUrl = ogpData?.imageUrl ? `https://your-image-domain.com/${ogpData.imageUrl}` : null;

      // OGPメタタグを含むHTMLを生成
      const html = `
      <!DOCTYPE html>
      <html lang="ja">
      <head>
        <meta charset="UTF-8">
        <title>${title}</title>
        <meta property="og:title" content="${title}" />
        <meta property="og:description" content="${description}" />
        ${fullImageUrl ? `<meta property="og:image" content="${fullImageUrl}" />
        <meta property="og:image:width" content="1024" />
        <meta property="og:image:height" content="1024" />
        <meta property="og:image:alt" content="${title}" />` : ''}
        <meta property="og:url" content="${request.url}" />
        <meta property="og:type" content="website" />
        <meta name="twitter:card" content="summary_large_image" />
        <meta name="twitter:title" content="${title}" />
        <meta name="twitter:description" content="${description}" />
        ${fullImageUrl ? `<meta name="twitter:image" content="${fullImageUrl}" />` : ''}
      </head>
      <body>
        <!-- 必要に応じてコンテンツを追加 -->
      </body>
      </html>
      `;
      
      return new Response(html, {
        headers: { 'Content-Type': 'text/html; charset=utf-8' }
      });

    } catch (error) {
      console.error("OGP generation error:", error);
      return fetch(request); // エラー時はNext.jsからのレスポンスをそのまま返す
    }
  } else {
    // OGPクローラーでない場合は、Next.jsアプリケーションにリクエストを転送
    return fetch(request);
  }
}
```

このWorkerを設定した後、Cloudflareの「Worker Routes」セクションで以下のようにルーティングを設定します：

```
example.com/*
```

```mermaid
sequenceDiagram
    participant Bot as OGPクローラー
    participant User as 通常ユーザー
    participant Worker as Cloudflare Worker
    participant API as 公開API
    participant Next as Next.jsアプリ
    
    Bot->>Worker: アクセス
    User->>Worker: アクセス
    
    Worker->>Worker: User-Agent判定
    
    alt OGPクローラーの場合
        Worker->>API: OGPデータ取得
        API-->>Worker: データ返却
        Worker->>Worker: OGPメタタグ生成
        Worker-->>Bot: OGPメタタグ含むHTML
    else 通常ユーザーの場合
        Worker->>Next: リクエスト転送
        Next-->>Worker: レスポンス
        Worker-->>User: アプリ画面
    end
```

これにより、あらゆるリクエストがまずこのWorkerを通過し、OGPクローラーからのアクセスのみ特別な処理を行い、その他のアクセスは通常通りNext.jsアプリケーションに転送されます。

![スクリーンショット 2025-03-24 11.49.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918450/fbaf1041-ac0c-45a4-853c-2a2f54c617af.png)

# まとめ

Cloudflareの柔軟な機能を活用することで、認証が必要なLIFFベースのアプリケーションでも動的なOGPを実装することができました。

特に注目すべき点は、Cloudflareを使用することで、Next.jsアプリケーションにアクセスが到達する前に処理を挟むことができる点です。これにより、認証の壁を回避しつつ、適切なOGPメタタグを提供することが可能になりました。

Cloudflareは無料プランでも多くの機能が利用でき、ドメイン購入と組み合わせることでさらに可能性が広がります。個人開発においてコストを抑えつつセキュアな環境を構築できる点は非常に魅力的です。

今回の実装を通じて、フロントエンドとインフラの境界線での工夫がユーザー体験を大きく向上させることを実感しました。みなさんも是非Cloudflareの柔軟な機能を活用して、より良いWebアプリケーションを構築してみてください！

# 実際のアプリケーション例
本記事で解説した認証フローを実装した実例として、私が開発している「cookLab」をご紹介します。

「cookLab」は、料理好きの方々が簡単にレシピを共有し、お気に入りのレシピを保存できるLINE Bot + LIFFアプリです。

主な機能：

- レシピの検索、解析
- レシピの相談
- 盛り付け例の画像提案
- セール品で作れるレシピの提案

等、料理を楽しめる様々な機能があります。
以下のQRコードまたはリンクから、LINEの公式アカウントを友だち追加して、実際の認証フローと機能を体験してみてください！

![Image from Qiita.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/918450/cdd5d4af-2014-42ab-9892-ad501678caff.png)


ぜひ以下のLINE公式アカウントを友だち追加して、実際の認証フローを体験してみてください！

[📱 LINE公式アカウントを友だち追加](https://line.me/R/oaMessage/@807rpwtd)