star back image
people4
電飾 電飾
moon
men

【shopify】カウントダウンをさせたい

BLOG AIshopifyWEBログ
読了約:47分

ただ粛々と数字が減っていく「カウントダウンする」だけのページが欲しい。
そんな要件はありますでしょうか。

何に使うんですか?

shopifyのお店をオープンする前にプレオープンして、カウントダウンな演出をしたい。みたいな感じかと思われます。

カウントダウンしている様子

オープン前の数日だけ使われるので、それ以降は日の目を見ることなく永遠に忘れ去られてしまう、悲しきカウントダウンページです。

寂しいので記録として残しておきます。

【共有】ソースコード

sectionsにカウントダウン用の.liquidファイルを作成し、共有コードを丸っとコピペください。

このコードの要件は以下です。
・カウント完了日時を自由に入力する場所がほしい
・現在時刻から逆算したカウント数字を出力する
・カウントは1秒間隔でする
・カウントが終わったらテキスト文に差し替えたい

sections/countdown.liquid

<style>
.set {
}
.cont-box .rte {
  position: relative;
  padding: 6.65rem 11.4rem;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}
.count-time-box {
  margin: 0 auto;
  min-width: 168px;
  font-family: Arial, Helvetica, 'Helvetica Neue', Arial, 'Lucida Grande', sans-serif;
  position: absolute;
  bottom: 86px;
  left: 50%;
  transform: translate(-50%, 0%);
}
.count-set {
  display: flex;
  margin-top: 8px;
}
.count-day {
  display: flex;
  width: 50%;
  font-size: 21px;
  letter-spacing: 0.03em;
  position: relative;
}
.count-day::before {
  content: "";
  position: absolute;
  top: 14px;
  left: 34px;
  width: 36px;
  height: 1px;
  background-color: #333;
}
.count-day span,
.count-time span {
  position: relative;
}
.count-day span::before {
  content: "Days";
  position: absolute;
  bottom: -20px;
  left: 0;
  width: auto;
  height: auto;
  color: #000;
  font-size: 10px;
  text-transform: none;
}
.count-time {
  width: 50%;
  font-size: 21px;
  letter-spacing: 0.03em;
  white-space: nowrap;
}
.count-time span.h::before {
  content: "H";
  position: absolute;
  bottom: -23px;
  left: 50%;;
  width: auto;
  height: auto;
  color: #000;
  font-size: 10px;
  text-transform: none;
  transform: translate(-50%, 0%);
}
.count-time span.m::before {
  content: "M";
  position: absolute;
  bottom: -23px;
  left: 50%;;
  width: auto;
  height: auto;
  color: #000;
  font-size: 10px;
  text-transform: none;
  transform: translate(-50%, 0%);
}
.count-time span.s::before {
  content: "S";
  position: absolute;
  bottom: -23px;
  left: 50%;
  width: auto;
  height: auto;
  color: #000;
  font-size: 10px;
  text-transform: none;
  transform: translate(-50%, 0%);
}
@media (max-width: 1024px) {
  .count-time-box {
    bottom: 55px;
  }
  .cont-box .rte {
    padding: 3.25rem 20px;
  }
}
</style>

<section class="set">
  <div class="cont-box">
    <div class="rte">

      <div class="center">
        {% assign current_time = 'now' | date: '%s' %}
        {% assign setting_limit_time = section.settings.display_limit_time | date: '%s' %}

        {% assign displayable = true %}
        {% if current_time > setting_limit_time %}
          {% assign displayable = false %}
        {% endif %}

        <div class="count-time-box" id="countdown-container">
          <div class="count-set">
            <p id="countdown-day" class="count-day">0</p>
            <p id="countdown-time-end" class="count-time">0:00:00</p>
          </div>
        </div>
      </div>

    </div>
  </div>
</section>

<script>
function padWithZero(num) {
  return num < 10 ? '0' + num : num;
}

