開発blog-platform完了
ブログ記事カレンダービュー実装報告
実装ステータス: ✅ 完了
実装日: 2025年11月28日
ファイル: apps/web/app/components/BlogCalendar.vue
実装サマリー
Googleカレンダー風のUIでブログ記事を月次カレンダー表示する機能を実装しました。当初の計画から一部仕様を変更し、以下の特徴を持つカレンダービューが完成しました:
- ✅ 全記事を表示(制限なし)
- ✅ 記事タイトルの折り返し表示
- ✅ シンプルなデザイン(装飾なし)
- ✅ CSSグリッドレイアウトの最適化
1. プロジェクト概要
ブログインデックスページに、Googleカレンダー風のUIで記事を日付ごとに表示する新機能を追加する。
目的
- 時系列でブログ記事を視覚的に把握しやすくする
- 執筆活動の頻度を一目で確認できるようにする
- ユーザーが興味のある日付の記事を素早く見つけられるようにする
2. 現状分析
既存のブログインデックスページ
- 場所:
apps/web/app/pages/blog/index.vue - 表示形式: テーブル一覧 + ページネーション
- データソース:
queryContent()APIで記事を取得
記事データ構造
interface BlogArticle {
title: string
description: string
path: string
tags: string[]
publishedAt: string // YYYY-MM-DD形式
}
3. 提案機能
3.1 カレンダービュー
- 表示形式: 月次カレンダーグリッド(7列 × 5-6行)
- 日付セルの内容:
- 日付番号
- その日に公開された記事のタイトル(複数ある場合は全て表示)
- 記事タイトルはクリック可能で、記事ページへ遷移
- 現在の月: デフォルトで今月(2025年11月)を表示
3.2 ナビゲーション
- 前月/次月ボタン:
<>ボタンで月を切り替え - 月表示:
2025年 12月のような形式でヘッダーに表示
3.3 UI要素
- 今日の日付: 背景色やボーダーで強調表示
- 記事のある日: 記事タイトルを表示(複数ある場合はリスト形式)
- 記事のない日: 日付のみ表示
- 曜日ヘッダー: 月, 火, 水, 木, 金, 土, 日
4. 技術仕様
4.1 使用技術
- Vue 3 Composition API
- Nuxt Content API:
queryContent()で記事データ取得 - TypeScript: 型安全性の確保
- CSS Grid: カレンダーグリッドレイアウト
4.2 新規コンポーネント
BlogCalendar.vue
カレンダービュー本体のコンポーネント
Props:
interface Props {
articles: BlogArticle[] // 全記事データ
}
State:
const currentYear = ref<number>(new Date().getFullYear())
const currentMonth = ref<number>(new Date().getMonth()) // 0-11
interface CalendarDay {
date: number
isCurrentMonth: boolean
isToday: boolean
articles: BlogArticle[]
}
const calendarDays = computed<CalendarDay[]>(() => {
// カレンダーグリッド生成ロジック
})
Methods:
const goToPreviousMonth = () => {
if (currentMonth.value === 0) {
currentMonth.value = 11
currentYear.value--
} else {
currentMonth.value--
}
}
const goToNextMonth = () => {
if (currentMonth.value === 11) {
currentMonth.value = 0
currentYear.value++
} else {
currentMonth.value++
}
}
const getArticlesForDate = (year: number, month: number, date: number): BlogArticle[] => {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(date).padStart(2, '0')}`
return articles.filter(article => article.publishedAt === dateStr)
}
5. 実装手順
Step 1: BlogCalendarコンポーネントの作成
-
apps/web/app/components/BlogCalendar.vueを作成 - カレンダーグリッドの基本構造を実装
- 月の日数計算ロジックを実装
- 前月/次月の空白セルを含む42セル(6週間分)のグリッド生成
Step 2: データ連携
- 記事データを日付ごとにグループ化
- 各日付セルに記事タイトルを表示
- 記事タイトルのクリックで記事ページへ遷移
Step 3: ナビゲーション機能
- 前月/次月ボタンの実装
- 月・年の表示
- 今日の日付へジャンプする機能(実装済み)
Step 4: スタイリング
- Googleカレンダー風のデザイン
- レスポンシブ対応(モバイル・タブレット)
- 今日の日付の強調表示
- 記事のある日のホバーエフェクト
Step 5: ブログインデックスページへの統合
-
apps/web/app/pages/blog/index.vueにカレンダービューを追加 - テーブルビューとカレンダービューの切り替え機能(実装済み)
- デフォルトの表示モードを設定
Step 6: テストと最適化
- 各月の日数が正しく表示されるか確認
- うるう年の2月が正しく処理されるか確認
- 記事の多い日の表示を確認(UI崩れを修正済み)
- パフォーマンスの確認
6. コンポーネント設計
ファイル構成
apps/web/app/
├── components/
│ └── BlogCalendar.vue # 新規作成
├── pages/
│ └── blog/
│ └── index.vue # 既存ファイル(修正)
BlogCalendar.vueの構造
<template>
<div class="blog-calendar">
<!-- ヘッダー: 前月/次月ボタン + 年月表示 -->
<div class="calendar-header">
<button @click="goToPreviousMonth"><</button>
<h2>{{ currentYear }}年 {{ currentMonth + 1 }}月</h2>
<button @click="goToNextMonth">></button>
</div>
<!-- 曜日ヘッダー -->
<div class="calendar-weekdays">
<div v-for="day in weekdays" :key="day">{{ day }}</div>
</div>
<!-- カレンダーグリッド -->
<div class="calendar-grid">
<div
v-for="day in calendarDays"
:key="`${day.date}-${day.isCurrentMonth}`"
class="calendar-day"
:class="{
'is-today': day.isToday,
'has-articles': day.articles.length > 0,
'other-month': !day.isCurrentMonth
}"
>
<div class="date-number">{{ day.date }}</div>
<div class="articles-list">
<NuxtLink
v-for="article in day.articles"
:key="article.path"
:to="article.path"
class="article-link"
>
{{ article.title }}
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
7. データフロー
blog/index.vue
↓ queryContent().find()
↓ (全記事データ取得)
↓
BlogCalendar.vue (props: articles)
↓ computed: calendarDays
↓ (年月に基づいてカレンダーグリッド生成)
↓ getArticlesForDate()
↓ (日付ごとに記事をフィルタリング)
↓
カレンダーセル表示
↓ クリック
↓
記事ページへ遷移
8. UI/UX詳細
8.1 レイアウト
- カレンダーグリッド: CSS Grid(7列 × 6行)
- 最大幅: 1400px
- セルの最小高さ: 120px(記事タイトルが複数入る余裕を持たせる)
8.2 色とスタイル
- 今日の日付: 背景色を薄い青に設定(例:
#e3f2fd) - 記事のある日: 記事タイトルを青いテキストで表示
- 記事のない日: グレーの日付のみ
- 他月の日付: 薄いグレーで表示
- ホバーエフェクト: 記事タイトルにアンダーライン
8.3 レスポンシブ対応
- デスクトップ(>1024px): 7列グリッド
- タブレット(768px-1024px): 7列グリッド(フォントサイズ調整)
- モバイル(<768px): 簡易リスト表示、または横スクロール
9. 追加機能(オプション)
9.1 ビュー切り替え
- テーブルビューとカレンダービューの切り替えボタン
- ユーザーの好みを localStorage に保存
9.2 フィルタリング
- タグでフィルタリング
- カレンダー上で特定のタグの記事のみを表示
9.3 記事数バッジ
- 日付の隣に記事数を表示(例:
15 (3)→ 15日に3記事)
9.4 今日にジャンプ
- 「今日」ボタンで現在の月に戻る機能
10. 実装上の注意点
- 日付計算: JavaScriptの
Dateオブジェクトは月が0始まり(0-11)なので注意 - タイムゾーン: 記事の
publishedAtがUTC/JSTのどちらかを確認 - パフォーマンス: 記事数が多い場合、
computedで効率的にフィルタリング - アクセシビリティ: キーボードナビゲーション、スクリーンリーダー対応
- 記事が多い日の表示: 1日に10記事以上ある場合のUI(スクロール or 省略表示)
11. 参考実装
カレンダー生成の基本ロジック:
function generateCalendarDays(year: number, month: number): CalendarDay[] {
const firstDay = new Date(year, month, 1)
const lastDay = new Date(year, month + 1, 0)
const daysInMonth = lastDay.getDate()
const startDayOfWeek = firstDay.getDay() // 0=日曜, 1=月曜, ...
const days: CalendarDay[] = []
// 前月の日付で埋める
const prevMonthLastDay = new Date(year, month, 0).getDate()
for (let i = startDayOfWeek - 1; i >= 0; i--) {
days.push({
date: prevMonthLastDay - i,
isCurrentMonth: false,
isToday: false,
articles: []
})
}
// 当月の日付
const today = new Date()
for (let i = 1; i <= daysInMonth; i++) {
const isToday = year === today.getFullYear() &&
month === today.getMonth() &&
i === today.getDate()
days.push({
date: i,
isCurrentMonth: true,
isToday,
articles: getArticlesForDate(year, month, i)
})
}
// 次月の日付で埋める(42セルになるまで)
const remainingCells = 42 - days.length
for (let i = 1; i <= remainingCells; i++) {
days.push({
date: i,
isCurrentMonth: false,
isToday: false,
articles: []
})
}
return days
}
12. スケジュール
- Phase 1: BlogCalendarコンポーネント基本実装(2-3時間)
- Phase 2: データ連携とナビゲーション(1-2時間)
- Phase 3: スタイリングとレスポンシブ対応(2-3時間)
- Phase 4: テストと調整(1時間)
合計: 6-9時間
13. 成功基準
- カレンダーが正しく月次で表示される
- 前月/次月ナビゲーションが動作する
- 記事タイトルが正しい日付に表示される
- 記事タイトルクリックで記事ページに遷移する
- 今日の日付が強調表示される
- モバイルでも快適に閲覧できる
- パフォーマンスの問題がない(1000記事以上でも快適)
14. 実装結果と変更点
14.1 当初計画からの変更
実装中にユーザー要求により、以下の仕様変更を行いました:
記事表示数の制限撤廃
当初計画:
- 1日あたり3記事まで表示
- 4記事以上ある場合は「+N more」と表示
変更後:
- 全記事を表示(制限なし)
- セルの高さを自動調整(
grid-auto-rows: minmax(120px, auto))
記事タイトルの表示方法
当初計画:
- 1行に省略表示(
text-overflow: ellipsis) - ホバーで完全なタイトルを表示
変更後:
- 複数行に折り返し表示(
word-wrap: break-word) - 長いタイトルも全文読める
デザインのシンプル化
当初計画:
- 記事タイトルの前に青いドット(●)を表示
- リッチな装飾
変更後:
- ドットを削除してシンプルに
- 記事タイトルのみをブロック表示
14.2 技術的課題と解決策
課題1: CSSグリッド列幅の不均等問題
問題:
- 長い記事タイトルによってグリッド列が拡大
repeat(7, 1fr)を指定しても、一部の列幅が2000px以上に- 結果、画面右側の日付(30日、31日など)が表示されない
原因:
- CSSグリッドのデフォルト動作: グリッドアイテムは内容に応じて拡大される
min-width: auto(デフォルト)により、長いテキストが列を押し広げる
解決策:
/* グリッドアイテムがコンテンツで拡大されるのを防ぐ */
.calendar-day {
min-width: 0; /* 重要: グリッドアイテムの縮小を許可 */
}
.articles-list {
min-width: 0;
}
.article-link {
min-width: 0;
word-wrap: break-word;
overflow-wrap: break-word;
}
.article-title {
word-wrap: break-word;
overflow-wrap: break-word;
}
技術的解説:
min-width: 0はCSS Gridの重要なテクニック- デフォルトの
min-width: autoはコンテンツの最小幅を保証するが、グリッドレイアウトを崩す min-width: 0により、グリッドアイテムは親の制約内で縮小可能になる- 参考: CSS Grid Layout and The New
min-width: auto
課題2: 日付番号の視認性
問題:
- 初期実装で日付番号が小さく見づらい
- 記事リストに埋もれて視認性が低い
解決策:
.date-number {
font-size: 1.1rem; /* 0.85rem から拡大 */
font-weight: 600; /* セミボールド */
min-width: 30px;
min-height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
/* 今日の日付はさらに大きく */
.calendar-day.is-today .date-number {
min-width: 34px;
min-height: 34px;
background-color: #1a73e8;
color: #fff;
border-radius: 50%;
font-weight: 700;
}
課題3: 記事タイトルの折り返し
問題:
- 当初の
white-space: nowrapにより1行表示 - 長いタイトルが省略され、内容が分かりづらい
解決策:
.article-link {
display: block; /* flex から block に変更 */
word-wrap: break-word; /* 長い単語を折り返す */
overflow-wrap: break-word;
line-height: 1.3; /* 行間を調整 */
}
14.3 最終的な実装の特徴
- レスポンシブグリッド: 7列均等配置(
repeat(7, 1fr)) - 高さの自動調整: 記事数に応じてセルが伸縮
- 視認性の向上: 日付番号を大きく太く表示
- 情報量の最大化: 全記事を折り返し表示
- シンプルなデザイン: 不要な装飾を削除
14.4 パフォーマンス
- 記事総数: 100+ 記事
- 最大記事数/日: 12記事(2025年11月27日)
- レンダリング: 問題なし
- グリッド計算:
computedによる効率的なフィルタリング
14.5 今後の改善案
- 仮想スクロール: 記事数が1000件を超えた場合の最適化
- タグフィルター: カレンダー上で特定タグの記事のみ表示
- 年ビュー: 12ヶ月分のミニカレンダー表示
- 記事プレビュー: ホバーで記事の説明を表示
- エクスポート機能: カレンダーをPDF/画像でエクスポート
15. 学んだこと
- CSS Gridの
min-width: 0の重要性: グリッドアイテムの縮小を許可する必須テクニック - コンテンツ駆動のレイアウト: 固定高さより自動調整の方が柔軟
- 段階的な要件変更: 実装中のフィードバックで仕様が進化
- Chrome DevToolsの活用: 実際の表示を確認しながらデバッグ
16. 参考資料
実装完了: 2025年11月28日 作成者: Claude Code with AI Assistant