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?

BDSM向け16診断の共有URLが長すぎたので、パラメータを削って短くした話【旧URLも読める後方互換つき】

0
Posted at

TL;DR

  • BDSM向けの16タイプ診断を作り、Xへの共有投稿を実装したところ、共有URLの?r=パラメータが長すぎることに気づいた
  • JSONをBase64にした形式(約136文字)から、独自の区切り文字列(約35文字)に変更して、パラメータを削った
  • 削るポイントは、①URLのパスと重複する値を持たない、②JSON+Base64をやめる、③小数を整数にする、の3つ
  • 見落としやすい点:URLを受け取って解析する別アプリが、旧形式しか読めなかった
  • 旧形式も読める状態で、受け取る側→発行する側の順にデプロイして、無事に移行できた

何を作っているの?

私は、SM・BDSM領域に向けたサービス「Kinkysエコシステム」を、個人で開発・運営しています。
マッチングアプリ(Nuxt + Firebase)、SNSアプリ、お店のデータベース、メディアなどがあり、その入口のひとつとして、BDSM向けの16タイプ診断を作りました(Next.jsの静的エクスポート)。

30問に答えると、16種類の「役職」と、スタンス(主導/対等/委ねる)などが出ます。結果は共有URLにでき、SNSでシェアできます。

今回は、このシェア機能を作ったときに、共有URLのパラメータが長すぎることに気づき、パラメータを削って短くするために奮闘した過程を、時系列で書きます。

1. Xシェアを実装したら、なんとURLが長すぎた!?

診断結果画面に「Xでシェア」ボタンを付けました。投稿文は、次のような作りです。

{名前}のBDSM 16役職診断の結果は「{役職名}」でした。{役職の説明文}
{共有URL}
#Kinkyer #BDSM16タイプ診断 #{役職別ハッシュタグ×3}