function updateCountdown() {
  var now = new Date().getTime();
  var countdownContainer = document.getElementById("countdown-container");
  
  var endTime = new Date("{{ section.settings.display_limit_time }}").getTime();
  var remainingTime = endTime - now;

  if (remainingTime > 0) {
    var days = Math.floor(remainingTime / (1000 * 60 * 60 * 24));
    var hours = Math.floor((remainingTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
    var minutes = Math.floor((remainingTime % (1000 * 60 * 60)) / (1000 * 60));
    var seconds = Math.floor((remainingTime % (1000 * 60)) / 1000);

    // 時間は1桁のまま、日付、分、秒は2桁にする
    var displayHours = hours < 10 ? hours.toString() : padWithZero(hours);
    
    document.getElementById("countdown-day").innerHTML = `<span>${padWithZero(days)}</span>`;
    document.getElementById("countdown-time-end").innerHTML = `<span class="h">${displayHours}</span>:<span class="m">${padWithZero(minutes)}</span>:<span class="s">${padWithZero(seconds)}</span>`;
  } else {
    countdownContainer.innerHTML = `
      <div class="count-time">
        <p>WEBSITE RENEWAL OPENING SOON.</p>
      </div>
    `;
  }
}

window.onload = function() {
  setInterval(updateCountdown, 1000);
};
</script>


{% schema %}
{
  "name": "Pages count down site",
  "settings": [
    {
      "type": "text",
      "id": "display_limit_time",
      "label": "指定: YYYY/MM/DD HH:MM",
      "default": "2040/01/01 01:00"
    }
  ]
}
{% endschema %}

↑このカウントダウン用のliquidを、表示させたいページに仕込みます。

pageテンプレートを作成します

templates/page.countdown.liquid(.json)
sections/countdown.liquidを読み込ませましょう(13行目)。

<div class="page-width">
  <div class="grid">
    <div class="grid__item medium-up--five-sixths medium-up--push-one-twelfth">
      <div class="section-header text-center">
        <h1>{{ page.title }}</h1>
      </div>

      <div class="rte">
        {{ page.content }}
      </div>

      <div class="saigamo-nyaan">
        {% section 'countdown' %}
      </div>
    </div>
  </div>
</div>
テスト環境のテンプレートが古いバージョンだなと気づきました。ですが、大体やってることは同じなので、新環境に合わせて.jsonにするなどして合わせてください。

「カウントダウン」ページを作成します
上で作成したテンプレートを適用しましょう。

作成したページに、先に作成したテンプレートを適用する

↑このページは「動作確認用のテスト」としてのサンプルページを作っています。

後ほど、このカウントダウンは/passwordページに移植して(パスワードの内容は非表示にします)プレオープン(のような意味合い)させて、カウントダウン表示させます。

数字を変えるとカウントも変わる1
数字を変えるとカウントも変わる2
カウントダウンが終わるとテキストに変わるのを確認できる

カウントダウンが終わるとテキストに変わります。

動きはできました。あとは/passwordページに移植してプレ公開。カウントダウンが終了したら、ショップを公開する流れです。

このコードでは、クライアントサイド(ブラウザ)で現在時刻を取得してカウントダウンを計算しています。そのため、表示されるカウントダウンの結果は そのユーザーの端末の現在時刻 を基にしています。台湾のユーザーがアクセスした場合、そのユーザーのパソコンやスマホの時間が正確であれば問題なく台湾時間で計算されます。

ただ、注意が必要な点があります。

【海外サイト】クライアントサイドの時差を考慮する

海外のショッピングサイトの時はありますでしょうか。
カウントダウンを作ったはいいけど、時差がある国でサイトを見た時は大丈夫?というのです。例えば、台湾の時。

日本で作ってますよ、何を言ってるんですか。

AIさんに聞きくしかないですね。

AIさんはいうのです。私の言うとおりにしていればと。

イーノックそんな装備で大丈夫か。

注意が必要な考慮事項

1.ユーザーのデバイス時間の信頼性
・クライアントサイドで時刻を取得するため、ユーザーのデバイスのシステム時刻が正確でない場合、カウントダウン結果が正しくなくなります。
・例えば、ユーザーの端末の時計が手動で変更されていた場合(未来や過去に設定されているなど)、その時刻を基にした計算になります。

2.Shopify設定時間
・Liquid側で section.settings.display_limit_time を設定しています。この設定はShopifyの管理画面から日時を設定していると思いますが、この日時は Shopifyストアのタイムゾーン設定 に基づきます。
Shopifyストアのタイムゾーンが日本に設定されている場合、台湾の時刻に合わせるためには手動で時差分を考慮する必要があります。

信頼性の云々は置いておいて、手動で時差分をタイムゾーン合わせる必要があるようです。

時差ってよくわかりません。
日本で作ってるけど海外?

地球時間とかないんですかw

そのショッピングサイトがある想定のその国に合わせる。

台湾時間を正確に扱いたい場合は、表示用の時刻(カウントダウン終了時刻)をクライアントサイドで調整します。

時差を知りたい。そんな時に便利なサイトがあります。

Time-j.net 世界時計 – 世界の時間と時差
https://www.time-j.net/WorldTime/Country/TW
↑ 例えば台湾の場合。
日本と台湾との時差は、1時間です。日本の方が、1時間進んでいます。

そうなると以下のように改修します。

function updateCountdown() {
  var now = new Date().getTime();

  // 台湾時間を計算する
  var endTime = new Date("{{ section.settings.display_limit_time }}").getTime();
  
  // 台湾のタイムゾーン補正(+1時間、日本との時差)
  var taiwanOffset = 60 * 60 * 1000; // 1時間 = 60分 × 60秒 × 1000ミリ秒
  endTime -= taiwanOffset;

  var remainingTime = endTime - now;

  if (remainingTime > 0) {
    var days = Math.floor(remainingTime / (1000 * 60 * 60 * 24));
    var hours = Math.floor((remainingTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
    var minutes = Math.floor((remainingTime % (1000 * 60 * 60)) / (1000 * 60));
    var seconds = Math.floor((remainingTime % (1000 * 60)) / 1000);

    var displayHours = hours < 10 ? hours.toString() : padWithZero(hours);

    document.getElementById("countdown-day").innerHTML = `<span>${padWithZero(days)}</span>`;
    document.getElementById("countdown-time-end").innerHTML = `<span class="h">${displayHours}</span>:<span class="m">${padWithZero(minutes)}</span>:<span class="s">${padWithZero(seconds)}</span>`;
  } else {
    document.getElementById("countdown-container").innerHTML = `
      <div class="count-time">
        <p>WEBSITE RENEWAL OPENING SOON.</p>
      </div>
    `;
  }
}
・endTime: 表示したいカウントダウンの終了日時(例: 台湾時間での終了日時)。
・now: 現在時刻(ユーザーの端末時間に基づく)。
・taiwanOffset: 台湾の時差(UTC+8に基づく補正値)。

(9行目)endTime -= taiwanOffset;
この行は、終了日時を台湾のタイムゾーンに合わせるために調整しています。
endTime が UTC(協定世界時)基準の日時と仮定した場合、これに台湾のタイムゾーン(UTC+8)を補正して、台湾時間で正確なカウントダウンを実現します。

(11行目)var remainingTime = endTime - now;
この行は、現在時刻(now)と調整済みの終了日時(endTime)の差分を計算しています。


Shopifyではサーバーサイドの現在時刻を取得する方法に制約があるため、基本的にはフロントエンドでの調整が推奨されます。

もし日本の設定でShopify管理画面から終了日時を設定している場合、クライアントサイドでタイムゾーンを補正する方法が簡単で効果的です。
台湾のユーザーに正しい結果を表示するために、 台湾時間の時差を考慮して調整する ことをお勧めします。

改修した後、とにかくendTime を「台湾時間での終了日時」に設定することが重要 です。

台湾時間を基準にすることで、日本や世界中からのアクセスでも 台湾基準のカウントダウン が実現できると言うのです。

とAIが言っています。(まぁいいやつだったよ)

サーバーサイドで正確な時刻を取得する方法もあるらしいです。しかし、よくわかりません。

ChatGPTさんのお勧めの方法でいいと思います。

でもユーザーの端末時間が狂ってたら、どうもならないですよね?

【AI】端末時間の影響を避ける方法(API)

ユーザーの端末時間が狂ってたら?と聞いてみました。以下、返答です。

その通りです!
クライアントサイドでの時間計算 は、ユーザーの端末のシステム時刻を利用するため、端末時間が狂っていると正しいカウントダウン結果は得られません。例えば、ユーザーが端末の時刻を1時間進めたり、日付を変更している場合、その影響を直接受けてしまいます。

そうですよね。
どうするのか聞くと、APIを使う方法を教えてくれました。

外部APIを利用して現在時刻を取得

JavaScriptで外部API(例えば、WorldTimeAPI)を利用して、正確な現在時刻を取得できます。
これにより、ユーザー端末の時刻ではなく、信頼できる時刻データを利用できます。

API利用の合体したスクリプトを以下になります。

<script>
  // 時間を2桁にフォーマットする関数
  function padWithZero(num) {
    return num < 10 ? '0' + num : num;
  }

  // カウントダウンの更新関数
  function updateCountdown(serverTime) {
    const countdownContainer = document.getElementById("countdown-container");
    const endTime = new Date("{{ section.settings.display_limit_time }}").getTime(); // 設定された終了時間

    const now = new Date(serverTime).getTime(); // サーバーから取得した現在時刻
    const remainingTime = endTime - now;

    if (remainingTime > 0) {
      const days = Math.floor(remainingTime / (1000 * 60 * 60 * 24));
      const hours = Math.floor((remainingTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
      const minutes = Math.floor((remainingTime % (1000 * 60 * 60)) / (1000 * 60));
      const seconds = Math.floor((remainingTime % (1000 * 60)) / 1000);

      // 時間を1桁のまま、日付、分、秒を2桁にする
      const displayHours = hours < 10 ? hours.toString() : padWithZero(hours);

      document.getElementById("countdown-day").innerHTML = `<span>${padWithZero(days)}</span>`;
      document.getElementById("countdown-time-end").innerHTML = `<span class="h">${displayHours}</span>:<span class="m">${padWithZero(minutes)}</span>:<span class="s">${padWithZero(seconds)}</span>`;
    } else {
      // カウントが終了した場合の表示
      countdownContainer.innerHTML = `
        <div class="count-time">
          <p>WEBSITE RENEWAL OPENING SOON.</p>
        </div>
      `;
    }
  }

  // サーバー時間を取得する関数
  function fetchServerTimeAndStartCountdown() {
    const apiUrl = "https://worldtimeapi.org/api/timezone/Asia/Taipei"; // 台湾時間を取得
    fetch(apiUrl)
      .then(response => response.json())
      .then(data => {
        const serverTime = data.datetime; // サーバーから取得した時刻
        setInterval(() => updateCountdown(serverTime), 1000); // カウントダウンを1秒ごとに更新
      })
      .catch(error => {
        console.error("サーバー時間の取得に失敗しました:", error);
        alert("時間情報を取得できませんでした。リロードしてください。");
      });
  }

  // ページ読み込み時にサーバー時間を取得してカウントダウンを開始
  window.onload = function() {
    fetchServerTimeAndStartCountdown();
  };
</script>

やはり注意点もあります。

注意点
・外部APIの可用性: 外部APIが利用できない場合、カウントダウンが動作しません。その場合のエラーハンドリング(例: ユーザーへの通知)を実装しています。
・通信遅延: API呼び出し時の通信遅延が生じる場合がありますが、通常は微小な誤差なので大きな影響はありません。
・WorldTimeAPIの制限: 無料APIなので、頻繁なアクセスがあると制限される場合があります。この場合、有料の時間同期サービスを検討してください。

あんまりアクセスあると、読み込まない、拒否されるといった可能性あるようです。まぁそうですよね。

無料なので文句も言えません。APIの使用はどう判断するか。

アクセスの少ないショボイサイトではこのAPIでも良いのではないでしょうか。
仕事で使うのはやめた方がいいかもです。

https://worldtimeapi.org/pages/faqs#commercial-apps
→抜粋
この API を商用アプリケーションに使用することは推奨されません 。APIは時々、比較的長期間にわたってダウンする可能性があります。SLA 、保証、直接的な資金提供は行われません。

この API を商用アプリケーションに使用する場合は、完全に自己責任で行ってください。開発は継続しており、頻繁に変更が行われており、安定性や稼働時間は保証されません。

どうしてもAPIで、しかも簡単にという場合は、超大手の
Google Time Zone APIが一番いいでしょうか。
※無料利用枠あり(毎月一定のリクエストが無料)。以降はリクエストごとの課金。

このくらいにしておこうと思います。

カウントダウンは、イベント内容によっては楽しい企画になりそうな気がしませんか。

【余談】ms(ミリセカンド)にしたい

カウントをミリセカンドにしたい。ようです。(デザインを見ると。。)
時間がないのに急に仕様を変えてきたり、そんな経験ありますでしょうか。

普通はそんなことないと思いますが、そういう会社も、うぅ。。
msも日の目を見る機会はもうないだろうと残しておきます。

sections/countdown-ms.liquid
台湾タイムゾーンになります。

<style>
.count-time-box .start {
  margin: 8px 0 0;
}
.tapei {
  font-size: 16px;
  letter-spacing: 0.03em;
}
.tapei span {
  font-size: 11px;
  letter-spacing: 0.05em;
  margin: 0 0 0 10px;
}
.cont-box {
  position: relative;
  padding: 6.65rem 11.4rem;
  height: 100svh;
  display: flex;
  justify-content: center;
  align-items: center;
}
.count-time-box {
  margin: 0 auto;
  width: 215px;
  min-width: 215px;
  font-family: Arial, Helvetica, 'Helvetica Neue', Arial, 'Lucida Grande', sans-serif;
  position: relative;
  bottom: -12.8%;
  padding: 20px 0 0;
}
.count-time-box .title {
  font-size: 11px;
  letter-spacing: 0.05em;
  line-height: 1.2;
  background-color: #25FF00;
  padding: 1px 0 0px;
}
.count-set {
  display: flex;
  margin-top: 3px;
}
.count-day {
  display: flex;
  width: 50%;
  font-size: 16px;
  letter-spacing: 0.03em;
  position: relative;
}
.count-day::before {
  content: "";
  position: absolute;
  top: 13px;
  left: 40px;
  width: 70px;
  height: 1px;
  background-color: #333;
}
.count-day span,
.count-time span {
  position: relative;
}
.count-day span::before {
  content: "DAYS";
  position: absolute;
  bottom: -15px;
  left: 0;
  width: auto;
  height: auto;
  color: #000;
  font-size: 8px;
  text-transform: none;
  transform: translateY(-50%) translateX(0%);
}
.count-time {
  width: 50%;
  font-size: 16px;
  letter-spacing: 0.03em;
  white-space: nowrap;
  text-align: right;
}
.count-time span.h::before {
  content: "";
  position: absolute;
  bottom: -18px;
  left: 50%;;
  width: auto;
  height: auto;
  color: #000;
  font-size: 10px;
  text-transform: none;
  transform: translate(-50%, 0%);
}
.count-time span.m::before {
  content: "";
  position: absolute;
  bottom: -18px;
  left: 50%;;
  width: auto;
  height: auto;
  color: #000;
  font-size: 10px;
  text-transform: none;
  transform: translate(-50%, 0%);
}
.count-time span.s::before {
  content: "";
  position: absolute;
  bottom: -18px;
  left: 50%;
  width: auto;
  height: auto;
  color: #000;
  font-size: 10px;
  text-transform: none;
  transform: translate(-50%, 0%);
}

@media (max-width: 1024px) {
  .set {
    height: auto;
    padding: 0 0 0;
  }
  .count-time-box {
    bottom: -11%;
  }
}
</style>

<section class="set">
  <div class="cont-box">
    <div class="rte">

      <div class="center">
        {% assign current_time = 'now' | date: '%s' %}
        {% assign setting_limit_time = section.settings.display_limit_time | date: '%s' %}

        {% assign displayable = true %}
        {% if current_time > setting_limit_time %}
          {% assign displayable = false %}
        {% endif %}

        <div class="count-time-box" id="countdown-container">
          <div class="start">
            <p class="tapei">10 MAY 2025<span>OPEN 12:00 (TST) </span></p>
          </div>
          <div class="count-set">
            <p id="countdown-day" class="count-day"></p>
            <p id="countdown-time-end" class="count-time"></p>
          </div>
        </div>
      </div>

    </div>
  </div>
</section>

<script>
function padWithZero(num) {
  return num < 10 ? '0' + num : num;
}

function updateCountdown() {
  var now = new Date();
  var countdownContainer = document.getElementById("countdown-container");

  var endTime = new Date("{{ section.settings.display_limit_time }}");
  // タイムゾーンを台湾に設定
  now.setUTCHours(now.getUTCHours() + 8); // UTC時間に8時間加算
  endTime.setUTCHours(endTime.getUTCHours() + 9); // UTC時間に8時間加算
  var remainingTime = endTime.getTime() - now.getTime(); // 残り時間を取得

  if (remainingTime > 0) {
    var days = Math.floor(remainingTime / (1000 * 60 * 60 * 24));
    var hours = Math.floor((remainingTime % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
    var minutes = Math.floor((remainingTime % (1000 * 60 * 60)) / (1000 * 60));
    var seconds = Math.floor((remainingTime % (1000 * 60)) / 1000);
    var milliseconds = Math.floor((remainingTime % 1000) / 10);

    // 時間は1桁のまま、日付、分、秒は2桁にする
    var displayHours = hours < 10 ? hours.toString() : padWithZero(hours);

    document.getElementById("countdown-day").innerHTML = `<span>${padWithZero(days)}</span>`;
    document.getElementById("countdown-time-end").innerHTML = 
      `<span class="h">${padWithZero(hours)}</span>:` +
      `<span class="m">${padWithZero(minutes)}</span>:` +
      `<span class="s">${padWithZero(seconds)}</span>:` +
      `<span class="ms">${padWithZero(milliseconds)}</span>`;
  } else {
    countdownContainer.innerHTML = `
      <div class="count-time" style="text-align: center; width: auto;">
        <p>WEBSITE RENEWAL</p>
        <p>OPENING SOON.</p>
      </div>
    `;
  }
}

window.onload = function() {
  setInterval(updateCountdown, 10);
};
</script>

{% schema %}
{
  "name": "Pages count down site",
  "settings": [
    {
      "type": "text",
      "id": "display_limit_time",
      "label": "Set Day",
      "default": "2040/01/01 01:00",
      "info": "Setting : YYYY/MM/DD HH:MM"
    }
  ]
}
{% endschema %}

詳しい解説はAIに放り込んで確認して下さい。

あと、面白いなと思ったのが、カウントが終わったらテキスト文に差し替えていますが、何かしら00:00で止まるなどの処理を入れないと、カウント時間が反転して、カウントアップしていきますよ。

00:00:00になったら自動で止まらないんですね。

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

この記事にピッタリなイラストのための考えたリクエストは、「オープン間近の仮想ショッピング店の大きな入り口の上には巨大なカウントダウンの数字の電光掲示板があり、急いで入店しようと、ジェットパック付きスーツ姿の3人のサラリーマンがジェットを吹かしながら飛び入りするシーン。」です。

そして選んだモードは以下の3つです。

  • 特撮
  • サイバーパンク風
  • UnrealEngine5風

もっと勢いよくしたかったですが、いい感じなのでこれで。

空中のお店とかにすれば、人間も飛んだのでは。

星間旅路のメロディ

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

一番いいのを頼む。

どうしたんですか。。久しぶりに聞きますか。

一番のさいわいとは。