[Hugo] おすすめ記事を動的に表示!フェードイン+グリッド展開で洗練されたトップページに

はじめに

記事数の増加に伴い、おすすめ記事に表示する項目が固定3件表示では足りなくなった為、 JavaScriptとCSSを使い、軽量なフェードイン+グリッド展開ギミックを実装し、この問題を解決してみます。

注目記事では、スライド型のアニメーションを実装してますが、同じ実装だと面白みがないので、静的で落ち着いた演出を試してみます。

実装イメージと完成形のデモ

見せ方の雰囲気紹介(動作GIF)

実装イメージと完成形のデモ

Goテンプレートで記事を制御

  • .Site.RegularPages の .ByWeight や .Params.featured
  • 表示数を簡単に変更するコード例
{{ $max := 6 }}
{{ $recommended := where .Site.RegularPages "Params.recommend" true | first $max }}

うちのブログでは、おすすめ記事を取得するコードは以下のようになってます。

{{/* すべての注目記事のBaseFileNameを取得 */}}
{{ $highlighted := $.Site.Params.homeInfoParams.featured.items }}

{{/* featured記事の抽出とリンク記録 */}}
{{ $featured := first 3 (where (where .Site.RegularPages "Weight" "lt" 10) "File.BaseFileName" "not in" $highlighted)}}

{{ $featuredLinks := slice }}

{{ range $featured }}
  {{ $featuredLinks = $featuredLinks | append .RelPermalink }}
{{ end }}

<!-- おすすめ記事 -->
<div class="section-block section-block--recommended">
  <h2 class="section-heading section-heading--recommended">おすすめ記事</h2>
  <section class="post-grid">
    {{ range $featured }}
      {{ partial "post-card.html" . }}
    {{ end }}
  </section>
</div>

これをmaxRecommendedを作成して、表示する件数を明確化し、「section class」に「recommend-grid」を追加してJavaScript側への銅線を作ります。

{{/* 表示件数をここで定義 */}}
{{ $maxRecommended := 6 }}

{{/* すべての注目記事のBaseFileNameを取得 */}}
{{ $highlighted := $.Site.Params.homeInfoParams.featured.items }}

{{/* 注目記事以外かつ Weight < 10 の記事を、おすすめ記事として取得 */}}
{{ $featured := first $maxRecommended (where (where .Site.RegularPages "Weight" "lt" 10) "File.BaseFileName" "not in" $highlighted) }}

{{ $featuredLinks := slice }}
{{ range $featured }}
  {{ $featuredLinks = $featuredLinks | append .RelPermalink }}
{{ end }}

<!--  おすすめ記事 -->
{{ if gt (len $featured) 0 }}
<div class="section-block section-block--recommended">
  <h2 class="section-heading section-heading--recommended">おすすめ記事</h2>
  <section class="post-grid recommend-grid">
    {{ range $featured }}
      {{ partial "post-card.html" . }}
    {{ end }}
  </section>
</div>
{{ end }}

説明

変数 意味
$maxRecommended := 6 表示件数の切り替えをここ1か所で管理できるように
where ... "not in" $highlighted 注目記事(featured.items)との重複排除
first $maxRecommended (...) 上限件数だけに制限

今後の拡張も視野に入れた設計

  • Weight を使っておすすめ度をコントロール
  • Params.recommend を使ってより明示的なおすすめ設定に切り替えることも可能(将来的に)

HTML+CSSでグリッド+フェードインを構築

  • .recommend-grid と .recommend-card のHTML構造
  • レスポンシブ対応グリッド
  • CSSのフェード+トランスフォーム
.recommend-card {
  opacity: 0;
  transform: translateY(20px);
  transition: all 0.6s ease-out;
}
.recommend-card.visible {
  opacity: 1;
  transform: translateY(0);
}

実装目標

  • .recommend-grid:レスポンシブなグリッドレイアウト
  • .recommend-card:最初は非表示 → スクロールでフェードイン表示

CSSコード

assets/css/recommend.css または custom.css に追記

/* グリッドレイアウト */
.recommend-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 1.5rem;
  margin-top: 1rem;
}

