star back image
people4
電飾 電飾
moon
men

【shopify】ブログ記事一覧ページでタグを使ったソートボタンを実装する

BLOG shopifyWEBログ
読了約:28分

ブログの記事一覧ページにソートボタンを付けたい。
ソートの内容は記事に付けたタグです。

そういう要件はありますでしょうか。

一般的にソートをするとしたら思いつくのは以下の実装でしょうか。

・カスタムクエリパラメーター: ?category=news&tag=information
・JavaScriptでのフィルタリング
・手動でのページネーション実装

めんどくさそうです。

しかしshopifyなら、taggedパラメーターという便利な独自機能があります。

shopify独自のtaggedパラメーター

ブログ一覧のテンプレートに仕込むと以下の機能が可能となります。
とても良いのはShopify標準機能だけで完結できることです。

  • 独自のtaggedパラメーター: /blogs/blog-handle/tagged/tag-name
  • 自動フィルタリング: そのタグを持つ記事のみを自動的に表示
  • ページネーション連動: フィルタされた記事に対してページネーションも自動適用
  • SEO対応: 検索エンジンにも適切にインデックスされる
リンクに仕込むと使えます。
<a href="{{ lang }}/blogs/{{ blog.handle }}/tagged/{{ ordered_tag | handleize }}">

※他にもあるshopify独自タグ
/collections/collection-handle/products/product-handle
/blogs/blog-handle/tagged/tag-name ←今回はこれ
/collections/collection-handle/tag-name
/search?q=search-term&type=article

サーバーサイドレンダリングで、JavaScriptのクリックイベントやDOM操作など一切必要ありません。

記事一覧ページのファイルにどのようにして仕込むのでしょうか。

AIに聞きくと答えてくれます。

【共有】方法を2種類

方法1はボツになった方法で「タグをソートボタンにする」方法。
方法2は「メニューで登録した単語でソートボタンにする」方法です。

方法1:タグをソートボタンにする

ブログ記事のタグがソートボタンとなります。

記事にタグを追加します

要件は以下です。

  • 記事のタグがソートボタンとして出力される
  • 選択されたタグにactiveクラスが適用される
  • 12記事ごとのページ分割
  • タグフィルター時も正しくページネーションが動作

main-blog.liquidはテンプレートによってファイル名が違うかもしれませんが、要はブログ一覧用のファイルです。

main-blog.liquid

