LINEトーク画面の中にWebフォームを表示する

LINE公式アカウントを運用していると、「顧客の連絡先を収集したい」「面談の希望日時を入力してもらいたい」といった場面に必ず出会います。Flex Messageのボタンだけでは対応できない複雑な入力を、LINEアプリの中でシームレスに実現するのが LIFF(LINE Front-end Framework) です。

この記事では、WIOの工事屋さんの実装事例をもとに、Cloudflare Pages上でLIFFアプリを構築し、D1データベースの顧客情報と連携するパターンを解説します。

LINE上でcompact LIFFが立ち上がり、連絡希望フォームが表示されている様子

上の画像は、LINEのトーク画面で「メールで連絡を希望」ボタンをタップした際の画面です。画面下部に小さなWebフォームがスライドアップし、氏名・メールアドレス・連絡希望時間帯を入力できます。このフォームがLIFFアプリです。

LIFFの3サイズと使い分け

LIFFには画面の占有率が異なる3つのサイズがあり、用途に応じて使い分けます。LINE Developers公式ドキュメントに詳しい仕様が掲載されています。

compact(画面の約40%) は、入力項目が3〜4個程度の簡易フォームに最適です。連絡先の入力やアンケート回答など、ユーザーに「ちょっとだけ入力してもらう」場面で使います。LINEのトーク画面が上部に見えた状態で下部にフォームが表示されるため、ユーザーは自分がLINEの中にいることを意識しながら操作できます。

tall(画面の約80%) は、やや多めの入力項目や一覧表示に向いています。マイページのような「複数項目を一度に閲覧・編集する」画面で威力を発揮します。WIOの工事屋さんでは、顧客情報の登録・編集画面(氏名・フリガナ・電話番号・メールアドレス・住所・備考の6項目)にtallサイズを採用しています。

full(画面の100%) は、複雑な操作やリッチなUIが必要な場面向けです。商品カタログの閲覧、地図表示、ファイルアップロードなど、LINEの外のWebアプリに近い体験を提供する場合に選びます。ただし、フルスクリーンはユーザーが「LINEの中にいる」感覚が薄れるため、必要以上に使わないのが得策です。

実務での判断基準はシンプルです。入力が3項目以下ならcompact、4〜6項目ならtall、それ以上や複雑なUIが必要ならfullです。迷ったらcompactから始めて、ユーザーの入力体験を見ながらサイズを上げるのが安全な進め方です。

なお、LIFFのサイズは作成後でもLIFF APIのPUT /liff/v1/apps/{liffId}で変更できます。本番環境でユーザーの操作ログを確認してからサイズを調整する運用も有効です。

アーキテクチャ全体像

LIFF連絡フォームのシステム構成は、3つのCloudflareサービスで成り立っています。

まず Cloudflare Pages がLIFFのHTMLを配信します。LIFFアプリの実体はWebページなので、静的ホスティングで十分です。次に Cloudflare WorkersAPI がフォームの送信先となり、データの保存とLINEへの確認メッセージ送信を担当します。最後に D1データベース が顧客情報と問い合わせ内容を永続化します。

データフローを追うと、ユーザーがLINE上でFlex Messageのボタンをタップすると、LIFFのURLが開かれます。LIFFアプリは起動時にLINE SDKの初期化を行い、ユーザーのアクセストークンを取得します。このトークンをAPIに送ることで、サーバー側でLINEのユーザーIDを特定し、既存の顧客情報があれば自動入力、なければ新規登録という分岐が実現します。

[LINE トーク画面]
  ↓ Flex Messageのボタンタップ
[LIFF起動 → liff.init()]
  ↓ liff.getAccessToken()
[Cloudflare Pages → HTML/JS]
  ↓ fetch(/api/mypage, Bearer Token)
[Cloudflare Workers API]
  ↓ LINE Profile API でユーザーID特定
[D1 Database]
  ↓ 顧客データ取得 or 新規作成
[フォーム自動入力 → 送信 → 確認メッセージ]

