Universal Link はどのパスで開くのか — AASA マッチの落とし穴
前回は Universal Link が開かない原因を、受け口から AASA まで疑う順に並べました。今回はその一段だけ深いところ、入力 URL が AASA のどのルールにマッチするのかを扱います。
きっかけは、検証アプリを実機に入れて試していて踏んだ違和感でした。
開くのに「扱うアプリはありません」
あるサービスのリンクを Safari で踏むと、アプリが開きます。UL は効いている。ところが同じドメインを AASA インスペクタで見ると「このURLを扱うアプリはありません」と出ました。
ツールのバグを疑いましたが、結論から言うとツールが正しく、こちらの読みが甘かった。順に見ていきます。
そのドメインの AASA(CDN 版)はこうなっていました。ドメイン名は伏せます。
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.example.app",
"paths": ["/app_store/", "/reserve/"]
}
]
}
}
そして私がインスペクタに入れていたのは、ドメインのルート https://example.com でした。ルートのパスは /。この AASA が主張しているパスは /app_store/ と /reserve/ の 2 つだけで、/ はどちらにも当たりません。だから「対象外」で正しい。
ドメインが UL 対応 ≠ どの URL でも開く
ここが最初の思い込みでした。「このアプリは UL 対応している」を、つい「このドメインのどの URL でもアプリが開く」と読んでしまう。実際は違います。
UL が成立するのは、AASA が paths/components で明示的に claim したパスだけです。ルートも、claim されていない任意のページも、対応アプリが入っていても Safari に落ちます。claim されたパスの集合がすべてで、そこに無い URL は「UL ではない普通の https URL」として扱われます。
私が「開く」と思っていたリンクは、実際にはルートではなく /reserve/… のページ(かそこへのリダイレクト先)だったわけです。ルート直打ちでは、この AASA では開きません。
レガシー paths は完全一致
もう一段。上の AASA には、実は落とし穴がもう一つ隠れています。/reserve/ というパターンは、パスがちょうど /reserve/ のときだけマッチします。
paths のパターンは前方一致でも部分一致でもありません。*(0 文字以上)と ?(ちょうど 1 文字)を使ったグロブで、パス全体に対して評価されます。ワイルドカードが無ければ、書いた文字列と完全に一致したときだけ当たります。
パターン: /reserve/
/reserve/ → マッチ
/reserve/12345 → 非マッチ(末尾に * が無い)
/reserve → 非マッチ(末尾スラッシュまで含めて一致が要る)
つまりこの AASA は、/reserve/ ちょうどのページしか UL にしていません。/reserve/店舗ID/ のような実際の予約ページを開かせたいなら、"/reserve/*" と書く必要があります。意図してこうしているのか、* の付け忘れなのかは外からは判断できませんが、挙動は AASA に書いた通りにしか決まりません。ツールが「対象外」と言うのは、ここまで含めて正しかったわけです。
正規表現ではない点も注意です。. や + はメタ文字ではなくただの文字で、使えるのは * と ? だけ。レガシー paths が見るのはパスだけで、クエリやフラグメントは対象外、既定で大文字小文字を区別します。
components は「先頭一致」で決まる
モダンな components 形式は配列で、iOS は上から順に評価し、最初にマッチしたルールで結果を確定します(first match wins)。だから並び順が意味を持ちます。
典型は、除外を先に、包括を後に置く形です。
"components": [
{ "/": "/help/*", "exclude": true },
{ "/": "/*" }
]
/help/faq は最初の exclude ルールに当たって「このリンクは扱わない」で確定します。後ろの /* も形の上では /help/faq にマッチしますが、先頭一致で既に決着しているので効きません。もしこの 2 つを逆順に書くと、/help/* は前に来た /* に食われてしまい、除外したかったページでアプリが開きます。
components は path だけでなく ?(クエリ)や #(フラグメント)も条件にでき、指定したキーはすべて満たす必要があります。caseSensitive の既定は true です。前回触れた「components キーがあると、たとえ空配列でも paths は無視される」も、この先頭一致の延長にある挙動です。
「どのパターンで決まったか」を見る
マッチの有無だけ分かっても、なぜ非マッチなのかは分かりません。デバッグで欲しいのは、全パターンを並べて、どれがマッチし、どれが結果を決めたかです。
素の手段なら、AASA を CDN から取って(前回参照)、パスを目視で当てていきます。数が少なければこれで足りますが、components が長く first match の順番が絡むと、追うのがだんだん厳しくなります。
Schemely の AASA インスペクタは、details を開くと全パターンを列挙して、各行に マッチ / 非マッチ / 除外 と、先頭一致で結果を決めた「効いたルール」を出します。冒頭の例なら、/app_store/ 非マッチ・/reserve/ 非マッチ → どのルールも当たらない、が一目で分かります。「開くはずなのに」で止まったとき、思い込みとパターンのどちらがずれているかを、並べて確かめられます。
まとめ
- ドメインが UL 対応でも、開くのは AASA が claim したパスだけ。ルート ≠ 対象。
- レガシー
pathsは完全一致。/reserve/は/reserve/123に当たらない。サブパスまで開かせるなら*が要る。 componentsは先頭一致(first match wins)。excludeは並び順で効き方が変わる。- 「開くのに開かない(逆も)」の多くは、
paths/componentsのマッチを誤読しているだけ。全パターンを並べて、どれが効いたルールかを見れば早い。
AASA は「書いた通り」に淡々とマッチします。人間の期待とずれるのは、たいてい * 一つの有無か、並び順です。