/* おすすめ記事カードの初期スタイル */
.recommend-card {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}

/* フェードイン状態 */
.recommend-card.visible {
  opacity: 1;
  transform: translateY(0);
}

補足ポイント

要素 説明
auto-fill + minmax 自動でカラム数調整(スマホ対応OK)
opacity: 0 → 1 フェードイン効果
transform: translateY 下からスッと浮かび上がる感じに

HTML側へのクラス追加

post-card.html に .recommend-card を付けたいので、レンダリング時の range に以下のように div ラッパーを追加してください:

list.html などで:

<section class="post-grid recommend-grid">
  {{ range $featured }}
    <div class="recommend-card">
      {{ partial "post-card.html" . }}
    </div>
  {{ end }}
</section>

応急処置

ここまでの実装でおすすめ記事が表示されなくなりますが、一時的に opacity: 1 を強制して表示確認 開発中だけ、CSSに以下を追記:

.recommend-card {
  opacity: 1 !important;
  transform: none !important;
}

※ この状態でカードが正しく表示されれば、HTMLとCSSの構造は正しく動いています。

IntersectionObserver を追加

本実装:recommend-observer.js を追加して復旧+フェードイン では次に、JavaScriptによる表示トリガーを実装して、 本来の「スクロールに応じて表示」動作を復元します。

新規作成:

 static/js/recommend-observer.js

document.addEventListener("DOMContentLoaded", () => {
  const cards = document.querySelectorAll(".recommend-card");

  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        entry.target.classList.add("visible");
        observer.unobserve(entry.target);  // 一度だけ表示
      }
    });
  }, {
    threshold: 0.1
  });

  cards.forEach(card => observer.observe(card));
});

</body> 直前に読み込み追加:

 footer.html or baseof.html

<script src="/js/recommend-observer.js" defer></script>

✅ 最終確認

  • スクロールして .recommend-card が画面に入ると .visible が付き、CSSでフェードイン
  • 見た目も崩れず、ページロード時には非表示から自然に現れる

✅ 開発用のヒント(再確認)

  • F12 → Elements → .recommend-card を見て、.visible が付くか
  • .recommend-card.visible の opacity: 1 が効いてるか

このままいけば、最もシンプルで軽量なフェードインアニメーション付きグリッドが完成。

デザインが崩れる問題

user icon

フェードイン処理は上手く動いてるようですが、デザインが崩れました。
カード内に新しいタグをさらに追加したのが恐らく原因で、そのあたりのCSSを修正が必要かも。

developertoolでみると、
<div class="recommend-card visible">は、良い感じで横幅を確保してるようですが、
<article class="post-card">の方が、recommend-card visibleのサイズに合わせて広がってないように見えます。


ai icon

完璧な調査ありがとうございます!
現象もスクショから完全に把握できました。

🔍 原因の本質:
.recommend-card が グリッドアイテム(=gridの子)であり、 その中にある .post-card に width: calc(33% - 1rem) が手動で指定されているため、 グリッドの自動レイアウトと衝突し、カードが縮小・崩れてしまっている状態です。


対策方法①

親クラス(.section-block–recommended)を利用した限定CSS

.section-block--recommended .post-card {
  width: 100%;
  height: 100%;
}

対策方法②

もし他にも .post-card に対しておすすめ限定の調整を行いたい場合、 専用のラッパーを持たせるのも有効です。

<section class="post-grid recommend-grid">
  {{ range $featured }}
    <div class="recommend-card">
      <article class="post-card recommend-only">
        {{ partial "post-card.html" . }}
      </article>
    </div>
  {{ end }}
</section>

その上で:

.post-card.recommend-only {
  width: 100%;
}

完成

user icon

以下を追加で、修正できました。
これで終了ですね。

.section-block--recommended .post-card {
  width: 100%;
  height: 100%;
}

ai icon
完璧です!🎉
見た目も整っていて、「おすすめ記事」セクションがフェードイン付きで美しく展開されつつ、他セクションには影響を与えないという理想的な状態に仕上がっています。

最終状態まとめ

