[Hugo] 一定時間ごとにおすすめ記事が1件ずつ変わるシャッフル表示を実装&紹介。小スペースに複数を収めたい場合に◎。

はじめに

Hugoのカスタム記事で、おすすめ記事を動的に表示!フェードイン+グリッド展開する実装をやりましたが、そこで出来なかった別プランのサンプルを作成して見ます。

詳細は、以下の記事を参照ください。

おさらい

Hugoでサンプルページを作成する際のディレクトリ構成。

my-blog/
├─ content/
│  └─ sample-ui/
│     └─ shuffle-card.md      ✅ ← Markdown記事本体
├─ layouts/
│  └─ sample-ui/
│     └─ shuffle-card.html    ✅ ← テンプレート(Goテンプレート構文OK)

2つのファイルを作成。

 content/sample-ui/shuffle-card.md
 layouts/sample-ui/shuffle-card.html

シャッフル表示UI:概要と特徴

項目 内容
表示形式 1件(または少数)ずつ表示する
更新方法 一定時間ごとに次のカードに切り替え
UI効果 ランダム切り替え/フェード/スライドなどを組み合わせ可能
メリット 省スペースで複数記事を目立たせられる、ページを縦に伸ばさない

サンプルパターン(ベタ書き)

おすすめ記事をJavaScriptで1件ずつ表示 → 数秒ごとに切り替え

HTML(例)

<div id="shuffle-feature" class="recommend-card">
  <!-- 最初のカードがここに挿入される -->
</div>
document.addEventListener("DOMContentLoaded", () => {
  const articles = [
    {
      title: "記事A",
      image: "/images/a.png",
      url: "/posts/a/",
      summary: "記事Aの要約です。"
    },
    {
      title: "記事B",
      image: "/images/b.png",
      url: "/posts/b/",
      summary: "記事Bの要約です。"
    },
    {
      title: "記事C",
      image: "/images/c.png",
      url: "/posts/c/",
      summary: "記事Cの要約です。"
    },
  ];

  const target = document.getElementById("shuffle-feature");

  let current = 0;
  function showArticle(index) {
    const a = articles[index];
    target.innerHTML = `
      <a href="${a.url}" class="post-card">
        <img src="${a.image}" alt="${a.title}">
        <h3>${a.title}</h3>
        <p>${a.summary}</p>
      </a>
    `;
  }

  showArticle(current);

  setInterval(() => {
    current = (current + 1) % articles.length;
    showArticle(current);
  }, 5000); // 5秒ごとに切り替え
});

 content/sample-ui/shuffle-card.md

---
title: "シャッフル表示のUIサンプル"
date: 2025-06-15T18:00:00.000Z
layout: shuffle-card
description: "一定時間ごとに記事カードが切り替わるシャッフルUIサンプルです。"
draft: false
tags: [サンプル, シャッフル, アニメーション]
categories: ["UIサンプル"]
---

<!--このページは、`layouts/sample-ui/shuffle-card.html` テンプレートを使って構成されています。-->

 layouts/sample-ui/shuffle-card.html

{{ define "main" }}
<div class="section-block">
  <h2 class="section-heading">シャッフル表示のUIサンプル</h2>
  <div id="shuffle-feature" class="recommend-card">
    <!-- JSがここに注入する -->
  </div>
</div>
{{ end }}

 layouts/partials/footer.html

<!-- シャッフルUI -->
{{ if eq .Layout "shuffle-card" }}
  <script src="/js/shuffle-feature.js" defer></script>
{{ end }}

サンプル動画

シャッフルUIサンプル

フェード切り替えを追加

現状だと、表示が瞬時に切り替わり、硬いイメージがあるのでフェード切り替えを追加。

<div id="shuffle-feature" class="recommend-card">
  <a class="post-card fade-card">
    <img src="/images/a.png" alt="タイトル">
    <h3>タイトル</h3>
    <p>説明</p>
  </a>
</div>
.fade-card {
  opacity: 0;
  transition: opacity 0.5s ease-in-out;
}

.fade-card.show {
  opacity: 1;
}
document.addEventListener("DOMContentLoaded", () => {
  const articles = [
    { title: "記事A", image: "/images/a.png", url: "/posts/a/", summary: "Aの概要" },
    { title: "記事B", image: "/images/b.png", url: "/posts/b/", summary: "Bの概要" },
    { title: "記事C", image: "/images/c.png", url: "/posts/c/", summary: "Cの概要" },
  ];

  const card = document.querySelector("#shuffle-feature .fade-card");

  let current = 0;

  function showArticle(index) {
    const a = articles[index];

    // 一旦フェードアウト
    card.classList.remove("show");

    setTimeout(() => {
      card.href = a.url;
      card.querySelector("img").src = a.image;
      card.querySelector("img").alt = a.title;
      card.querySelector("h3").textContent = a.title;
      card.querySelector("p").textContent = a.summary;

      // フェードイン
      card.classList.add("show");
    }, 300); // フェードアウト後に差し替え
  }

  showArticle(current);

  setInterval(() => {
    current = (current + 1) % articles.length;
    showArticle(current);
  }, 4000);
});

効果の感じ方を強めるなら

.fade-card {
  opacity: 0;
  transform: scale(0.95);
  transition: opacity 0.6s ease-in-out, transform 0.6s ease-in-out;
}

.fade-card.show {
  opacity: 1;
  transform: scale(1);
}

完成版

上記の実装では、表示しているカードを一度に変更してしまう為、表示してるカードの中から設定枚数のみをランダムに切り替えるように変更しました。

