capacitor-deep-linking について
Capacitor アプリの開発者向けに、カスタムスキーム、iOS Universal Links、Android App Links の設定と調査を支援するスキルです。コールド/ウォーム起動時の URL 処理や iOS の SceneDelegate にも対応します。
主な機能
- 01カスタムスキームと検証済みリンクの設定
- 02コールド/ウォーム起動 URL の処理
- 03iOS SceneDelegate ライフサイクルへの対応
- 04リンクとプラットフォーム設定の検証
- 05OAuth などのリダイレクト調査
活用シーン
- 01OAuth や決済のコールバックをアプリへ戻す
- 02アプリ起動後にホーム画面が開く問題の修正
- 03Android のドメイン検証失敗の診断
- 04Capacitor 8.5 以降の変更への対応
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ cap-go/ capgo-skills --skill capacitor-deep-linkingインストール後、Claude Code で/capacitor-deep-linkingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.jsonreferences/android-app-links.mdios-scene-delegate.mdios-universal-links.mdrouting-patterns.md詳細
Capacitor のディープリンク
カスタムスキームリンク、iOS の Universal Links、Android の App Links を Web アプリにルーティングします。
使用する場面
次の場合に使用します:
- Capacitor アプリに
myapp://スキーム、Universal Links、または App Links を追加する - リンクを開くとアプリではなくブラウザーが開く、またはアプリは開くがホームページに移動する
- ウォーム起動では
appUrlOpenが発火するが、コールド起動では URL が取得できない(またはその逆) - Capacitor 8.5 以降への更新後、または
SceneDelegate.swiftの採用後にディープリンクが動作しなくなった - OAuth/マジックリンク/決済のリダイレクトでアプリに戻る必要がある
adb shell pm get-app-linksの結果がverifiedではなくnoneまたは1024になる
使用しないでください:
- アプリ全体の AppDelegate から SceneDelegate への移行:
capacitor-uiscene-migration - 通知タップのルーティング:
capacitor-push-notifications - デバッグ時のネイティブログの取得:
ios-android-logs - Sign in with Apple/Google プラグインの設定:
capacitor-plugins
リンクの種類
| 種類 | プラットフォーム | 形式 | ホストされたファイルが必要 | 所有権の検証 |
|---|---|---|---|---|
| カスタムスキーム | 両方 | myapp://product/123 | いいえ | いいえ(どのアプリでも登録可能) |
| Universal Links | iOS | https://example.com/product/123 | /.well-known/apple-app-site-association | はい |
| App Links | Android 6 以降 | https://example.com/product/123 | /.well-known/assetlinks.json | はい |
セキュリティ上重要な用途(OAuth、マジックリンク)には、検証済みの HTTPS リンクを推奨します。カスタムスキームは別のアプリに乗っ取られる可能性があります。
ワークフロー
- まず調査します。
capacitor.config.*、ios/App/App/Info.plist、ios/App/App/*.entitlements、ios/App/App/AppDelegate.swift、ios/App/App/SceneDelegate.swift(存在する場合)、android/app/src/main/AndroidManifest.xml、およびpackage.jsonの@capacitor/appのバージョンを確認します。 - iOS のライフサイクル経路を決めます。
- Info.plist に
UIApplicationSceneManifestがある(Capacitor 8.5 以降のテンプレート、または移行済みアプリ)場合 -> URL イベントはSceneDelegateに届きます。AppDelegate のapplication(_:open:options:)またはapplication(_:continue:restorationHandler:)にあるカスタムコードは動作しません。references/ios-scene-delegate.md を参照してください。 - シーンマニフェストがない場合 -> 8.5 コアでは引き続き AppDelegate 経路が使えますが、Xcode 27/Capacitor 9 ではシーンライフサイクルが必要です。移行は
capacitor-uiscene-migrationに引き継いでください。
- Info.plist に
- インストールします。
@capacitor/app(npm install @capacitor/app && npx cap sync)。 - JS ハンドラーをアプリの起動時に一度だけ登録します(ルーターのレンダリング前)。
appUrlOpenとgetLaunchUrl()の両方を使用します。以下を参照してください。 - プラットフォームを設定します:カスタムスキーム(Info.plist の
CFBundleURLTypes、Android の<intent-filter>)、続いてプラットフォームごとの検証済みリンク:- iOS Universal Links:references/ios-universal-links.md
- Android App Links:references/android-app-links.md
- ルーティングします。フレームワークのルーターで設定します:references/routing-patterns.md(React、Vue、Angular、OAuth コールバック、遅延ディープリンク)。
- 実機で以下のコールド/ウォーム起動の表を使って検証します。
トピックが該当する場合にのみ、参照ファイルを読み込んでください。
JS ハンドラー(すべてのアプリに必要な部分)
import { App, type URLOpenListenerEvent } from '@capacitor/app';
let lastHandled: string | undefined;
function route(rawUrl: string) {
if (rawUrl === lastHandled) return; // cold start can deliver the same URL to both paths
lastHandled = rawUrl;
const url = new URL(rawUrl);
// myapp://product/123 -> host "product", pathname "/123"
// https://example.com/product/123 -> pathname "/product/123"
const path = url.protocol.startsWith('http')
? url.pathname
: `/${url.host}${url.pathname}`;
router.navigate(path + url.search); // your router
}
export async function initDeepLinks() {
await App.addListener('appUrlOpen', (event: URLOpenListenerEvent) => route(event.url));
const launch = await App.getLaunchUrl(); // resolves undefined or { url }
if (launch?.url) route(launch.url);
}
注意点:
- カスタムスキーム URL は、最初のセグメントが
pathnameではなくhostとして解析されます。上記のように両方の形式を正規化してください。 - リスナーは遅延読み込みされるページ内ではなく、起動時に登録してください。イベントの発火後に追加したリスナーはイベントを受け取れません。コールドスタート時は
getLaunchUrl()が代替手段になります。 - リスナーからの状態更新は、フレームワークのゾーン/実行ループ(Angular の
NgZone.run)でラップしてください。そうしないと、ナビゲーションが何も行わずに終わります。 - 重複を排除してください:コールドスタート時は
getLaunchUrl()とappUrlOpenの両方から URL を取得できる場合があります。 - ナビゲーションの前にパスを検証し、許可リストと照合してください。ディープリンクは信頼できない入力です。
カスタムスキームの設定
iOS ios/App/App/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.example.app</string>
<key>CFBundleURLSchemes</key>
<array><string>myapp</string></array>
</dict>
</array>
Android AndroidManifest.xml の既存の MainActivity 内(ウォームオープン時に同じアクティビティを再利用できるよう、テンプレートの android:launchMode="singleTask" は維持してください):
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>
検証
すべてのケースを実機で実行してください(ユニバーサルリンクはシミュレーターのアドレスバーからは安定して動作しないため、代わりにメモまたはメッセージからタップしてください)。
| シナリオ | iOS | Android |
|---|---|---|
| カスタムスキーム、ウォーム | xcrun simctl openurl booted "myapp://product/123" | adb shell am start -W -a android.intent.action.VIEW -d "myapp://product/123" com.example.app |
| カスタムスキーム、コールド | アプリを終了してから、同じコマンドを実行 | adb shell am force-stop com.example.app を実行してから、同じコマンドを実行 |
| 検証済みリンク、ウォーム | メモで https://example.com/product/123 をタップ | adb shell am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://example.com/product/123" |
| 検証済みリンク、コールド | アプリをスワイプして終了し、リンクをタップ | 強制終了してから、同じコマンドを実行 |
| 所有権の確認 | curl -sI https://example.com/.well-known/apple-app-site-association(200、リダイレクトなし) | adb shell pm get-app-links com.example.app に verified と表示される |
合格基準:各ケースで正しいルートが開き、appUrlOpen が一度だけログに記録され、コールドスタート時に getLaunchUrl() が URL を返すこと。Capacitor 8.5 のシーン移行後は、すべてのケースを再実行してください。コールド起動はウォームオープン(scene(_:openURLContexts:)/scene(_:continue:))とは異なるネイティブパス(scene(_:willConnectTo:options:))を通ります。
移行後に残っている古いハンドラーを grep で検索します。
grep -n "open url: URL\|continue userActivity" ios/App/App/AppDelegate.swift
grep -n "SceneDelegateProxy" ios/App/App/SceneDelegate.swift
エラー処理
| 症状/メッセージ | 原因 | 修正方法 |
|---|---|---|
| アプリをインストール済みなのにリンクが Safari で開く | AASA が取得されていない、appID が間違っている、エンタイトルメントがない、またはユーザーがリンクを長押しして「Safari で開く」を選択した | Apple CDN 経由で AASA を確認し、TEAMID.bundleId と applinks: エンタイトルメントを確認します。リンクを長押しして「アプリで開く」を選択し、設定をリセットします |
| 8.5 への更新後、カスタム AppDelegate の URL コードが実行されなくなった | シーンマニフェストが存在するため、iOS は SceneDelegate を呼び出します | ロジックを SceneDelegate に移します(参考:ios-scene-delegate) |
| ウォームオープンは動作するが、コールド起動でホーム画面が開く | リスナーの登録が遅い、またはカスタム SceneDelegate コードが connectionOptions を無視している | 起動時に getLaunchUrl() を呼び出し、SceneDelegateProxy.shared.scene(_:willConnectTo:options:) を維持します |
Safari cannot open the page because the address is invalid | インストール済みビルドでカスタムスキームが登録されていない | CFBundleURLTypes を追加して再ビルドします(cap sync だけでは不十分です) |
Error: Activity not started, unable to resolve Intent | スキーム/ホストに一致するインテントフィルターがない | <data> エントリを修正し、アプリを再インストールします |
pm get-app-links に 1024 または none と表示される | assetlinks.json に到達できない、パッケージが間違っている、または SHA-256 が間違っている(多くの場合、Play App Signing キーが原因) | ファイルを修正してから、adb shell pm verify-app-links --re-verify com.example.app を実行します |
| Android で https リンクを開くとアプリ選択画面が表示される | ドメインが検証されていない(autoVerify がない、または検証に失敗した) | android:autoVerify="true" を追加して、再検証します |
| OAuth リダイレクトがアプリではなくブラウザーのタブに戻る | リダイレクト URI が登録済みのスキーム/検証済みドメインと一致しない | 正確なリダイレクト URI を登録します。検証済みの https リンクを推奨します |
リソース
- Capacitor ディープリンクガイド:https://capacitorjs.com/docs/guides/deep-links
- App プラグイン API:https://capacitorjs.com/docs/apis/app
- 8.5 への更新(UIScene):https://capacitorjs.com/docs/updating/8-5
- Apple ユニバーサルリンク:https://developer.apple.com/documentation/xcode/supporting-associated-domains
- Android App Links:https://developer.android.com/training/app-links/verify-android-applinks