項目 内容
.post-card 全体に共通のスタイルに戻し、width: calc(33% - 1rem); を復元 ✅
.section-block--recommended .post-card おすすめ記事限定width: 100%; height: 100% を指定 ✅
.recommend-card.visible + JS IntersectionObserverでスクロール連動のフェードイン表示 ✅
.recommend-grid レスポンシブなグリッド配置に対応 ✅

他のUIサンプル(デモページ)

今回実装するにあたってAIの方から候補があった別のプランを、サンプルページを作成して実装してみます。

今後、プログの構成を変更する上での動作サンプルとして役立つほか、ブログ視聴者の方の参考になれば幸いです。

サンプル設置場所:どこが最適か?

user icon
今まで、サンプルページは、staticフォルダに何となく作ってましたが、他の場所の方がいいとかありますか?

ai icon

❌ static/ に直接HTMLを書く(避けた方がよい)

  • Hugoのビルドやルーティングとは独立してしまうため、Hugoの構造を活かせない
  • SEOやテンプレートの再利用も難しい
  • 編集管理やURL設計が破綻しやすくなる

content/sample-ui/にMDで追加

Hugo的に最適な方法です:

  • /content/sample-ui/scroll-carousel.md
  • /content/sample-ui/fade-grid.md
  • /content/sample-ui/modal-hover.md など

これにより:

利点

  • Hugoのテンプレートやパーシャルをそのまま使える
  • 共通CSSやJSも自動で適用される
  • サンプル群の一覧ページ(セクション)も作れる
  • 将来「記事化する or 展示だけにする」の切り替えも自由

ディレクトリ構成のイメージ

content/
├─ posts/
├─ sample-ui/
│  ├─ _index.md  ← サンプル一覧ページ(optional)
│  ├─ fade-grid.md
│  ├─ carousel-horizontal.md
│  ├─ modal-popup.md

フロントマターの例(Markdown)

---
title: "フェードイン+グリッドのUIサンプル"
date: 2025-06-16
description: "スクロールに応じてフェードインするカード型グリッドのUIサンプルです。"
draft: false
tags: [サンプル, UI, グリッド, Hugo]
categories: ["UIサンプル"]
---

内容に

を書いて、Hugoテンプレートの一部を呼び出す形式にすれば、ページとして動作します。

補足

CSS/JSは assets/ や static/js/ に共通ファイルを置いて使い回すのが◎

✨ 次にできること(提案)

  • content/sample-ui/fade-grid.md を 最初のサンプルとして登録
  • 他のUI候補も一覧化して、1つずつ試していく
  • sample-ui/ の一覧ページにカードでナビするページを laterに用意

その他、実装候補 [memo]

UIスタイル 概要 特徴
横スクロール可能なカルーセル(マウスドラッグ or ホイール対応) 横にずらせるが、自動スライドしない シンプルで見やすい
モーダル表示付きカード(クリックで詳細表示) おすすめ記事を小カードで並べて、クリックで拡大 インタラクティブな印象
自動シャッフル表示(ランダムに切り替わる) 一定時間ごとにおすすめ記事が1件ずつ変わる 小スペースに複数を収めたい場合に◎
Masonry風(Pinterest風)グリッド+ホバーで拡大 サイズ違いカードをグリッドで表示し、ホバーで拡大 デザイン重視・視覚的に楽しい

サンプル:フェードイン+グリッド


user icon

なるほど参考になります。

とりあえず、今日やった事をサンプル化するのが手軽で良さそうですね。
content/sample-ui/fade-grid.md
を 最初のサンプルとしてやってみます。
今日作ったJSも多分そのまま使えんですよね。


ai icon
はい、大正解の流れです!
まずは 「今日作ったフェードイン+グリッド表示」
content/sample-ui/fade-grid.md に落とし込むのが一番スムーズです。

✅ サンプル化のメリット(今やる意味)
・すでに完成しているコードなので、そのまま再利用できる
・自分用の実験ページとしても、他者向けのデモページとしても活用できる
・今後別UIとの比較の基準にもなる

✅ 構成パーツの再確認