LIFF作成の手順

LIFFアプリはLINE Developersコンソールの LINE Loginチャネル に紐づきます。Messaging APIチャネルではないことに注意してください。LIFF IDの先頭の数字がLoginチャネルIDと一致するので、複数プロジェクトを運用する際の識別に役立ちます。

作成はLIFF REST APIで行えます。LINE LoginチャネルのアクセストークンをAuthorizationヘッダーに設定し、LIFFのサイズとエンドポイントURLを指定してPOSTリクエストを送ります。レスポンスにLIFF IDが含まれるので、これをHTMLのJavaScriptに埋め込みます。

アクセストークンの取得は、v2エンドポイント(POST /oauth2/v2.1/token)を使用します。v2.1ではJWT assertionが必要になるため、用途が異なります。チャネルIDとチャネルシークレットを使ったclient_credentials認証でトークンを発行し、このトークンでLIFFの作成・更新・削除が可能です。

トークンの有効期限に注意してください。 v2で発行したチャネルアクセストークンは有効期限が30日です。本番運用ではトークンの定期的な再発行をスケジュールに組み込むか、v2.1のステートレストークン(JWT assertion方式)への移行を検討しましょう。

Cloudflare PagesでのCSP設定

LIFFをCloudflare Pagesで配信する際に最もハマりやすいのが Content Security Policy(CSP) の設定です。LIFFのSDKは複数の外部ドメインにアクセスするため、適切にCSPを設定しないとSDKの読み込み自体がブロックされます。

Cloudflare Pagesの_headersファイル設定ドキュメントに従い、プロジェクトルートの_headersファイルでCSPを一括管理します。LIFFアプリに必要なドメインは以下の通りです。

script-src には static.line-scdn.net が必要です。LIFF SDKのJavaScriptがこのドメインから配信されます。

connect-src には api.line.me と liffsdk.line-scdn.net の2つが必要です。前者はユーザープロフィール取得などのAPI呼び出し、後者はLIFF SDKが起動時にマニフェストファイルを取得するためにアクセスします。この liffsdk.line-scdn.net は見落としがちで、これが漏れるとLIFF初期化が「TypeError: Failed to fetch」で失敗します。

frame-src には liff.line.me が必要です。LIFF SDKがiframeで内部的に通信を行うために使用します。

form-action には access.line.me を追加します。LINEログインのリダイレクト先として使われます。

さらに、自社APIのドメインも connect-src に含めることを忘れないでください。LIFFからAPIへのfetch呼び出しがCSPでブロックされるケースは非常に多いです。

CSPのデバッグには、ブラウザのデベロッパーツールのコンソールが最も確実です。CSPブロックはネットワークタブには出ず、コンソールにのみエラーが表示されます。デプロイ後は必ずコンソールを確認する習慣をつけてください。

実際のCSP設定例を示します。