ボタンを押すと、Xの投稿画面(https://twitter.com/intent/tweet?text=...)が開きます。ローカルで試したところ、文字数の上限をすぐに超えてしまい、投稿できませんでした。

今思えば実装中に気づいてもよかったはずなのに、共有URLだけで文字数超えるって、、、

共有URLは、結果をJSONにしてBase64URLにした ?r=... を持っていて、これが約136文字ありました。URLの大半が、このパラメータです。

2. 何を削れば短くなるのか

Xの投稿は課金した人であれば上限は増えますが、無料で利用している人たちは上限があります。

ただ、これは文字数の問題だけでなく、長いパラメータそのものが無駄だと感じました。LINEなどに貼る場面では、URLの長さがそのまま見た目になりますし、静的サイトなので、サーバー側でURLを短縮する仕組みも持てません。

そこで、投稿文の見直しと並行して、パラメータ自体を削ることにしました。

3. 対策を考える

URLと投稿文を、次のように整理しました。

案 判断
外部のURL短縮サービスを使う 不採用。リダイレクトが1段増え、第三者に依存する。Xでは、URLが長さに関係なく23文字扱いなので、効果も薄い。静的サイトなので、自前の短縮も難しい
ハッシュタグを減らす 採用。5個→2個(#BDSM16タイプ診断 と、役職別の1つ目)
投稿文を短くする 採用。役職の説明文を外し、ハッシュタグを改行で並べる。末尾の「@アカウント より」(via)も外す
?r= のパラメータ自体を短くする 採用。URLの無駄を減らし、LINEなどに貼るときの見た目も短くなる

最後の、パラメータを短くするのが、今回のメインの作業です。

4. パラメータを短くする

変更前

const payload = {
  p: "9",              // 主役職
  sv: -12.5,           // スタンスのスコア
  av: 9.68,            // 主導のスコア
  smv: 0,              // SM軸のスコア
  rr: [["9", 100], ["1", 98.35], ["13", 91.2], ["2", 88.02], ["7", 80.5]] // 上位5件
};
const r = base64url(JSON.stringify(payload)); // 約136文字

短縮のポイント

  1. p(主役職)を削除:URLのパス(/share/{役職}/)に、すでに役職が入っている
  2. JSON+Base64をやめて、区切り文字列にする:Base64は、元のデータの約1.33倍に膨らむ
  3. 小数を整数に丸める
// 例: -12_10_0_9-100_1-98_13-91_2-88_7-81
//     スタンス_主導_SM軸_役職キー-スコア_役職キー-スコア…
export function encodeCompactShare({ sv, av, smv, rr }) {
  const head = [sv, av, smv].map((v) => Math.round(Number(v) || 0));
  const pairs = rr.map(([key, score]) => `${key}-${Math.round(Number(score) || 0)}`);
  return [...head, ...pairs].join("_");
}

結果、?r= は136文字から35文字になりました(上の例)。

整数への丸めと、表示のズレ

小数を整数に丸めると、共有ページの表示が、本人の結果画面(小数2桁)と、1%未満ズレます。最初は、小数を残す形式も検討しました。

しかし、**「最初から、表示も整数に統一すればよいのでは」**という案に至りました。内部のスコア計算は小数2桁のまま、画面の表示とURLに入れる値だけを整数にすると、本人の画面と共有ページの数字が一致し、URLも短いまま済みます。

5. 旧URLも読めるようにする

すでにSNSなどで共有された、旧形式のURLが壊れては困ります。そこで、新旧を確実に判別できることを条件にしました。

  • 旧形式(JSONのBase64)は、{" をBase64にした eyJ で必ず始まる
  • 新形式は、数字か - で始まり、正規表現で厳密に判定できる
const COMPACT_SHARE_PATTERN = /^-?\d+_-?\d+_-?\d+(_\d+-\d+)+$/;

export function decodeCompactShare(value) {
  if (typeof value !== "string" || !COMPACT_SHARE_PATTERN.test(value)) {
    return null; // 新形式ではない
  }
  const [sv, av, smv, ...pairs] = value.split("_");
  return {
    sv: Number(sv), av: Number(av), smv: Number(smv),
    rr: pairs.map((p) => { const [key, score] = p.split("-"); return [key, Number(score)]; }),
  };
}

// 新形式を先に試し、ダメなら旧形式
const payload = decodeCompactShare(raw) || decodeBase64Url(raw, null);

新形式は p を持たないので、主役職はパスから復元します。

6. 見落としやすい点:URLを受け取る側のアプリ

診断サイトのローカル確認が済み、デプロイの準備ができた段階で、確認すべき点に気づきました。

「URLのパラメータが変わるので、連携先のアプリ側も対応が必要では?」

Kinkysエコシステムには、共有URLを、マッチングアプリ(Nuxt + Firebase)のプロフィール画面に貼ると、診断結果を取り込める連携があります。そのアプリは、URLを検証して ?r= を旧形式(Base64のJSON)としてしか読めません。このままデプロイすると、新しく発行されたURLは、「URLの形式を確認してください」と弾かれ、診断結果を取り込めなくなります。

発行する側の実装に集中していると、受け取る側の存在が計画から抜けやすいです。AIに実装を任せる場合は、「指示した範囲」が正確に作られるぶん、影響範囲の洗い出しは、人間が意識して確認する必要があると感じました。

受け取る側(TypeScript)の対応

同じ判定を、受け取る側にも入れました。

const COMPACT_RESULT_PATTERN = /^-?\d+_-?\d+_-?\d+(_\d+-\d+)+$/

const payload = getCompactPayload(rawResult) || getBase64UrlJson(rawResult)

// 新形式は p を持たないので、パスの役職キーから復元する
const role = String(payload.p || getRoleIdByKey(pathMatch[1]))

既存の検証は、そのまま残しました。

  • スコアの範囲(-100〜100、0〜100)
  • 上位5件まで、重複なし
  • パスの役職と、先頭の役職が一致すること(新形式では、取り違えを防ぐ役割も持つ)

テストも追加しました(短縮形式の復元、役職の不一致、範囲外・6件以上・重複・形式違いの拒否)。

7. 検証:2つのリポジトリをまたいだ結合テスト

別々のリポジトリで実装したので、「発行側が作ったURLを、受け取る側が読めるか」を、発行側の実際のスコア計算コードと、受け取る側の解析関数を組み合わせて確認しました。

// 一時的な結合テスト(コミットなし)
for (let i = 0; i < 2000; i++) {
  const answers = randomAnswers();                      // ランダムな回答
  const result = calculateResult(definition, answers);  // 発行側の実コード
  const url = `.../share/${result.primary}/?r=${encodeCompactShare(payload)}`;
  const parsed = parseResultUrl(url);                   // 受け取る側の実コード
  expect(parsed.role).toBe(expectedRoleKey);
  expect(parsed.stance).toBe(Math.round(result.stance.score));
}

ランダム回答2,000件で、16役職すべてについて、主役職とスタンスが一致しました。すでに連携済みのユーザーが保存していた旧形式のURLも、更新後の解析で読めることを、保存データ(読み取りのみ)で確認しています。

?r= の長さは、例のペイロード(上位5件)で、136文字から35文字になりました。

8. デプロイの順序

順 対象 理由
1 受け取る側(新旧両対応) 新URLが発行される前に、読める状態にする
2 発行する側(新形式に切り替え) 受け取る側が準備できてから

逆にすると、新URLが読めない期間ができます。 旧URLは、どちらの順でも読めます。

最後に:16診断と、Kinkysエコシステムの紹介

ここまで読んでくださった方向けに、今回の舞台になったサービスを紹介します。

BDSM向け16タイプ診断

BDSMの世界での自分の傾向を、言葉にするための無料の診断です。

  • 30問に、5段階(はい/たぶんそう/どちらでもない/たぶんちがう/いいえ)で答えます
  • 結果は、16種類の「役職」(支配者、緊縛師、奴隷志願者、ロープバニー、リトルなど)で出ます
  • 役職のほかに、スタンス(Lead/Even/Yield=主導/対等/委ねる)、主導(Top/Middle/Bottom)、SM軸(Sadistic/Masochistic/Versatile)も出ます。1つの肩書きに決めるのではなく、複数の軸で傾向を見る作りです
  • 結果は、画像カードの保存、リンクのコピー、Xでのシェアができます(今回の記事の共有URLです)
  • 結果の共有URLは、Kinkyerのプロフィールに貼って取り込むこともできます(今回、受け取る側のアプリとして出てきたのが、このKinkyerです)

Kinkysエコシステム

私は、SM・BDSM・フェチなどの「界隈」向けのサービス群を、個人で開発・運営しています。診断は、その入口のひとつです。

Kinkyer(マッチングアプリ)

SM特化のSNS型マッチングアプリです(Nuxt 3 + Firebase)。

  • 男女とも基本無料です。有料プランは、データの保存期間を延ばすためのものです
  • 年齢確認(本人確認書類の提出)を、男女とも行っています
  • 受け取る「いいね」を、年齢などの条件で絞り込めるフィルタリングがあります
  • マッチング前でも、相手が許可した場合のみ、メッセージを送れます(送信数に上限あり)。性癖が合っても、話してみると違うことがよくあるため、ミスマッチを減らすための仕組みです
  • 行きつけのお店をプロフィールに登録でき、マッチ後のお店探しにも使えます

Kinky'up(界隈向けSNSアプリ)

iOS/Androidの、界隈に特化したSNSアプリです(Flutter + Supabase)。きっかけは、Xの凍結が相次いだことで、凍結リスクの低い場所が欲しいと思ったことでした。

  • イベント管理:主催者はイベントを作成でき、参加者はイベントへの参加と同時に、グループチャットへ自動で追加されます
  • アイテム管理:道具を作っている人のポートフォリオページです
  • 出勤投稿:1つの投稿で、今月の出勤情報をまとめられます
  • Web名刺:プロフィールのURLを1つ渡すだけで、投稿やイベントを紐付けて見せられます

そのほか

診断で自分の傾向を言葉にして、Kinkyerで相手を探し、Kinky'upで界隈とつながる。そんな流れを目指して、少しずつ作っています。興味を持った方は、まず診断を試してみてください。

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?