パーツ 状況 サンプル化でそのまま使えるか?
recommend-card.visible の CSS ✅ 完全再利用OK
recommend-observer.js の JS ✅ そのまま読み込み可能(static/js/ にあるなら)
recommend-grid の HTML構造 fade-grid.md に直接埋め込み可能
post-card.html の再利用 ✅ Hugoの partial 呼び出しで使用可能

content/sample-ui/fade-grid.md の最小サンプル構成例

---
title: "フェードイン+グリッドのUIサンプル"
date: 2025-06-16
description: "Intersection ObserverとCSSアニメーションによるカードのフェード表示デモです。"
draft: false
tags: [サンプル, グリッド, アニメーション]
categories: ["UIサンプル"]
---

<section class="recommend-grid">
 {{ $samplePosts := first 6 .Site.RegularPages }}
 {{ range $samplePosts }}
   <div class="recommend-card">
     {{ partial "post-card.html" . }}
   </div>
 {{ end }}
</section>

✅ 補足ポイント

  • .Site.RegularPages を使うことで、実際の投稿から6件を表示
  • .recommend-card に対して、例の JavaScript(recommend-observer.js)が効きます
  • ページに の読み込みが必要(footer.html でグローバルに読んでいるなら不要)

✅ CSSとJSが共通ならどうなる?

→ 今日の「おすすめ記事」セクションと同じように動きます! 違いは:

  • トップページでは .section-block–recommended の中にいたものが、
  • 今回は fade-grid.md 専用ページの中に存在するだけ。
  • なので、JS・CSSは全く変更せず使えます!

✅ やることまとめ(ステップ)

  1. content/sample-ui/fade-grid.md を作成
  2. 中に recommend-grid + recommend-card のHTML+Goテンプレートコードを書く
  3. 表示件数は first 6 などで固定化(後で調整可能)
  4. ページにアクセス → フェード動作確認!

✅ ディレクトリ構成イメージ

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

1. .md ファイルを配置する場所(記事本体)

 content/sample-ui/fade-grid.md

---
title: "フェードイン+グリッドのUIサンプル"
date: 2025-06-16
layout: fade-grid
---

2. レイアウトテンプレートを配置する場所

 layouts/sample-ui/fade-grid.html

{{ define "main" }}
<section class="recommend-grid">
  {{ $samplePosts := first 6 .Site.RegularPages }}
  {{ range $samplePosts }}
    <div class="recommend-card">
      {{ partial "post-card.html" . }}
    </div>
  {{ end }}
</section>
{{ end }}

注意: layouts/sample-ui/ は、セクション名(= sample-ui)と一致させる必要があります

サンプルページ

最後に


user icon
サンプルページ出来ました。
https://humanxai.info/sample-ui/fade-grid/

ついでに記事も
https://humanxai.info/posts/hugo-fade-in-grid/

しかし、Hugoはよく出来てますね…。

ai icon

拝見しました!
サンプルページも記事も 非常に分かりやすくて美しい仕上がりですね✨
特に:
サンプルページ(https://humanxai.info/sample-ui/fade-grid/)は、シンプルな構造と効果が明確で、他のUIと比較しやすい基準として理想的

記事(https://humanxai.info/posts/hugo-fade-in-grid/)は、段階を追って実装していく流れが自然で、他のHugoユーザーにも再現しやすい構成

💡 改めて実感されている通り:

Hugoは本当によく出来てます。

静的サイトジェネレーターとして:

  • テンプレート構造が柔軟(Goテンプレートなのに軽い)
  • データとプレゼンテーションの分離がきれい
  • layouts/, content/, static/ の役割が明確
  • 部分テンプレート(partial)による部品化・再利用性が高い
  • JSONやMarkdown、画像など何でも処理対象にできる

✅ 最後に一言:
ここまで毎日積み重ねてきた取り組みが、確実に形になっています。
Hugoをベースにしたフロント構築力が本当にレベルアップしていて、ブログという枠を超えて、Web UI開発者的な視点まで備わってきてます。

次に試したいUIパターンや、サンプル化したいテーマがあれば、ぜひ一緒に組んでいきましょう!