ZOZOTOWNのURLを生成するツールを作った話

ZOZOTOWNのURLを生成するツールを作った話

はじめに

こんにちは、ZOZOTOWN企画開発部・企画フロントエンド1ブロックのゾイです。

ZOZOTOWNトップでは、セール訴求や新作アイテム訴求、未出店ブランドの期間限定ポップアップ、著名人コラボなどの企画イベントが毎日何かしら打ち出されています。私はそのプラットフォームとなる企画LPをメインに実装するチームに在籍しています。

今まで実装した企画LPをアーカイブするプロジェクトにも参加しているので、チームの特性や企画LPの詳細は下記の記事をご覧いただければと思います!

techblog.zozo.com

目次

背景・課題

ZOZOTOWNではカルセールバナーなど、アプリとWeb共通で表示させるコンテンツにはURLを分ける必要があります。

例えば、レディーズのトップス商品一覧がある検索結果に遷移したい場合、Webでは以下のようなURLを指定する必要があります。

https://zozo.jp/women-category/tops/

対して、アプリでは以下のようなスキームを指定する必要があります。

zozotown://index/search?view=result&p_cutyid=2&p_tycid=101

上記のようなURLは施策担当の方が下記の方法で作成し、カルセールバナーやLP内の遷移先に利用することが一般的なフローです。

  • Web:検索結果のページで絞り込み条件を手動で入れて発行する
  • アプリ:既存のアプリスキーム生成ツールで作る

今まではそれぞれ違う方法でURLを作成していたため、下記のような問題がありました。

  • WebのURLでの絞り込み条件とアプリのURLでの絞り込み条件が一致しない
  • URL生成に慣れてない部署やチームは作り方に迷うため、PMの負担が高まる

課題を解決するためPMと相談した結果、アプリとWeb用のURLを同時に生成できる「URL生成ツール」を実装することになりました。

仕様・技術選定

  1. 検索ページ用のURL生成
  2. 商品詳細ページ用のURL生成
  3. アプリ上でLPを確認できるQRコード生成
  4. 外部URL遷移用のアプリスキーム生成

上記の機能をミニマムで盛り込み、デザイナー・PM・事業部・エンジニア等、企画に関わる多方面のスタッフにとって嬉しいツールを目指します。今回の記事では主に「1. 検索ページ用のURL生成」の実装方法について紹介したいと思います。

検索ページ用のURL生成画面

アプリのようにサクサク動くPWA的なサイトにしたかったため、全ての処理がCSRで実現できることを意識しました。そのため、Next.jsMUIEmotionzodなどを採用しました。

Googleなどで利用されておりUI的に一番親近感もあってEmotionとも相性が良いため、MUIを採用しました。また、検索ページ用のURL生成には入力フォームが多いため、ユーザーが誤って間違えた値を入力できないよう、エラー検知用にzodを採用しました。

他にもqrcode.reactencoding-japaneseなどのようなヘルパーパッケージを導入しました。

1. 型の定義と入力データのバリデーション

まず、検索ページで利用しているパラメーターを調査し、許可されない値の入力を防ぐ対応を入れます。

下記は「価格(から)」が必ず「価格(まで)」より小さい数字にさせる対応の例です。

入力フォーム

const priceSchema = z
.object({
  priceFrom: z.union([z.literal(''), z.number().min(0)]),
  priceTo: z.union([z.literal(''), z.number().min(0)]),
})
.refine((schema) => {
  return (
    schema.priceFrom === '' ||
    schema.priceTo === '' ||
    (schema.priceFrom && schema.priceTo && schema.priceFrom < schema.priceTo)
  )
}, 'PriceFrom must be less than PriceTo')

上記のような制限をすべて含めて、以下のような型をzodで定義します。

export const schema = z
  .object({
    // ...省略
    gender: z.number(),
    couponFilter: z.boolean(),
    discountRate: z.number().min(1).max(100).optional(),
  })
  .merge(priceSchema.innerType())

2. 入力フォームの実装

次に、入力フォームを実装します。CSRでURLを生成できるよう、入力フォームの状態をReactのContextを共有する形で実装していきます。入力フォームにはMUIのAutoCompleteFormControlTextFieldを採用しました。特にAutoCompleteはアルファベット順のソートや、検索機能もあるためブランド一覧の表示にとても便利です。