{%- style -%}
/* タグフィルターボタンのスタイル */
.blog-tag-filter {
  margin: 0 0 20px;
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

.tag-btn {
  background: #fff;
  border: 1px solid #dddddd;
  border-radius: 50px;
  padding: 9px 20px 8px;
  cursor: pointer;
  font-family: "Helvetica Neue",
    Arial,
    "Hiragino Kaku Gothic ProN",
    "Hiragino Sans",
    Meiryo,
    sans-serif;
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0;
  text-transform: uppercase;
  color: #101010;
  text-decoration: none;
  display: inline-block;
  transition: background .2s, color .2s, border .2s;
}

.tag-btn.active,
.tag-btn:hover {
  background: #fff;
  border: 1px solid #000A82;
  color: #000A82;
}

/* スマホ対応:横スクロール */
@media screen and (max-width: 767px) {
  .blog-tag-filter {
    width: calc(100% + 40px);
    margin: 0 0 20px -20px;
    padding: 0 20px 0 0;
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    white-space: nowrap;
    gap: 5px;
  }
  
  .blog-tag-filter .tag-btn {
    flex: 0 0 auto;
    white-space: nowrap;
  }
  
  .blog-tag-filter .tag-btn:first-child {
    margin: 0 0 0 20px;
  }
}
{%- endstyle -%}

{%- paginate blog.articles by 12 -%}
  <div class="p-news-list__inner l-section__inner">
    <div class="p-news-list__heading c-heading">
      <h1 class="p-news-list__title c-heading__title u-font-en-h2">{{ blog.title | escape }}</h1>
    </div>

    <div class="blog-tag-filter">
      {%- if request.locale.name == "English" -%}{% assign lang = '/en' %}{%- else -%}{% assign lang = '' %}{%- endif -%}
      <a href="{{ lang }}/blogs/{{ blog.handle }}" class="tag-btn{% unless current_tags %} active{% endunless %}">ALL</a>
      
      {% comment %} 全記事からタグを収集(フィルタリングされていない全記事から) {% endcomment %}
      {% assign all_tags_string = '' %}
      {% assign all_blog_articles = blogs[blog.handle].articles %}
      {% for article in all_blog_articles %}
        {% for tag in article.tags %}
          {% assign all_tags_string = all_tags_string | append: tag | append: ',' %}
        {% endfor %}
      {% endfor %}
      {% assign all_tags = all_tags_string | split: ',' | uniq %}

      {% comment %} タグの表示順序を指定 {% endcomment %}
      {% assign ordered_tags = 'INFORMATION,POP UP,CAMPAIGN,WOMEN,MEN,ITEMS,STORY,HOW TO' | split: ',' %}
      {% for ordered_tag in ordered_tags %}
        {% if all_tags contains ordered_tag %}
          <a href="{{ lang }}/blogs/{{ blog.handle }}/tagged/{{ ordered_tag | handleize }}" class="tag-btn{% if current_tags contains ordered_tag %} active{% endif %}">{{ ordered_tag }}</a>
        {% endif %}
      {% endfor %}
      
      {% comment %} 指定した順序以外のタグを表示 {% endcomment %}
      {% for tag in all_tags %}
        {% unless ordered_tags contains tag or tag == blank %}
          <a href="{{ lang }}/blogs/{{ blog.handle }}/tagged/{{ tag | handleize }}" class="tag-btn{% if current_tags contains tag %} active{% endif %}">{{ tag }}</a>
        {% endunless %}
      {% endfor %}
    </div>
    
    <div class="p-news-list__list c-news {% if blog.handle == 'feature' %}p-feature-list__list c-feature{% endif %}">
      {%- for article in blog.articles -%}
        <div class="c-news__item {% if blog.handle == 'feature' %}c-feature__item{% endif %}">
          {%- render 'article-card',
            article: article,
          -%}
        </div>
      {%- endfor -%}
    </div>

    {%- if paginate.pages > 1 -%}
      {%- render 'pagination', paginate: paginate -%}
    {%- endif -%}
  </div>
{%- endpaginate -%}

{% schema %}
{
  "name": "t:sections.main-blog.name",
  "tag": "section",
  "class": "l-section p-news-list",
  "settings": [
  ]
}
{% endschema %}

実装後、ソートボタンの表示順を変えたいと来ました。

なぜ?

デザインがそのようになっているのでとのことです。

ボツになったのは以下のコードが原因です。※84行目
{% assign ordered_tags = 'INFORMATION,POP UP,CAMPAIGN,WOMEN,MEN,ITEMS,STORY,HOW TO' | split: ',' %}

ボタンの順番をハードコーディング指定しています。
また順序を変えたい時、この方法は一般的に運用する人には荷が重いからという理由です。

タグ表示は全自動で表示非表示ができる反面、順番は自由にできません。
記事の公開順などで変わります。

方法2:メニューで登録した単語をソートボタンにする

メニューにする理由はソートボタン名を管理画面のメニューから登録できるからです。
並び順も自由にドラッグで変更できます。

メニューにソート名の単語を登録する

ブログ記事へのタグ登録は、比べるために普通に必要です。

ブログ記事へのタグ登録は必要

要件は以下です。

  • メニューの単語からソートボタンにする
  • 選択されたタグにactiveクラスが適用される
  • 12記事ごとのページ分割
  • タグフィルター時も正しくページネーションが動作

main-blog.liquid

{%- style -%}
/* 省略 方法1と同じ */
{%- endstyle -%}

{%- paginate blog.articles by 12 -%}
  <div class="p-news-list__inner l-section__inner">
    <div class="p-news-list__heading c-heading">
      <h1 class="p-news-list__title c-heading__title u-font-en-h2">{{ blog.title | escape }}</h1>
    </div>

    <div class="blog-tag-filter">
      {%- if request.locale.name == "English" -%}{% assign lang = '/en' %}{%- else -%}{% assign lang = '' %}{%- endif -%}
      <a href="{{ lang }}/blogs/{{ blog.handle }}" class="tag-btn{% unless current_tags %} active{% endunless %}">ALL</a>

      {% comment %} メニューからタグを取得(blog-menu-news、blog-menu-feature) {% endcomment %}
      {% assign menu_handle = 'blog-menu-' | append: blog.handle %}
      {% assign blog_menu = linklists[menu_handle] %}
      {% assign menu_to_render = blog.handle %}
      
      {% if blog_menu and blog_menu.links.size > 0 %}
        {% for link in blog_menu.links %}
          {% comment %} リンクURLからタグ名を抽出 {% endcomment %}
          {% assign display_tag = link.title %}
          {% comment %} 単語間のスペースはハイフンに変換 {% endcomment %}
          {% assign linkurl = link.title | replace: ' ', '-' | downcase | url_encode %}
          {% assign current_tags_lower = current_tags | replace: ' ', '-' | downcase %}
          {% assign current_tags_array = current_tags_lower | split: ',' %}
          {% assign is_active = false %}
          {% assign current_tag_raw = current_tags_lower | split: ',' | first | strip %}
          {% assign current_tag = current_tag_raw | remove: '[' | remove: ']' | remove: '"' | strip %}
          <!-- current_tag:{{ current_tag }}-->
          {% if current_tag == linkurl %}
            <!-- current_tags_lower:{{ current_tags_lower }}-->
            <!-- linkurl:{{ linkurl }}-->
            {% assign is_active = true %}
          {% endif %}
          {% comment %} デバッグ用出力 {% endcomment %}
          <!-- current_tags:{{ current_tags }}-->
          <!-- linkurl:{{ linkurl }}-->
          <!-- linktitle:{{ link.title }}-->
          <a href="{{ lang }}/blogs/{{ menu_to_render }}/tagged/{{ linkurl }}" class="tag-btn{% if is_active %} active{% endif %}">{{ link.title }}</a>
          {% comment %} デバッグ用出力 {% endcomment %}
          <!--デバッグ用出力:{{ lang }}/blogs/{{ menu_to_render }}/tagged/{{ linkurl }}-->
        {% endfor %}
      {% endif %}
    </div>
    
    <div class="p-news-list__list c-news {% if blog.handle == 'feature' %}p-feature-list__list c-feature{% endif %}">
      {%- for article in blog.articles -%}
        <div class="c-news__item {% if blog.handle == 'feature' %}c-feature__item{% endif %}">
          {%- render 'article-card',
            article: article,
          -%}
        </div>
      {%- endfor -%}
    </div>

    {%- if paginate.pages > 1 -%}
      {%- render 'pagination', paginate: paginate -%}
    {%- endif -%}
  </div>
{%- endpaginate -%}

{% schema %}
{
  "name": "t:sections.main-blog.name",
  "tag": "section",
  "class": "l-section p-news-list",
  "settings": [
  ]
}
{% endschema %}

デバッグ用出力コード込みです。

かなり時間を割いていましたね。

はい。WOMENとMENで、MENが両方入っているから?同じと判定されるのか、両方アクティブになってしまっていました。

この部分でメニューの単語を整理しています。
{% assign linkurl = link.title | replace: ' ', '-' | downcase | url_encode %}
{% assign current_tags_lower = current_tags | replace: ' ', '-' | downcase %}
{% assign current_tag = current_tag_raw | remove: '[' | remove: ']' | remove: '"' | strip %}

{% if current_tag == linkurl %}
{% assign is_active = true %}
{% endif %}

**処理内容**
1. メニュータイトルをURL用に変換(スペース→ハイフン、小文字化)
2. 現在のタグと比較してアクティブ状態を判定
3. 不要な文字(`[`, `]`, `"`)を除去して正確な比較を実現

処理内容の「3.不要な文字(`[`, `]`, `”`)を除去して正確な比較を実現」が、私には難しく、プログラムが得意な人に助けてもらいました。AIにどう聞いていいのかもわかりませんでした。

配列に不要な[や”が紛れて特殊だったとのこと。

この方法はshopifyの管理画面で、ボタンを増やしたいならメニューに追加するというきちんとした管理をしてもらうことになります。

【妄想】プログラマーの思考プロセス

問題: WOMENとMENが両方アクティブになる

1. 仮説を立てる

  • 「MENが両方でアクティブになる」
  • → 「文字列比較に問題がある?」
  • → 「配列処理に問題がある?」

2. 段階的に確認

  • 元データの確認
  • 各変換ステップの確認
  • 最終的な比較値の確認

3. 問題を特定

  • 配列が文字列化されている
  • 不要な文字([, ], “)が混入
  • 文字列比較が正確でない

デバッグ手順

<!-- 1. 現在のタグを確認 -->
<p>current_tags 生データ: {{ current_tags }}</p>
<!-- 出力例: ["WOMEN", "MEN"] または "WOMEN,MEN" -->

<!-- 2. 分割処理を確認 -->
{% assign tags_array = current_tags | split: ',' %}
<p>分割後の配列:</p>
{% for tag in tags_array %}
  <p>  {{ forloop.index }}: "{{ tag }}"</p>
{% endfor %}

<!-- 3. 最初の要素を確認 -->
{% assign first_tag = tags_array.first %}
<p>first_tag: "{{ first_tag }}"</p>

<!-- 4. 文字列クリーンアップを確認 -->
{% assign clean_tag = first_tag | remove: '[' | remove: ']' | remove: '"' | strip %}
<p>クリーンアップ後: "{{ clean_tag }}"</p>
<!-- 実際の出力例 -->
current_tags 生データ: ["WOMEN", "MEN"]
分割後の配列:
1: "["WOMEN""
2: " "MEN"]"
first_tag: "["WOMEN""
クリーンアップ後: "WOMEN"

AIさんにプロセスを見える化してもらいました内容です。

これからこういう妄想路線で行くんですか?

【AI】イラストを描いてもらった

今回の記事のキャッチ画像で使わせてもらいます「Memeplex.app」で作成した画像です。誰でもgoogleアカウントでログインして使えます。

この記事にピッタリなイラストのための考えたリクエストは、「記憶が走馬灯のように目の前を過ぎていく。記憶の映像を操作するUIが異世界転生のアニメのように目の前に現れるコントロールパネル。」です。

星間旅路のメロディ

「宇宙の静けさに包まれながら、漂流する過去の音楽を捜し求め、銀河の奥底でその旋律に耳を傾ける。」

「この電波はどこの星からきたのだろうか。」

どこかで聞いたことがあるような。