/*
  Content-Security-Policy: default-src 'self'; script-src 'self' static.line-scdn.net; connect-src 'self' api.line.me liffsdk.line-scdn.net https://your-api.example.com; frame-src liff.line.me; form-action 'self' access.line.me; style-src 'self' 'unsafe-inline'; img-src 'self' data: profile.line-scdn.net

profile.line-scdn.net を img-src に追加しているのは、LINEユーザーのプロフィール画像を表示する場合に必要だからです。マイページ等でアイコンを表示する予定があれば忘れずに含めてください。

CORS設定の注意点

LIFFページはサイトのドメインから配信され、APIはサブドメインで動作するため、クロスオリジンリクエストが発生します。HonoのcorsミドルウェアでallowHeadersにAuthorizationを含めることが重要です。

LIFFアプリはLINEのアクセストークンをAuthorization Bearerヘッダーで送信します。このヘッダーはCORSのシンプルリクエストに含まれないため、ブラウザはプリフライト(OPTIONS)リクエストを送ります。サーバーがAccess-Control-Allow-HeadersにAuthorizationを含めて返さないと、本リクエストがブロックされます。

allowHeadersにContent-Typeだけ設定しているケースは非常に多く、Authorizationの追加を忘れがちです。この問題はエラーメッセージが分かりにくく、「ネットワークエラー」としか表示されないため原因の特定に時間がかかることがあります。MDN Web Docsのプリフライトリクエスト解説が理解の助けになります。

Honoでの設定例は以下の通りです。

import { cors } from 'hono/cors';

app.use('/api/*', cors({
  origin: ['https://your-liff.pages.dev', 'https://your-domain.com'],
  allowHeaders: ['Content-Type', 'Authorization'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  maxAge: 86400,
}));

maxAge: 86400(24時間)を設定することで、ブラウザがプリフライトの結果をキャッシュし、同一オリジンからの後続リクエストではOPTIONSリクエストを省略します。LIFFフォームのように短時間に複数のAPI呼び出しが発生する画面では、体感速度の改善につながります。

LINEトークン認証パターン

LIFFアプリからのAPI呼び出しでは、セッションCookieではなくLINEのアクセストークンで認証を行います。LIFF内でliff.getAccessToken()を呼び出して取得したトークンを、APIにBearerトークンとして送信します。

API側では、受け取ったトークンをLINEのVerify APIエンドポイントもしくはProfile APIに転送してユーザー情報を取得します。Verify APIはトークンの有効性確認のみを行い、Profile APIはユーザーID・表示名・プロフィール画像を返します。用途に応じて使い分けてください。トークンが無効であれば401を返し、有効であれば取得したユーザーIDをキーにD1の顧客データを検索します。

この方式のメリットは、セッション管理が不要なことです。LIFF内では常にLINEのセッションが有効なため、APIは状態を持たずにリクエストごとにトークンを検証するだけで認証が完結します。

セキュリティ上の注意点として、 liff.getIDToken()で取得できるIDトークン(JWT)をサーバー側で検証する方法も選択肢にあります。IDトークンはLINE側の署名が付与されているため、外部APIへの問い合わせなしにオフラインで検証が可能です。IDトークン検証の公式ガイドを参照してください。ただし、IDトークンの署名検証には公開鍵の管理が必要になるため、実装の複雑さとのトレードオフを考慮する必要があります。

D1顧客データとの連携

LIFFフォームの入力データをD1のcustomersテーブルに保存する際、upsertパターンを使います。LINEユーザーIDで既存レコードを検索し、存在すればUPDATE、なければINSERTします。

D1(SQLiteベース)では INSERT ... ON CONFLICT DO UPDATE 構文でupsertを実現できます。

INSERT INTO customers (line_user_id, display_name, email, phone, updated_at)
VALUES (?, ?, ?, ?, datetime('now'))
ON CONFLICT (line_user_id) DO UPDATE SET
  display_name = excluded.display_name,
  email = excluded.email,
  phone = excluded.phone,
  updated_at = datetime('now');

この設計により、マイページで入力済みの氏名・電話番号・メールアドレスが、連絡フォームを開いた際に自動入力されます。逆に、連絡フォームで入力した情報はマイページにも反映されます。ユーザーが同じ情報を何度も入力する必要がなくなり、フォームの離脱率を下げる効果があります。

APIエンドポイントの設計としては、顧客データの取得と保存を担当する /api/mypage と、連絡希望の保存を担当する /api/contact-request を分離しています。/api/contact-request は問い合わせテーブルへの保存と顧客テーブルのupsertを同一リクエストで行うため、フォームの送信ボタンを1回押すだけで両方のテーブルが更新されます。Cloudflare D1の公式ドキュメントにD1固有のSQL方言や制約事項が記載されています。

また、D1はCloudflare Workersの実行コンテキスト内でのみアクセス可能なため、外部から直接データベースに接続されるリスクがありません。この点は従来のMySQL/PostgreSQLベースの構成と比較した際のセキュリティ上の利点です。

フォーム送信後のLINE確認メッセージ

ユーザーがフォームを送信した後、LINEのトーク画面に確認メッセージを送ることで、情報が正しく受理されたことを伝えます。このメッセージはFlex Messageではなく、装飾のない平文テキストで送ります。理由は、確認メッセージは情報の正確な伝達が目的であり、見栄えよりも内容の読みやすさが重要だからです。

技術的には、APIの処理フローの最後にLINE Messaging APIのpushメッセージを送信します。Messaging APIのアクセストークンはD1のsettingsテーブルに保存されており、APIワーカーが必要に応じて読み取ります。pushメッセージは無料メッセージ通数の範囲を超えると課金対象ですが、問い合わせ1件あたり1通の確認メッセージなので、コスト面で問題になることはほぼありません。

送信するメッセージには、ユーザーが入力した連絡手段と希望時間帯を含め、担当者からどのような形で連絡が来るのかを明示します。「担当者よりメールをさせていただきます」「担当者よりお電話をさせていただきます」のように、次に起こるアクションを具体的に記載することで、ユーザーの不安を取り除きます。

URLパラメータによるフォーム切替

1つのLIFFアプリで複数のフォームパターンを表示するために、URLパラメータを活用しています。type=email でメール連絡フォーム、type=phone で電話連絡フォーム、type=meeting でオンライン面談フォームと、パラメータに応じてフォームの内容を動的に切り替えます。

HTML上には全パターンのフィールドを非表示で配置しておき、JavaScriptで該当するフィールドだけを表示します。この方式のメリットは、LIFFアプリの登録が1つで済むことです。LINE Developersコンソールでの管理が簡素になり、CSPやCORSの設定も1箇所で完結します。

Flex Messageのボタンには、それぞれ異なるtypeパラメータ付きのLIFF URLを設定します。LINEで相談する場合はpostbackアクションでテキスト応答、それ以外はURIアクションでLIFFを起動するという使い分けです。

URLパラメータの取得には、LIFF SDKが提供する liff.getContext() ではなく、標準の URLSearchParams を使います。LIFF URLにクエリパラメータを付与すると、そのまま開かれたWebページのURLにも引き継がれるため、通常のWeb開発と同じ手法でパラメータを取得できます。

const params = new URLSearchParams(window.location.search);
const formType = params.get('type') || 'email';

エラーハンドリングとフォールバック

LIFF環境特有のエラーパターンを把握しておくと、トラブル発生時の対応が速くなります。

liff.init()の失敗 は、CSP設定ミスかLIFF IDの誤りが主な原因です。init()はPromiseを返すため、catchブロックでエラーメッセージをユーザーに表示し、LINEの外部ブラウザで開き直すリンクを提示するのが親切です。

外部ブラウザからのアクセス も考慮が必要です。liff.isInClient()がfalseを返す場合、ユーザーはLINEアプリの外(ChromeやSafariなど)からLIFFのURLにアクセスしています。この場合、liff.login()でLINEログインに誘導するか、LINE公式アカウントの友だち追加画面に案内するか、運用方針に応じて分岐させます。liff.isInClient()のリファレンスで動作の詳細を確認できます。

まとめ

LIFFを活用した連絡フォームは、LINE公式アカウントの運用を次のレベルに引き上げます。重要なポイントを整理します。

LIFFサイズはcompactから始めるのが安全です。入力項目が増えてきたらtallに変更し、複雑なUIが必要な場合のみfullを使います。

CSPの設定は最も注意が必要な工程です。static.line-scdn.net、api.line.me、liffsdk.line-scdn.net、liff.line.me、access.line.me の5ドメインを正しく設定し、デプロイ後は必ずブラウザコンソールで確認してください。

顧客データのupsertパターンにより、一度入力した情報を再利用できる仕組みを整えることで、ユーザー体験が格段に向上します。「何度も同じ情報を入力させない」という基本を、LINE上の体験でも実現できるのがLIFFの強みです。

LIFFの開発を始める際は、LINE Developers公式のLIFFスターターアプリを雛形にすると、環境構築の手間を大幅に省けます。