◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-06-09中級

Rork Max でディープリンクを実装する — Universal Links と URL Schemes

Rork Max アプリにディープリンクを追加する実装ガイド。URL Scheme と Universal Links の使い分け、AASA/assetlinks の落とし穴、コールド起動の取りこぼしを実例つきで解説します。

rork59deep-linksuniversal-linksreact-native12tutorial

タップ一つで「目的の画面」まで運ぶ

プッシュ通知を送ったのに、開いてみるとホーム画面に戻されてしまう。せっかく「今日の壁紙が届きました」と知らせても、ユーザーがその壁紙までたどり着く前に離脱してしまう — これは私自身が壁紙アプリの運営で何度も経験した、もったいない取りこぼしでした。

ディープリンクは、この距離をゼロにする仕組みです。myapp://wallpaper/sky-blue のような URL をタップすると、アプリが起動して、その壁紙の詳細画面が直接開きます。通知・Web サイト・他アプリ・広告計測のリンク、いずれの入口からでも「目的の一画面」までまっすぐ運べるようになります。

ここからは Rork Max(Expo ベース)で動くアプリに、URL Scheme と Universal Links の両方を実装する手順と、実機で初めて気づくつまずきどころを順に見ていきます。

URL Scheme と Universal Links、どちらをいつ使うか