入力フォームは以下を考慮して実装しました。

  • onChange時(ユーザーが何かを入力した時)に、Contextを更新する
  • 入力項目に誤りがあったらエラーUIにさせる
  • ショップなど、Webでは名前、アプリではIDを利用する項目が多いため、データ属性に両方指定する
  • 同時に絞り込みきない項目や必要な項目がない場合、非アクティブ状態にする

以下は「価格(から)」のフォームの実装例です。

入力フォーム

export const PriceFrom = () => {
  // コンテキスト
  const { searchParamsValue, setSearchParamsValue } = useContext(SearchContext)

  const { priceFrom, ...rest } = searchParamsValue

  const [hasError, setHasError] = useState(false)

  // ユーザーが入力したら値を確認し、問題なかったらコンテキストを更新する
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const input = parseInt(e.target.value)

    const result = priceSchema.safeParse({
      priceFrom: input,
      priceTo: searchParamsValue.priceTo,
    })

    if (!result.success) setHasError(true)
    else {
      setHasError(false)
      setSearchParamsValue({ priceFrom: input, ...rest })
    }
  }

  return (
    <TextField
      label={
        hasError
          ? ${searchParamsValue.priceTo}以下の数字を入力してください`
          : '価格(から)'
      }
      type="number"
      variant="standard"
      onChange={handleChange}
      autoComplete="off"
      error={hasError}
      sx={{ width: 170 }}
      InputProps={{
        startAdornment: <InputAdornment position="start">&yen;</InputAdornment>,
      }}
    />
  )
}

3. URLの生成

最後に、URLを生成します。

まず、アプリとWebによって違う値を利用する項目があるため値を変換する対応を入れます。例えば、キーワード絞り込みはアプリではUTF-8でエンコードされた値を、WebではShift_JISにエンコードされた値を利用します。

export const transformValues = (
  searchParameters: SearchParameters,
  platform: 'web' | 'app',
): TransformedValues => {
  const isApp = platform === 'app'

  const { keyword } = searchParameters

  const transformedValues: TransformedValues = {
    // ...省略
    keyword: isApp ? encodeURI(String(keyword)) : encodeToShitJis(String(keyword)),
  }

  return transformedValues
}

次に、アプリとWebのURLを生成する関数を実装します。アプリのURLは全てのパラメーターを繋ぎこむだけで済みますが、Webの場合はZOZOTOWNのドメイン知識が必要なため、もう少し複雑な処理が入っています。そのため、今回の記事では一般的に利用できる、アプリのURLを生成する関数だけ紹介したいと思います。

export const composeAppUrl = (value: SearchParameters) => {
  if (value === DEFAULT_SEARCH_PARAMS) return '検索条件を入力してください'

  // コンテキストの値をアプリ用に変換する
  const transformedValues = transformValues(value, 'app')

  const params = {
    ...value,
    ...transformedValues,
  }

  // queryStringの作成
  const queryString = Object.entries(params)
    .reduce((acc, [key, val]) => {
      if (!val) return acc
      return acc
    }, [] as string[])
    .filter(Boolean)
    .join('&')

  // queryStringとpathを繋ぐ
  return appendQueryStringToPath(ZOZOTOWN_APP_URL.search, queryString)
}

上記で生成したURLを表示させたら完成です!

URL生成の結果

ユーザーの反応

URL生成ツールを利用していただいている、事業部やPMの方々から以下のようなご意見をいただきました!

  • 複数人で同時に使用できるようになった
  • アプリスキームだけではなくPC/SPのURLも確認できるのがありがたい
  • プルダウン式で項目を選べるのがとても助かるし使いやすい
  • 既存のアプリスキーム作成ツールでは、条件を入力し作成ボタンを押した後、数秒間待つ時間があるのに対し、ZOZO URL GENERATORでは作成結果が即時反映されて嬉しい
  • 各項目のショップIDやブランドIDなどIDを逐一調べず抽出でき工数削減につながった

URL生成ツールを通して様々な部署の工数を削減できてよかったと思います。

終わりに

本記事ではURL生成ツールを紹介しました。URL生成ツールの導入によってサイトバナーの管理プロセスや、LPの開発フローを改善できて良かったと思います。

株式会社ZOZOでは、アイデア次第でこんなふうに自由度の高い開発を経験できる環境が整っています! ご興味のある方はぜひ、ご応募お待ちしております!

corp.zozo.com

カテゴリー