【BASEテンプレート作成】
デザインマーケットで
「camera」を販売するまで

※2020年12月現在、弊社は「camera」含め7つのテーマの審査をクリアして販売に至っており、現在も新しいテーマを申請中ですが、この記事の執筆以降、BASEはどんどん進化しており、新しいルールの追加やデザイン編集画面のフルリニューアルがあったり、より便利なBASE Appsが次々に登場しているため、テーマ作成するための工数が執筆時よりも増えました。執筆時より間違いなく審査のハードルは上がっています。そのためこちらの記事に書いているコードや修正点の情報はほんの一部であり、また情報としても古くなっている可能性がありますので、今後テーマ申請をお考えの方はあくまでも参考程度にお読みください。

BASEテンプレートって?

手軽にネットショップを解説できるBASEでショップのテンプレートを自由に編集できる機能のことです。この機能を使ってオリジナルのテーマを作ることができます。

また、作成したオリジナルテンプレートはテーマ申請して審査が通ればデザインマーケットで1テーマ5000円からで販売することができます。
販売手数料は売上の30%。製作者には70%が支払われるそうです。

今回、弊社で作ったテーマはこちらです!

camera | BASE デザインマーケット ネットショップのデザインをもっと自由に

https://design.thebase.in/detail/54

テーマの使い方などの詳細は別途LPを用意しました。
https://sharesl.net/base/camera/

最初はシンプルなデザインなのですぐ通ると思っていたのですが…
全然通らず8回目の申請でやっと通りました笑

今回はこちらをテーマ申請する際に苦労した点や、注意点を忘れないようにメモしておきたいと思います。

まずはアカウントを取得

BASEは普通にHTMLとCSSをただ書ければ良いというわけではなく、独自のテンプレートタグを使ったルールなどが細かく決まっていたり、「HTML編集App」というBASEの管理画面内で使えるエディタから更新する必要があったりしますので、アカウントがないと開発ができません。
BASE Developersからアカウント登録してはじめましょう。

ドキュメントを読む

BASE Templateには公式ドキュメントがあります。

テーマの作り方に関して細かく丁寧にこちらのドキュメントに書いてあるので、しっかり読めば基本的なところは詰まることなく作れると思います。

こんなわかりやすいドキュメントがあるなら簡単やん!
そう思ったのは僕だけじゃないはずです。

実際、僕自身も申請するまではそんな詰まることもなくちゃちゃっと作れて、すぐにできたー!って感じでした。

テーマ申請・審査

苦労したのはここからです。

はっきり言ってドキュメントを読み込んだだけでは審査は通りません。BASEさんはテーマに高いクオリティを求めているため審査がめちゃ厳しいです。

申請が却下されるとありがたいことに修正箇所をフィードバックしてもらえるのですが、1回目のフィードバック時は20項目ほどの修正が・・・。

『まぁ初めてだしこんなもんかな、想定内、想定内』

と強がりながら全部修正して再申請すると、またも却下。
しかも今度は別の修正箇所が30項目以上増えている・・・

『なんぞ!?どういうことや!?』

と心の中で発狂しそうになりましたが、ここはBASEさんのルールに従う以外に道はありません。そのため修正しては再申請し、また却下され新しい修正項目がフィードバックで返ってくる、という先の見えない無限ループのようなやりとりを約1ヶ月、計8回繰り返しました。笑

苦労してわかった注意点

前置きが長くなりましたがここからが本題です。
たくさんフィードバック修正をいただいたので、申請時にどこに気をつけると良いかわかってきました。順番にメモしていきます。

デザインガイドライン・チェックリストをしっかりと守る

まずドキュメントに載っているこちらを頭に入れておくことが最も大事です。

デザインガイドライン

https://docs.thebase.in/docs/template/guideline/

チェックリスト

https://docs.thebase.in/docs/template/guideline/check_list

とは言ってもデザインガイドラインについての説明はかなり抽象的です。
良い例・悪い例といった具体例がないので、実際に審査にかけないとどこがひっかかるかという判断は難しい印象です。
↑2020年6月現在、チェックリストの「よくあるNG事例・注意事項」に具体的に示されています。

では「camera」申請時によく指摘された部分について、どういう修正を行ったかここから書いていきます。

IE11対応を完璧にする

これはかなり厳しく言われました。