二つの方式は役割が違います。URL Scheme(myapp://path)は実装が最も手軽で、アプリ内テストや他アプリからの呼び出しに向いています。ただしアプリが未インストールだとタップしても何も起きず、ユーザーには「壊れたリンク」に見えてしまいます。

Universal Links(iOS)と App Links(Android)は、どちらも https://myapp.com/path という通常の Web URL を使います。アプリが入っていれば直接アプリが開き、入っていなければそのまま Web ページが開く — この「切り替えのシームレスさ」が最大の利点で、Apple も公式に推奨している方式です。マーケティングや通知から外部に出す URL は、原則 Universal Links / App Links を使うのが安全だと考えています。

実務での使い分けはシンプルで、アプリ内部やデバッグでは URL Scheme、ユーザーに渡すリンクは Universal Links、と二段構えにしておくと迷いません。

app.config でスキームと関連ドメインを設定する

Rork Max が生成したプロジェクトは Expo 構成なので、app.json(または app.config.ts)に入口を宣言します。

{
  "expo": {
    "scheme": "myapp",
    "ios": {
      "associatedDomains": ["applinks:myapp.com"]
    },
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": true,
          "data": [
            { "scheme": "https", "host": "myapp.com", "pathPrefix": "/wallpaper" }
          ],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

scheme が URL Scheme(myapp://)の登録、associatedDomains と intentFilters が Universal Links / App Links の宣言です。ここまでは設定ファイルだけの話で、つまずく人はほとんどいません。問題は次のサーバー側にあります。

Universal Links が「効かない」ときに最初に疑う場所

「設定は全部入れたのに、リンクをタップすると Safari が開いてしまう」— Universal Links で最もよく聞く症状で、原因のほとんどはアプリ側ではなくサーバー側の検証ファイルにあります。

iOS は、宣言したドメインの https://myapp.com/.well-known/apple-app-site-association(AASA)というファイルを取りに行き、内容が一致して初めてアプリを開きます。このファイルでつまずく典型が三つあります。配信パスが /.well-known/ 配下になっていない、Content-Type が application/json でない、そして途中でリダイレクト(301/302)が挟まっている、の三つです。いずれか一つでも該当すると iOS は黙ってあきらめ、ただの Web リンクとして Safari を開きます。

正しい AASA は、リダイレクトなしの 200 で次の JSON を返す状態です。

{
  "applinks": {
    "details": [
      {
        "appIDs": ["YOUR_TEAM_ID.com.example.myapp"],
        "components": [
          { "/": "/wallpaper/*", "comment": "壁紙詳細へのリンク" }
        ]
      }
    ]
  }
}

Android 側は https://myapp.com/.well-known/assetlinks.json を見ます。

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "com.example.myapp",
      "sha256_cert_fingerprints": ["YOUR_SHA256_FINGERPRINT"]
    }
  }
]

ここで私がはまったのは sha256_cert_fingerprints でした。手元のデバッグ証明書のフィンガープリントを入れたまま本番 AAB を出すと、Play 署名鍵と一致せず App Links が検証されません。本番では Play Console の「アプリの署名」に表示される SHA-256 を使う、というのが地味ですが大事なポイントです。AASA / assetlinks は配置したら、ブラウザで実際にその URL を開いて、リダイレクトなしの素の JSON が返ることを必ず目視確認するようにしています。

React Navigation との統合とコールド起動の取りこぼし

入口が通ったら、URL をどの画面に割り当てるかを定義します。Rork Max には自然言語でそのまま頼めます。

「アプリに次のディープリンクを追加してください:
- myapp://home → ホーム画面
- myapp://wallpaper/:id → 壁紙詳細画面
- myapp://profile → プロフィール画面
React Navigation の linking 設定を使って実装してください。」

生成される linking 設定はおおむね次の形になります。

const linking = {
  prefixes: ['myapp://', 'https://myapp.com'],
  config: {
    screens: {
      Home: 'home',
      WallpaperDetail: 'wallpaper/:id',
      Profile: 'profile',
    },
  },
};
 
<NavigationContainer linking={linking}>
  {/* ... */}
</NavigationContainer>

ここで実機でしか気づけない落とし穴があります。リンクの受け取りには二つの経路があり、アプリが起動中(ウォーム) のときは Linking.addEventListener('url', ...) が発火しますが、アプリが完全に終了した状態(コールド) から起動したときは、最初の URL を Linking.getInitialURL() で拾わないと取りこぼします。

2014年から個人開発で壁紙アプリを作り続けてきましたが、私自身がこれに遭遇したのもその一つでした。自前で addEventListener だけを書いていたため、通知から「アプリが落ちている状態」でタップすると、毎回ホーム画面に着地してしまう。期待した壁紙画面には飛ばないのです。

// Before: ウォーム起動しか拾えず、コールド起動を取りこぼす
import * as Linking from 'expo-linking';
 
useEffect(() => {
  const sub = Linking.addEventListener('url', ({ url }) => handleUrl(url));
  return () => sub.remove();
}, []);
// After: 起動時の初期 URL も明示的に拾う
import * as Linking from 'expo-linking';
 
useEffect(() => {
  // コールド起動: アプリを開いた URL を取得
  Linking.getInitialURL().then((url) => {
    if (url) handleUrl(url);
  });
  // ウォーム起動: 実行中の遷移
  const sub = Linking.addEventListener('url', ({ url }) => handleUrl(url));
  return () => sub.remove();
}, []);

幸い、React Navigation の linking 設定を使う場合は、内部で getInitialURL を読んでくれるため、この二経路は自動的に処理されます。逆に言えば、linking 設定に任せず自前でハンドリングするときだけ、コールド起動を自分で拾う必要がある、と覚えておくと混乱しません。

プッシュ通知から特定の画面へ運ぶ

最後に、冒頭の「通知を開いたらホームに戻される」を解消します。通知のデータ部にディープリンク URL を載せ、タップ時にそれを開くだけです。

// 送信側(サーバー): 通知データに遷移先 URL を含める
const notification = {
  title: "今日の壁紙が届きました",
  body: "新しい空のグラデーションを追加しました",
  data: { url: "myapp://wallpaper/sky-blue" }
};
 
// アプリ側: タップ時に URL を開く
import * as Notifications from 'expo-notifications';
 
Notifications.addNotificationResponseReceivedListener((response) => {
  const url = response.notification.request.content.data?.url;
  if (url) Linking.openURL(url);
});

Linking.openURL を呼べば、先ほど定義した linking 設定が URL を解釈して、そのまま壁紙詳細画面まで運んでくれます。通知の開封率そのものより、開いた後にどこへ着地するかのほうがリテンションに効きます。AdMob の広告収益を支えに個人開発を続けてきた立場としても、離脱を一つ減らすこの設計は、地味ながら効果が大きいと感じています。

次の一歩

まずは URL Scheme だけを設定し、シミュレータのターミナルから npx uri-scheme open "myapp://wallpaper/sky-blue" --ios を実行して、狙った画面に着地するかを確認してみてください。ここが通れば、あとは AASA / assetlinks を整えるだけで Universal Links まで地続きです。実装の参考になれば幸いです。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

⬡ 開発ツール2026-05-20
iOS の 0x8badf00d ウォッチドッグ強制終了に Rork アプリが落ちるときの対処
Rork で作った iOS アプリが起動直後に消える。クラッシュログに 0x8badf00d。Apple のウォッチドッグが React Native の重い初期化を切ったときの正しい直し方を、私の運用中アプリで実際に効いた手順で説明します。
⬡ 開発ツール2026-04-24
Rork アプリのバイナリサイズが 150MB を超えた時に切り分ける 5 つの要因と実践的な削減手順
Rork で生成したアプリが 180MB まで膨らみ、App Store Connect の警告で止まった経験から、原因を 5 要素に分解して 69MB まで落とすまでの手順を、実測値と失敗した箇所も含めて共有します。
⬡ 開発ツール2026-10-15
EXPO_PUBLIC_ の値が、開発では読めて本番だけ undefined になる書き方を、変換結果で見分けます
API の URL が開発中は読めるのに、ビルドした途端に undefined になる。原因は process.env の書き方でした。babel-preset-expo の変換結果を開発・本番で並べ、置き換わる書き方と残ってしまう書き方を表にしました。