document.addEventListener("DOMContentLoaded", () => {
  const articles = [
    { title: "記事A", image: "/images/a.webp", url: "/posts/a/", summary: "Aの概要" },
    { title: "記事B", image: "/images/b.webp", url: "/posts/b/", summary: "Bの概要" },
    { title: "記事C", image: "/images/c.webp", url: "/posts/c/", summary: "Cの概要" },
    { title: "記事D", image: "/images/d.webp", url: "/posts/d/", summary: "Dの概要" },
    { title: "記事E", image: "/images/e.webp", url: "/posts/e/", summary: "Eの概要" },
    { title: "記事F", image: "/images/f.webp", url: "/posts/f/", summary: "Fの概要" },
    { title: "記事G", image: "/images/g.webp", url: "/posts/g/", summary: "Gの概要" },
    { title: "記事H", image: "/images/h.webp", url: "/posts/h/", summary: "Hの概要" },
    { title: "記事I", image: "/images/i.webp", url: "/posts/i/", summary: "Iの概要" }
  ];

  const cards = document.querySelectorAll('#shuffle-feature .fade-card');
  let currentIndices = [];

  // ✅ 初期表示を遅らせて DOM が完全に構築された後に描画
  setTimeout(() => {
    cards.forEach((card, i) => {
      const a = articles[i];
      if (!a) return;

      card.href = a.url;
      const img = card.querySelector("img");
      const h3 = card.querySelector("h3");
      const p = card.querySelector("p");

      if (img) {
        img.src = a.image;
        img.alt = a.title;
      }
      if (h3) h3.textContent = a.title;
      if (p) p.textContent = a.summary;

      currentIndices[i] = i;
      card.classList.add("show");
    });
  }, 100); // 100ms後に描画(必要に応じて増やせます)

  function shuffleSomeCards() {
    const availableIndices = articles
      .map((_, i) => i)
      .filter(i => !currentIndices.includes(i));

    if (availableIndices.length === 0) return;

    const shuffleCount = Math.min(3, availableIndices.length);
    const targets = getRandomIndices(cards.length, shuffleCount);
    const newArticles = getRandomItems(availableIndices, shuffleCount);

    targets.forEach((cardIndex, i) => {
      const articleIndex = newArticles[i];
      const card = cards[cardIndex];
      const a = articles[articleIndex];

      card.classList.remove("show");

      setTimeout(() => {
        card.href = a.url;
        const img = card.querySelector("img");
        const h3 = card.querySelector("h3");
        const p = card.querySelector("p");

        if (img) {
          img.src = a.image;
          img.alt = a.title;
        }
        if (h3) h3.textContent = a.title;
        if (p) p.textContent = a.summary;

        card.classList.add("show");
        currentIndices[cardIndex] = articleIndex;
      }, 300);
    });
  }

  function getRandomIndices(max, count) {
    const indices = [];
    while (indices.length < count) {
      const r = Math.floor(Math.random() * max);
      if (!indices.includes(r)) indices.push(r);
    }
    return indices;
  }

  function getRandomItems(source, count) {
    const items = [];
    while (items.length < count) {
      const r = source[Math.floor(Math.random() * source.length)];
      if (!items.includes(r)) items.push(r);
    }
    return items;
  }

  setInterval(shuffleSomeCards, 4000);
});

動作サンプル

サンプルページ

コード解説

user icon
ありがとうございます。
現在のコードの仕様をブログ記事で解説用に簡単に説明してもらっていいでしょうか?

ai icon
どういたしまして!
以下に現在の shuffle-feature.js の仕様を、ブログ記事向けにわかりやすく整理した解説文をご用意しました。

✅ 自動シャッフル表示機能の仕組み解説

この実装では、表示中の複数記事カードを一定間隔でランダムに1〜3枚だけ切り替える動きを実現しています。

🔧 使用技術

  • JavaScript(Vanilla JS)
  • CSSトランジションによるフェードアニメーション
  • HTML構造:6枚のカード(.fade-card)を設置

🧠 実装の基本設計

要素 内容
articles 表示候補の9件の記事データ配列
.fade-card(6枚) 表示枠。1枚ずつHTML内に設置し、JavaScriptで中身を差し替える
currentIndices 現在表示されている記事のインデックス番号(重複表示を防ぐ)
setInterval() 4秒ごとにランダムで1〜3枚のカードを別の未表示記事と差し替え
setTimeout()(初期表示) 初回描画時にDOM構築完了を待ってから記事を順番に埋め込む

⚙️ 処理の流れ

  1. 初期化

    • articles[0]〜articles[5]を使って、6枚のカードを順番に埋める
    • currentIndicesにその表示状態を記録
  2. 定期シャッフル(4秒ごと)

    • articlesの中でまだ表示されていない記事を抽出
    • ランダムで最大3件選び、既存のカードのうち同数を置き換え
    • フェードアウト → 中身差し替え → フェードイン で自然な切り替えを演出
  3. 表示中の記録更新

    • 差し替えたカードのcurrentIndicesも更新して、次のシャッフルで重複しないように管理

ビジュアル効果(CSS)

.fade-card {
  opacity: 0;
  transition: opacity 0.5s ease;
}
.fade-card.show {
  opacity: 1;
}

💡 この実装の特徴

  • 一度にすべてを切り替えないため、閲覧者の注意を引きすぎず自然な変化ができる
  • 重複表示を防ぐ工夫により、最大9記事までを循環的に見せることが可能
  • Hugo + JavaScript の静的サイトでも、動的コンテンツのようなインタラクションを実現
user icon
ありがとうございます。
次は完成した物を、ブログのトップページ(おすすめ記事)で動くように実装ですね。

ai icon
素晴らしいです!
では次のステップ「トップページのおすすめ記事セクションへの実装」に進みましょう。

実装(トップページ)

おすすめ記事をシャッフル表示できるように作り変えます。