0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LP の「近日公開」はいつ嘘になるか — iTunes Lookup API でストア表記のズレを検出する

0
Last updated at Posted at 2026-08-19

Cosoado Lab Blog にも同時掲載予定です(公開でき次第このブロックにリンクを追記します)。

自分でアプリの LP を運用している個人開発者向けの話です。

TL;DR

  • リンク切れ検査で見つかるのは「死んだ URL」だけ。LP の文言とストアの実態がズレていても無風で通る
  • iTunes Lookup API は 1 リクエストで価格・バージョン・公開日・対応言語まで返すので、文言との突き合わせに使える
  • 公式の制限は「約 20 calls / 分」。超えたときの挙動はドキュメントに記載がないが、実際には 429 が返る

点検したら 3 種類のズレが出てきた

止めていた SNS の定期投稿を再開するにあたって、遷移先の LP を一通り見に行きました。広告を出す前に着地点を確認する、程度の軽い気持ちでした。

出てきたのは 3 件です。

1. 存在しない App Store ID を指していた

あるアプリの LP が id6739487979 にリンクしていました。ストアボタンも、JSON-LD の downloadUrl も、apple-itunes-app メタタグも、3 箇所すべて同じ ID。実際に配信されているのは別の ID でした。

2.「近日公開」のまま、アプリは 3 日前から配信されていた

別のアプリの LP には「App Store で近日公開」と書いてありました。ストアボタンは押せますが、飛び先はページ内の #price セクションです。アプリ自体は 3 日前からストアに並んでいました。

HTML の先頭にはご丁寧にこんなコメントまで残っていました。

<!--
  App Store 公開後、次の1コマンドでストアバッジを本番リンクに切り替えてください:
  ...
-->

書いた本人が、公開後にこのコメントを読み返さなかった。

3.「Android 近日公開」と書いてあるが、Play では配信中

トップページの製品カードに「App Store 配信中 / Android 近日公開」。Play の URL を叩いたら 200 でした。

リンク切れ検査では 1 しか見つからない

死んだ App Store ID がどう見えるか確かめました。

$ curl -s -o /dev/null -w '%{http_code}\n' -L \
    'https://apps.apple.com/jp/app/example/id6739487979'
404

1 は 404 なので、ふつうのリンクチェッカで拾えます。拾えないのは 2 と 3 のほうでした。

2 のリンクは #price へのページ内アンカーです。リンクとしては 100% 生きている。「ボタンのラベルが App Store なのに飛び先が自ページ」という状態は、HTTP のレイヤーからは何の異常にも見えません。

3 に至っては、Play へのリンクが存在しないことが問題です。「無いリンク」はチェッカの守備範囲外です。

つまり、URL の死活だけを見ていると、LP が事実と食い違っている状態は永久に検出されません。しかも自分の LP のストアボタンは、自分では押さない。壊れていても気づく機会がない。

Lookup API は実態を返してくれる

iTunes Search API の lookup エンドポイントは、ID を渡すと現在の実態を返します。

curl -s 'https://itunes.apple.com/lookup?id=6756719838&country=jp'
{
  "resultCount": 1,
  "results": [
    {
      "trackName": "謎かけメーカー",
      "formattedPrice": "無料",
      "version": "1.4.4",
      "currentVersionReleaseDate": "2026-08-03T15:10:36Z",
      "languageCodesISO2A": ["EN"],
      "minimumOsVersion": "15.0",
      "trackContentRating": "4+"
    }
  ]
}

存在しない ID なら resultCount0 になります。ここが判定の芯です。

判定したいこと 使うフィールド
ID が生きているか resultCount
「無料」表記が今も正しいか formattedPrice
「近日公開」が嘘になっていないか resultCount が 1 なら公開済み
「日英対応」等の表記が正しいか languageCodesISO2A
対応 OS の表記が古くないか minimumOsVersion

404 チェックでは絶対に取れない情報が、同じ 1 リクエストで返ってきます。

LP の文言と突き合わせる

やっていることは単純で、LP の HTML に含まれる文言と、Lookup の応答を照合するだけです。

const CHECKS = [
  { id: '6756719838', lp: 'https://example.com/app-a/' },
  { id: '6751740564', lp: 'https://example.com/app-b/' },
];

async function lookup(id) {
  const res = await fetch(`https://itunes.apple.com/lookup?id=${id}&country=jp`);
  if (res.status === 429) throw new Error('rate limited');
  const { resultCount, results } = await res.json();
  return resultCount === 0 ? null : results[0];
}

for (const { id, lp } of CHECKS) {
  const app = await lookup(id);
  const html = await (await fetch(lp)).text();

  if (!app) {
    console.error(`[dead] ${lp} が存在しない ID ${id} を指している`);
    continue;
  }
  if (/近日公開|まもなく公開|coming soon/i.test(html)) {
    console.error(`[stale] ${lp} が「近日公開」のまま。実際は公開済み (${app.version})`);
  }
  if (/無料/.test(html) && app.formattedPrice !== '無料') {
    console.error(`[price] ${lp} が「無料」表記。実際は ${app.formattedPrice}`);
  }
  await new Promise(r => setTimeout(r, 3500)); // 約 20 req/min の制限に合わせる
}

「近日公開」という語が LP に残っているのに resultCount が 1、という条件だけで、上の 2 番目は落とせます。文字列マッチなので雑ですが、雑でも人間の目視より確実に動きます。半年に一度しか読まない HTML の、しかも自分で書いたコメントを信用するよりはるかにましでした。

制限は約 20 calls / 分。超えると 429

叩きすぎました。公式ドキュメントにはこう書いてあります。

The Search API is limited to approximately 20 calls per minute (subject to change).
Search API — Apple Performance Partners

超えたときの挙動については、ドキュメントに記載がありません。実際に検証中に何度も叩いていたら、まず apps.apple.com 側が 429 を返しはじめました。数十秒待てば戻ります。

lookup エンドポイント自体で 429 を踏んだわけではないので、上のコードの res.status === 429 は保険です。制限が「約」としか書かれていない以上、踏んでから考えるより先に置いておくほうが安いと判断しました。

なので、上のスクリプトではリクエスト間に 3.5 秒のスリープを入れています。20 req/min = 3 秒間隔なので、少し余裕を持たせた値です。アプリが 10 本あっても 35 秒で終わるので、CI に置いても邪魔になりません。

まとめ

  • リンクチェッカが見るのは URL の死活だけ。「ラベルは App Store なのに飛び先が自ページ」は正常として通る
  • LP の文言とストアの実態の照合には Lookup API が要る。resultCount / formattedPrice / languageCodesISO2A あたりで足りる
  • 「近日公開」という文字列が LP に残っているのに resultCount が 1、という条件だけで実害の大きいズレが取れる
  • 制限は約 20 calls / 分。ドキュメントには書いていないが超えると 429。3.5 秒スリープで十分

自分の LP のストアボタンは自分では押しません。押さないものは壊れていても気づけないので、機械に押させるのが結局いちばん安上がりでした。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?