※2020/11/15以降、BASEがIE11のサポートを終了したのでIE11対応は現在必要ありません。(Edgeは対応が必要です

開発環境がMacだったこともあり最初の申請時にIEについてはあまり見れていませんでした。言い訳。笑
VirtualBoxを使ってWindowsを動かし細かくチェック・修正するようにして対応しました。

特にCSS周りは、最近はスマホのモダンブラウザが主流なこともありobject-fitFlexboxなど最新のCSSを使うようになりましたので、この辺りのpolyfill忘れやIEのみのバグ修正などがよくありました。あとはselectタグのCSSハックなど。

また、調子に乗ってVue.js入れたれ!と思って商品リストやブログリストをコンポーネント化して作ったのですが、IE11でめちゃ崩れるという事態に。
そして結局対応がめんどくさすぎて一からjQueryでやり直すハメに・・・笑

jQueryが必須タグになっているので初めからjQueryで書けば良かったと思って後悔しました。

ただそのjQueryに関しても問題がありました。BASEのテンプレートタグでjQuery3.x系を使える(2019年6月当時)ので最初はそれを使いました。しかし、もともとBASE側に備わっているフロント側の機能が古いjQueryのメソッドを使っているため、エラーを吐いてしまう!こんな時はjQuery Migrateを追加していっちょあがり!と思ってテーマ申請したのですが・・・

migrateMuteがtrueになっていません

という修正が返って来ました。
これはjQuery MigrateがJQMIGRATE: Logging is activeというメッセージログを勝手にコンソールに出してしまうんですよね。調べてみてmigrateMuteをtrueにしたところ、エラーログや警告は出ないようにできましたが最終的には上記のメッセージを完全に出力しないようにできませんでした。

そのため結局は3.x系は捨てて、jQuery1.x系を読み込むことに。
最初から普通にこうしてれば良かったです。笑
もちろんjQuery1.x系はIE11をサポートしているので安心です。(ES6を使う場合はBabelでトランスパイル対応が必要)

これからテンプレート作成する方は、jQuery3.x系は使わずにjQuery1.x系を使いましょう

スライドの設置対応

商品詳細ページに商品画像のスライドを置くことがあると思います。
その場合、1枚しか画像が入っていない場合はスライドさせないようにしてください画像が入っていない場合はテンプレートタグで用意されているNo Image画像を入れてください、といったところが指摘されました。

普段ECサイトを作ることがあまりないのでそこまで気が回っていませんでしたが、重要な部分です。このブログでも紹介したSwiperでスライドを作ったのですが、ループモードに設定していると画像1枚でも勝手に複製されてスライドが動いてしまいます。そのため画像が2枚以上の時のみ適用させるように修正しました。

余白や幅・高さを合わせる

審査ではきっちり揃ったデザインを要求されます。
特に意図のわかりにくい余白の使い方をしている場合に指摘されました。
例えば、「camera」のテーマでは最初、コンテンツによって左右の幅が一致せずにバラバラになっていました。

こういうものはデザイナーが作る時に意味を持ってやっていたとしてもBASE側の審査基準としてはアウトらしく、すべて指摘されます。

基本的にBASEとは申請とフィードバックのやりとりしかありませんので指摘されたことはどんなに意味があっても反論するだけ無駄ですので、従って修正するしかありません。

ただ、表示崩れのバグなどあきらかにミスとわかりやすい指摘は納得できますが、こういう指摘は正解のないデザインの部分になるのでモヤモヤしながらも修正対応しました。

他にも「上下の余白がここだけ狭いので他の部分と合わせてください」といった修正もあり、かなり全体の調和を求められます。デザインガイドラインではすごく端的な説明ですが、一度申請して修正をもらってようやく、そういうことね、と納得できました。そこから余白はデザイナーの作ったデザインをBASE側のガイドラインに合うようにこっそり変更して再申請しました。

また、よくひっかかりそうな修正として例をあげると、今回のような商品リストをカード型のグリッドにした場合、コンテンツの高さが揃っていない以下のような状態は修正として指摘されます

画像サイズやコンテンツの長さによって高さがずれるとアウト

そのためカード型リストの場合は画像の高さを指定しておく必要があります。また今回のように3カラムや2カラムになる場合はfloatで段組みすると高さのずれに対応できずにJSで調整しなくてはならなくなるので、CSS Gridシステムを使うか、Flexboxを使うかどちらかで対応した方が楽です。Flexboxはブラウザごとに多少のバグ修正がありますが、IE11でも使えるのでオススメです。

文字やボタンの視認性や強調

Webサイト制作では基本的なことではありますが、ある程度のコントラストを保っていなければ文字やボタンを認識しづらくなります。

BASEのテーマでは背景色や文字色などを変更できるようにデザインオプションをテンプレート内で独自に定義してカスタマイズ性を高めることができます。

BASE Template デザインオプション

https://docs.thebase.in/docs/template/syntax/designoption

最初にこれを使っていろんな部分の背景や文字色を細かく変更できるように作ったのですが、「この部分が場合によってはローコントラストになる可能性があります」「ここの背景を白以外にした場合に文字が見えなくなる場合があります」などの指摘で修正が埋め尽くされてしまいました。笑

結局、自由に色設定でき、かつ視認性を保つカラー設計は今回のテーマでは難しいと判断し、ほとんど全てデザインオプションから削除しました。

もし今後、テーマカラーを変更する機能を付ける場合は、よくあるテキストエディタみたいに「Darkモード」「Whiteモード」みたいな感じでコントラストを保ったまま好きなテイストを選べるように設計すれば審査も通るかな?と思います。やるかどうかはわからないですが。笑

また、BASE側が用意しているテンプレートタグで出力されるボタンやメッセージに関してもかなり指摘されました。

例えば、販売期間設定Appを設定すると商品詳細ページに「販売予告」「販売開始直前」「販売中」「販売終了」の4つのステータスごとにメッセージテキストに微妙な違いが出ます。

最初はそのテキストを全て同じ色にして強調させていたのですが、ステータスごとに色を変更するようにと指摘されました。理由はその方がユーザーがわかりやすいからとのことでした。たしかにそうですよね。
↑※2020年6月現在、直近で新しく審査に出したテーマに関してはこちらを厳しく言われることはなくなりました。ガイドラインの変更や審査する人によって私的内容がかなり違うようです。

また、ボタンの文言も「カートに入れる」から「販売期間のお知らせを希望する」などに文言が変わるので、その影響で「段落ち」や「改行」しないようにと指摘されました。

ステータスごとにCSSを用意

このあたりはチェックする時にどれだけ細かいところまで気を配れるかという話になってくるかと思いますが、テーマ申請が初めてだとけっこう引っかかるポイントかもしれません。

画像やテキストなど入力項目の汎用性

例えばロゴ画像の代わりにテキストを使う場合や、ブログのタイトルや商品タイトルなどの自由記述で文字の長さが変わるテキスト入力項目の部分については、どんな長さの文字が入っても崩れないようにするなどの対応を求められます

これはロゴテキストにかなり長めの文字を入力した場合ですが、こんな感じである程度の文字量が来ても変に崩れたりしないようにCSSをうまく書いておく必要があります。

また、画像の入力についてもかなり指摘されます。
「camera」のロゴは横長のものが合いますが、縦長の画像やものすごく大きい画像を入れた場合も崩れないように維持しなければいけません

【NG】縦長や大きい画像を考慮していない場合
【OK】どんな画像が入ってもデザインを維持するよう修正

画像を入力する箇所ではこういう崩れがないようにと指摘されますので作る段階でどんな画像が入っても対応できるようにしておくことがポイントです。

他には「ブログで縦長の大きい画像を設置するとスマホで見た時に画像で画面が埋め尽くされてしまうので高さの上限を設定してください」といった修正もありました。こういった修正はCSSのobject-fitがかなり役出ちました。

object-fit - CSS: カスケーディングスタイルシート | MDN

https://developer.mozilla.org/ja/docs/Web/CSS/object-fit

画像にmax-widthmax-heightを設定してobject-fit:contain;を指定すれば上限に合わせて画像が切れずにリサイズされて表示できます。注意点としてはIE対応する場合にobject-fit-imagesというpolyfillを使う必要があります。

BASE Apps対応

BASEのショップはBASE Appsをインストールすることでより便利に使えるようになります。ただ便利なAppもテーマがそもそも対応していないと使えないので、テーマ作成時にいくつか対応しておくべきAppがあります。

具体的なソースや対応については作成するテーマによって違うと思うのでドキュメントをよく読んで各自で対応する必要があります。ただ申請時に見落としがちな部分を1点メモしておきます。

メッセージApp

「スマホでメッセージAppがある場合にフッターのコピーライトに『ショップに質問する』ボタンが重なって表示されています」

これはドキュメントのチェックリストに数行だけ掲載されています。

インストールすると表示される下部固定のメッセージアイコンによって、重要な要素の閲覧・操作が阻害されないかどうか

https://docs.thebase.in/docs/template/guideline/check_list

具体的には下記のフッター画像のような対応が必要です。

スマホのフッターにボタンが重ならないための余白が必要

PCでは丸型のアイコンが画面右下固定されますが、今回のデザインではそんなに問題になりませんでした。スマホは画面の下に上記画像のような大きめのボタンが画面下に固定表示されますので、フッターでの要素の重なりには注意が必要です

Ajax対応

ここまではデザイン面の調整でしたが、ここからJavaScriptの話になります。

まずBASEの商品一覧ページや検索結果ページは、ループで表示できる最大件数は24件となっているようですが、デフォルトでページネーションの機能がないため、テンプレートタグだけでは25件目以上の商品が表示できません。そのため「もっと見る」などのボタンを用意してAjaxで次の商品リストを取得・更新する方法を実装する必要があります。

BASEがもとから用意しているデフォルトのテーマを見ると実装がわかりやすいです。ただ僕は何を血迷ったか全然違う方法で更新させようとしてハマりました。笑

最終的にはなんとか申請が通ったのでその実装方法をメモしておきます。

HTML

まずテンプレート内でJSを読み込む直前に、scriptタグを書きます。その中でグローバル変数BASE_APIを宣言し、名前空間にAjax処理で使用する情報を書いていきます。(グローバル汚染が気になる方は別の方法を探してください)

<script>
    var BASE_API = {
      // 商品ロードページのURL
      items          : "{LoadItemsPageURL}",
      // 商品ロードページのURLのパラメーター
      items_param    : "{LoadItemsPageURLParams}",
      // 最大ページ数
      item_max_page  : "{MaxPageNumber}",
      // 次のページ
      item_next_page : "{NextPageNumber}",
      // ページを認識するための文字列(カテゴリーAppと商品検索Appを使っている場合)
      category       : "{block:IndexPageCategory}{IndexPageCategory}{/block:IndexPageCategory}{block:IndexPageSearch}search{/block:IndexPageSearch}{block:ItemPage}item{/block:ItemPage}",
      // ショップのURL
      shop_url       : "{ShopURL}",
      // ショップID
      shop_id        : "{ShopId}"
    };
</script>
<!-- ここから下にJSを読み込む -->

急に商品ロードページってなに?って感じですが、これは実装方法が現在のドキュメントには詳しく書いていません。ドキュメント=>テンプレートタグ=>ページに項目としては載っています。要はBASE側がAjaxの更新を前提とした機能を提供していますが、やり方は各自で考えてくださいね、ということでしょう。

商品ロードページも必要なので作っていきます。

{block:NotLoadItemsPage}
<!-- 商品ロードページ以外のソース -->
{/block:NotLoadItemsPage}
{block:LoadItemsPage}
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="robots" content="noindex,nofollow">
</head>
<body>
  {block:HasItems}
  {block:Items}
  <!-- ループ表示する内容 -->
  {/block:Items}
  {/block:HasItems}
</body>
</html>
{/block:LoadItemsPage}

商品ロードページは、Ajaxで取得する商品一覧リストを表示できるページです。
「ループ表示する内容」というところに追加するリストと同じhtmlを入れてください。

ここまでで勘の良い方はわかると思いますが、要はこのページをそのままhtmlとしてjQueryの$.ajaxで取得し、今表示しているリストに追加する処理をします。

ちなみに僕はこのページにDOCTYPE宣言やnoindex設定などを入れて最低限のhtmlとして作っていますが、BASEのデフォルトテーマではループする部分のみが書かれています。これはどちらでも構いません。申請時もここに関しては何の修正もなく通ったのでどちらでも良いと思います。

jQueryで処理を書く

僕はES6 + Babel7 + Webpackでバンドルしたものを外部ファイルとしてBASE Developerの管理画面からアップして使いました。ここに書いてあるものは説明のためにその一部を抜粋して書き換えています。そのままコピペでは使えないため、考え方のひとつとして参考にしていただければと思います。

ボタンを押下するとリストを追加する処理

とりあえずMainというClassに書いていくことにします。

class Main {
  constructor() {
    this.doc = $(document);
    //商品リスト次のページ
    this.next         = parseInt(BASE_API.item_next_page);
    //商品リストの最後のページ
    this.max          = parseInt(BASE_API.item_max_page);
    //グローバルに定義した変数が空の場合はtopにしておく
    this.category     = !BASE_API.category ? 'top' : BASE_API.category;
    //AjaxのリクエストURL
    this.requestUrl   = `${BASE_API.items}${this.next}${BASE_API.items_param}`;
    //Ajax通信中フラグ
    this.progress     = false;
  }
  /*
  * 次の商品を追加する
  * @param {Object} $btn moreボタンのjQueryオブジェクト
  */
  addNextItemsList($btn) {
    //最後のページ以下で、通信中ではない(2重クリック防止)の場合
    if((this.next <= this.max) && !this.progress){
      this.progress = true;
    
      //ajax処理
      return $.ajax({
        cache      : false,
        url        : this.requestUrl,
        method     : 'get',
        dataType   : 'html',
        //10秒でタイムアウトする
        timeout    : 10000,
      })
      //ajax成功時
      .done((...args) => {
        const [data, textStatus, jqXHR] = args;

        const $items = $('商品ロードページから取得する要素', data);

        //要素がなければ終了
        if(!$items[0]){
          return;
        }
     
     //リストを追加する
        const $currentList = $('htmlを追加する要素');
        $currentList.append($items);

        //上限を超えたらボタンを非表示
        if(this.next >= this.max){
          $btn.css('display', 'none');
        }
        else{
          $btn.css('display', 'block');
        }
     
     // 商品リストの次ページ番号を加算
        this.next++;
        //ここで次ページ番号を保存
        this.saveNextPage();
        //通信処理完了
     this.progress = false;
      })
      //ajax失敗時
      .fail((...args) => {
     //なんらかのエラーハンドリング
        this.progress = false;
      });
    }
  }
  //次のページ番号を保存
  saveNextPage() {
    if(('sessionStorage' in window) && (window.sessionStorage !== null)) {
      //セッションストレージのキーは短すぎると競合の可能性があるので、ショップ情報などと組み合わせて競合しないようにする
      sessionStorage.setItem(`${this.shopId}-${this.themeName}-${this.themeVersion}-${this.category}-next`, this.next);
    }
  }
  //イベントの登録
  bind(){
    //「もっとみる」ボタンをクリックで商品リストを追加
    this.doc.on('click', '「もっとみる」ボタン', (e) => {
      this.addNextItemsList($(e.currentTarget));
    });
  }
}

const main = new Main();
main.bind();

下記の要素は自分の環境に合わせて変更してください。

  • '商品ロードページから取得する要素':先ほど商品ロードページで作ったhtmlから取得する要素
  • 'htmlを追加する要素':ここに取得した要素を追加する
  • '「もっとみる」ボタン':リストの下にボタンを追加し、これを押すことでAjaxを実行し次の商品リストを追加する

少し長くなりましたが、やっている内容はあまり難しくありません。
言葉で簡単に説明すると「もっとみるボタンを押すと次の商品リストが表示される」という処理ですが、さらに噛み砕くと下記のような流れです。

  1. 「もっとみる」ボタンを押す
  2. Ajaxで次の商品リスト(html)を取りに行く
  3. 取って来た商品リストを今の商品リストの末尾に追加する
  4. もしまだ次のリストがあれば「もっとみる」を表示、なければ非表示にする
  5. SessionStrageに次のページ番号を保存する

ポイントとしては、商品リストの次のページと最後のページを比較して、次のページが最後のページ以下の場合にリストを取得することと、次のページが最後のページ以上の場合に「もっとみる」ボタンを非表示にすることです。

また、Ajaxを使う際は重複処理を回避するために通信中フラグ(ここではthis.progress)を入れておいた方が良いです。

SessionStrageについては次のセクションで説明します。

ブラウザバック時にAjaxで更新した状態を復元する

これがデザイン以外で1番難しい部分でした。
ドキュメントのチェックリストを確認すると、「商品一覧」のところに下記の記述があります。

ajaxで商品を追加読み込みをした場合、商品をクリック→商品ページ→ブラウザバックでTOPページに戻った場合にスクロール位置が保たれているか

https://docs.thebase.in/docs/template/guideline/check_list

これについてはすぐには理解できず、何度か修正をもらうことでようやく理解できました。ポイントを書いていきます。

追加読み込みがどういう状態になっていれば良い?

まず大前提として商品リストの話です。ブログの記事リストは関係ありません。
その上で、テンプレートタグで表示できる最初の24件を1ページ目として、次の24件を2ページ目、その次を3ページ目として追加でリストを読み込んでいくことができる、ということです。

さらに、商品リストから3ページ目の商品をクリックして商品詳細ページに入り、そこからブラウザバックで戻ると、元の3ページ目のその商品をクリックした位置に画面を復元しておかなければならないという状態も必要です。先ほど書いたAjax処理だけでは、ページ遷移してブラウザバックすると追加したDOMが消えてしまいますので不可能です。

そこで試行錯誤してSessionStrageに次の商品リストページ数とスクロール位置を保存しておき、ブラウザバックした時に保存しておいたページ分の回数Ajaxをリクエストして無理矢理リストを表示させ、そのあとに保存しておいたスクロール位置を適用するというゴリ押しで対応させて再申請しました。笑

するとBASE側からの修正で、

BASEデフォルトテーマではchange_status.jsというファイルを読み込んでいます。そちらを参考になさってください。商品一覧から商品詳細に遷移する際にヒストリーとして戻れればよいので、ナビゲーションなど回遊してTOPページに来た時は初期表示の商品のみが表示されていれば問題ありません。

といった内容や、

特定のページ数までを取得した状態のHTMLは ショップのURL/4 のような、URLの末にページ数を指定する形でも実現でき、こちらの方がリクエスト数が少なくすむため、可能であれば実装を検討していただけますでしょうか。

といった指摘をいただきました。

そういえばデフォルトのテーマってそういう風にできてるんだから答えはそこにありますよね。はじめから参考にするべきでした。笑

ただ、特定のページ数までを取得した状態のHTMLは ショップのURL/4 のような、URLの末尾にページ数を指定する形でも実現できる、というのはドキュメントに載っていなかったのでここは重要なポイントだと思います。これを知らずに僕のようにブラウザバック時にAjaxをリクエストしまくる方法を思いついた方は申請したら注意されます。笑

そして修正するためにデフォルトテーマにリンクされているchange_status.jsをみてみると、どうやらHistoryAPIを使って、リンクをクリックした時にAjaxで更新したページ数をショップのURL/4のような形でブラウザに履歴として残すという処理のようです。

これをこちらで先ほど実装したものと組み合わせると、下記のようになりました。

makeHistory() {
  // add history (カテゴリーも考慮。検索はpage対応してない)
  const href = window.location.href;
  let url    = BASE_API.shop_url;
  if (url.substring(url.length - 1) != '/') {
    url = url + '/';
  }
  let categoryStr = href.match(/categories\/.+/);
  if (categoryStr != null) {
    // page対策
    const re = /([0-9]+\/)([0-9]+)$/;
    categoryStr = categoryStr[0].replace(re, "$1");
    if (categoryStr.substring(categoryStr.length - 1) != '/') {
      categoryStr = categoryStr + '/';
    }
  }
  else {
    categoryStr = "";
  }
  const next_page = this.next - 1;
  if (next_page > 1 && href.match(/\/search\?q/) == null) {
    window.history.pushState(null, null, url + categoryStr + next_page);
  }
}

ほとんどコピペですが、関数として使えるようにしました。
ただコメントに「検索はpage対応してない」と記載があります。そのため検索ページでは履歴を作る処理は省かれているようです。これは仕様上、仕方ないので検索ページだけは先ほどのゴリ押し復元処理を実行するようにしておきます。

最終的なコードはこちら。

class Main {
  constructor() {
    this.doc          = $(document);
    //商品リスト次のページ
    this.next         = parseInt(BASE_API.item_next_page);
    //商品リストの最後のページ
    this.max          = parseInt(BASE_API.item_max_page);
    this.category     = !BASE_API.category ? 'top' : BASE_API.category;
    //AjaxのリクエストURL
    this.requestUrl   = `${BASE_API.items}${this.next}${BASE_API.items_param}`;
    //通信中フラグ
    this.progress     = false;
    //ブラウザバック時の取得スタートページ
    this.pageIndex    = 2;

    this.themeName    = 'テーマ名';
    this.themeVersion = 'テーマのバージョン';
    //ショップID
    this.shopUrl      = BASE_API.shop_url;
    //ショップID
    this.shopId       = BASE_API.shop_id;
  }

  /*
  * 次の商品を追加する
  * @param {Object} $btn moreボタンのjQueryオブジェクト
  */
  addNextItemsList($btn) {
    if((this.next <= this.max) && !this.progress){
      this.progress = true;
      const $frame   = $('ローディングエフェクトを付ける要素');
      $frame.addClass('is-loading');
      
      //ajax処理
      return $.ajax({
        cache      : false,
        url        : this.requestUrl,
        method     : 'get',
        dataType   : 'html',
        timeout    : 100000,
      })
      //ajax成功時
      .done((...args) => {
        const [data, textStatus, jqXHR] = args;

        const $items = $('商品ロードページから取得する要素', data);

        //要素がなければ終了
        if(!$items[0]){
          return;
        }

        const $currentList = $('htmlを追加する要素');
        $currentList.append($items);
        
        //もしもCSSのobject-fitを使用している場合は、
        //IE11対応するためここで再度polyfillを実行する
        objectFitImages();

        //上限を超えたらボタンを非表示
        if(this.next >= this.max){
          $btn.css('display', 'none');
        }
        else{
          $btn.css('display', 'block');
        }
        
        //次ページ番号を加算
        this.next++;
        //次ページ番号をセッションストレージに保存
        this.saveNextPage()
      })
      .fail((...args) => {
        
        $frame.removeClass('is-loading');
        this.progress = false;
      })
      .always(() => {
        $frame.removeClass('is-loading');
        this.progress = false;
      });
    }
  }

  //次のページ番号を保存
  saveNextPage() {
    if(('sessionStorage' in window) && (window.sessionStorage !== null)) {
      sessionStorage.setItem(`${this.shopId}-${this.themeName}-${this.themeVersion}-${this.category}-next`, this.next);
    }
  }

  //セッションストレージをクリア
  clearSessionStrage() {
    if(('sessionStorage' in window) && (window.sessionStorage !== null)) {
      sessionStorage.clear();
    }
  }

  //スクロール位置を保存
  saveScrollPosition() {
    if(('sessionStorage' in window) && (window.sessionStorage !== null)) {
      //「もっとみる」ボタンが存在するページのみで実行
      if($('「もっとみる」ボタン')[0]){
        sessionStorage.setItem(`${this.shopId}-${this.themeName}-${this.themeVersion}-${this.category}-scroll`, this.win.scrollTop());
      }
    }
  }

  //履歴を作成
  makeHistory() {
    // add history (カテゴリーも考慮。検索はpage対応してない)
    const href = window.location.href;
    let url    = this.shopUrl;
    if (url.substring(url.length - 1) != '/') {
      url = url + '/';
    }
    let categoryStr = href.match(/categories\/.+/);
    if (categoryStr != null) {
      // page対策
      const re = /([0-9]+\/)([0-9]+)$/;
      categoryStr = categoryStr[0].replace(re, "$1");
      if (categoryStr.substring(categoryStr.length - 1) != '/') {
        categoryStr = categoryStr + '/';
      }
    }
    else {
      categoryStr = "";
    }
    const next_page = this.next - 1;
    if (next_page > 1 && href.match(/\/search\?q/) == null) {
      //ショップのURL/4のような形でブラウザに履歴として残す
      window.history.pushState(null, null, url + categoryStr + next_page);
    }
  }

  //次ページ番号までの商品リストを復元する
  addItemsList(){
    this.progress  = true;
    const $frame   = $('ローディングエフェクトを付ける要素');
    $frame.addClass('is-loading');

    const url = `${BASE_API.items}${this.pageIndex}${BASE_API.items_param}`;

    return $.ajax({
      cache      : false,
      url        : url,
      method     : 'get',
      dataType   : 'html',
      timeout    : 100000,
    })
    .done((...args) => {
      const [data, textStatus, jqXHR] = args;

      const $items = $('商品ロードページから取得する要素', data);

      //要素がなければ終了
      if(!$items[0]){
        return;
      }

      const $currentList = $('htmlを追加する要素');
      $currentList.append($items);
      this.objectFitImages();

      //元々表示していたページまで再帰的に呼び出す
      if(this.pageIndex < this.next - 1){
        this.pageIndex++;
        this.addItemsList();
      }
      else {
        const category = !BASE_API.category ? 'top' : BASE_API.category;
        if($('「もっとみる」ボタン')[0]){
          const scroll = sessionStorage.getItem(`${this.shopId}-${this.themeName}-${this.themeVersion}-${category}-scroll`);
          if(scroll){
            this.win.scrollTop(scroll);
          }
        }
      }

      //上限を超えたらボタンを非表示
      if(this.pageIndex >= this.max){
        $('「もっとみる」ボタン').css('display', 'none');
      }
      else{
        $('「もっとみる」ボタン').css('display', 'block');
      }

    })
    //ajax失敗
    .fail((...args) => {
      //なにかしらのエラーハンドリング
      $frame.removeClass('is-loading');
      this.progress = false;
    })
    .always(() => {
      //最後にローディング処理を終了
      $frame.removeClass('is-loading');
      this.progress = false;
    });
  }

  //検索結果一覧ページで商品リストをセッションストレージから復元
  restoreItemListBySessionStorage(){
    if(('sessionStorage' in window) && (window.sessionStorage !== null)) {
      const next = sessionStorage.getItem(`${this.shopId}-${this.themeName}-${this.themeVersion}-${this.category}-next`);
      //sessionStorageの中身がある場合
      if(next) {
        this.next = parseInt(next);
        if('search' === category){
          this.pageIndex = 2;
          this.addItemsList();
        }
        else {
          if(this.next > this.max){
            $('「もっとみる」ボタン').css('display', 'none');
          }
          else {
            $('「もっとみる」ボタン').css('display', 'block');
          }
        }
      }
      //sessionStorageの中身がない場合
      else {
        if(this.next > this.max){
          $('「もっとみる」ボタン').css('display', 'none');
        }
        else{
          $('「もっとみる」ボタン').css('display', 'block');
        }
      }
    }
  }

  //イベントの登録
  bind(){
    //商品リストを追加
    this.doc.on('click', '「もっとみる」ボタン', (e) => {
      this.addNextItemsList($(e.currentTarget));
    });

    //ブラウザバック時に商品リストを復元
    window.addEventListener('pageshow', ()=> {
      this.restoreItemListBySessionStorage();
    });

    //商品詳細リンク以外を踏んだらセッションストレージを解放
    this.doc.on('click', 'a:not(商品詳細へのリンク要素)', (e) => {
      this.clearSessionStrage();
    });

    //検索フォームをsubmitするとセッションストレージを解放
    this.doc.on('submit', '検索フォーム', () => {
      this.clearSessionStrage();
    });

    //商品詳細リンクを踏んだ時に履歴・スクロール位置を保存
    this.doc.on('click', '商品詳細へのリンク要素', (e) => {
    this.makeHistory();
      this.saveScrollPosition();
    });
  }
}

const main = new Main();
main.bind();

下記の要素は自分の環境に合わせて変更してください。

  • 'テーマ名':BASEで申請するテーマ名
  • 'テーマのバージョン':BASEで申請するテーマのバージョン
  • 'ローディングエフェクトを付ける要素':この要素にローディング中はis-loadingというclass名を付け、CSSでローディング表示・非表示処理をできるようにしておく
  • '商品ロードページから取得する要素':先ほど商品ロードページで作ったhtmlから取得する要素
  • 'htmlを追加する要素':ここに取得した要素を追加する
  • '「もっとみる」ボタン':リストの下にボタンを追加し、これを押すことでAjaxを実行し次の商品リストを追加する
  • '検索フォーム':検索フォームのform要素
  • '商品詳細へのリンク要素':商品リスト内の商品詳細へのリンク要素

先ほどの「ボタンを押下するとリストを追加する処理」で作ったJSにブラウザバックでページを復元する機能を追加しました。コードは長いですが、処理としてはこちらもそんなに難しくはないと思います。処理の流れは下記です。

  1. 「もっとみる」ボタンを押す
  2. Ajaxで次の商品リストを取りに行く
  3. 取って来た商品リストを今の商品リストの末尾に追加する
  4. もしまだ次のリストがあれば「もっとみる」を表示、なければ非表示にする
  5. SessionStrageに次のページ番号を保存する(ここでは例として4ページ目まで表示しているとする)
  6. 商品リスト内のリンクをクリック(商品詳細ページへ)
  7. 遷移する直前にショップのURL/4のような形でブラウザに履歴を追加
  8. 遷移する直前にSessionStrageにスクロール位置を保存
  9. ブラウザバックするとショップのURL/4を表示するので4ページ目まで読み込んだ状態のページが表示される。
  10. SessionStrageからスクロール位置を取得し元の位置に復元する

なお、検索結果一覧ページの場合はBASE側にページング機能がないためpageshowイベントを使ってブラウザバックした時にSessionStrageに保存しているページまでの回数分、Ajaxを実行させます。

実装の注意点としては、商品リストから商品詳細ページへのリンク以外の遷移ではSessionStrageをクリアすることです。これをしておかないと、他のページからのブラウザバック時もpageshowイベントが毎回発生するので、その度にSessionStrageを見にいってしまい他のページでブラウザバックした時にバグります。そのため商品詳細ページ以外のリンクと検索フォームのsubmit時にSessionStrageをクリアするイベントを書いています。

ここが先ほどの指摘の1つ目の部分、「商品一覧から商品詳細に遷移する際にヒストリーとして戻れればよいので、ナビゲーションなど回遊してTOPページに来た時は初期表示の商品のみが表示されていれば問題ありません」の修正点です。

動作については実際にテーマを公開して、商品一覧を追加=>商品詳細ページに遷移=>ブラウザバックという挙動を試してもらえばどういうことかわかるかと思います。

camera | BASE デザインマーケット ネットショップのデザインをもっと自由に

https://design.thebase.in/detail/54

さいごに

BASEのテーマ申請で審査に通りにくかった所と、それにどう対応したら通ったかをメモしました。たぶん僕だけじゃなく、みなさん何かしら審査に引っかかって解決するのにどうしたらいいか悩んだり苦労したりしているはずなので、少しでもそういう方の助けになればと思います。

今後もしも2つ目のテーマを作ることになった場合は、今回の大量修正の反省を活かして、目標は3回目ぐらいの再申請で通るようにできればと思います。笑

ここまで読んでくださってありがとうございます。
そして苦労して作ったテーマ「camera」をどうぞよろしくお願いします!

記事一覧

RELATED

inoue
TRY

【WordPress】GA4連携の人気記事ランキング機能を自作プラグイン化してみた

inoue
TRY

【netlify】ビルドイメージを更新 [ Ubuntu Xenial 16.04 → Ubuntu Focal 20.04 ]

inoue
TRY

スクリーンショットのAPI「screendot」を使ってみた

inoue
TRY

DartSassがなかなか辛かったのでGulpを修正してみた

inoue
TRY

【howler.js】ストリーミング音源からオーディオビジュアライザーを作る

inoue
TRY

Simple GA Ranking[ver2.1.6]が表示されないので調べてみた

NEW POSTS

inoue
TIPS

【CSS + JS】テキストの無限ループアニメーション

inoue
TIPS

【Shopify】カートへの遷移を飛ばしてチェックアウトに進むボタンを設置する

inoue
TIPS

【JS】PhotoSwipe v5を使って画像をポップアップ表示する

inoue
TIPS

gulpでjpg・pngからAVIF画像を生成する

inoue
TIPS

【JS】MutationObserverでDOMを監視[PhotoSwipe(v5)+Swiper(v8)連携]

inoue
TIPS

【Google Sheets API】 スプレッドシートのデータをJSONで取得する

ブログ記事一覧
  • HOME
  • TRY
  • 【BASEテンプレート作成】デザインマーケットで「camera」を販売するまで