フルスタックチャンネル
サインアップサインアップ
ログインログイン
利用規約プライバシーポリシーお問い合わせ
Copyright © All rights reserved | FullStackChannel
受付中
サイドバーの新着記事、カテゴリーアーカイブ
チュートリアル
josei
2024/09/16 08:40

実現したいこと

ブログのサイドバーの新着記事、カテゴリー、アーカイブを表示したい。
https://gyazo.com/2b185bb5482ab265b1bd6cde9469e4f8

発生している問題

新着記事、カテゴリー、アーカイブが取得できていない。
https://gyazo.com/35da03bc7cea51dedf18aec04bfb3f0a
https://gyazo.com/aaad3994ee32f6fb434b31dd93f79f72

ソースコード

components/providers/QueryProvider.tsx

"use client"

import { useState } from "react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"

// クエリプロバイダ
const QueryProvider = ({ children }: { children: React.ReactNode }) => {
  const [queryClient] = useState(() => new QueryClient())

  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  )
}

export default QueryProvider

app/layout.tsx

import type { Metadata, Viewport } from "next";
import { M_PLUS_1 } from "next/font/google"
import QueryProvider from "@/components/providers/QueryProvider"
import "./globals.css";

const mPlus1 = M_PLUS_1({
  weight: ["400", "700", "900"],
  subsets: ["latin"],
})

export const metadata: Metadata = {
  title: {
    template: "ブログシステム",
    default: "ブログシステム",
  },
}

export const viewport: Viewport = {
  maximumScale: 1,
  userScalable: false,
}

interface RootLayoutProps {
  children: React.ReactNode
}

// ルートレイアウト
const RootLayout = async ({ children }: RootLayoutProps) => {
  return (
    <html lang="ja">
      <body className={mPlus1.className}>
        <QueryProvider>{children}</QueryProvider>
      </body>
    </html>
  )
}

export default RootLayout

types/index.ts

export interface AboutType {
  id: string
  content: string
  createdAt: string
  publishedAt: string
  updatedAt: string
}

export interface CategoryType {
  id: string
  name: string
  color: string
}

export interface BlogType {
  id: string
  title: string
  content: string
  image: {
    url: string
  }
  category: CategoryType
  ranking?: number
  isRecommended: boolean
  isSpecial: boolean
  createdAt: string
  publishedAt: string
  updatedAt: string
}

export interface ArchiveMonthType {
  year: number
  month: number
  count: number
}

export interface CategoryCountType {
  id: string
  name: string
  count: number
}

export interface SidebarData {
  latestBlogs: BlogType[]
  archiveMonths: ArchiveMonthType[]
  categoryCounts: CategoryCountType[]
}

/components/navigation/Sidebar.tsx

"use client"

import { useQuery } from "@tanstack/react-query"
import { format } from "date-fns"
import { microcms } from "@/lib/microcms"
import Image from "next/image"
import Link from "next/link"
import {
  BlogType,
  ArchiveMonthType,
  CategoryCountType,
  SidebarData,
} from "@/types"

// サイドバーのデータを取得する関数
const fetchSidebarData = async (): Promise<SidebarData> => {
  const allBlogs = await microcms.getList<BlogType>({
    endpoint: "blog",
    queries: {
      orders: "-publishedAt",
      limit: 1000,
    },
  })

  // 最新の5件を取得
  const latestBlogs = allBlogs.contents.slice(0, 5)

  // アーカイブの年月を取得
  const extractArchiveMonths = (blogs: BlogType[]): ArchiveMonthType[] => {
    const monthCounts = new Map<string, ArchiveMonthType>()
    blogs.forEach((blog) => {
      const date = new Date(blog.publishedAt || blog.createdAt)
      const year = date.getFullYear()
      const month = date.getMonth() + 1
      const key = `${year}-${month}`
      const current = monthCounts.get(key) || { year, month, count: 0 }
      monthCounts.set(key, { ...current, count: current.count + 1 })
    })
    return Array.from(monthCounts.values()).sort(
      (a, b) => b.year - a.year || b.month - a.month
    )
  }

  // カテゴリごとの記事数を取得
  const extractCategoryCounts = (blogs: BlogType[]): CategoryCountType[] => {
    const categoryCounts = new Map<string, CategoryCountType>()
    blogs.forEach((blog) => {
      const { id, name } = blog.category
      const current = categoryCounts.get(id) || { id, name, count: 0 }
      categoryCounts.set(id, { ...current, count: current.count + 1 })
    })
    return Array.from(categoryCounts.values()).sort((a, b) => b.count - a.count)
  }

  return {
    latestBlogs,
    archiveMonths: extractArchiveMonths(allBlogs.contents),
    categoryCounts: extractCategoryCounts(allBlogs.contents),
  }
}

// サイドバー
const Sidebar = () => {
  const { data: sidebarData, isLoading } = useQuery({
    queryKey: ["sidebarData"],
    queryFn: fetchSidebarData,
    staleTime: 1000 * 60 * 5, // 5分間キャッシュを保持
    refetchOnWindowFocus: true, // フォーカス時に再フェッチ
  })

  const latestBlogs = sidebarData?.latestBlogs || []
  const categoryCounts = sidebarData?.categoryCounts || []
  const archiveMonths = sidebarData?.archiveMonths || []

  return (
    <div className="space-y-10">
      <div className="border flex flex-col items-center justify-center p-5 space-y-5">
        <Link href="/about">
          <Image
            src="/default.png"
            width={120}
            height={120}
            alt="avatar"
            className="rounded-full"
            priority={false}
          />
        </Link>

        <div className="font-bold text-xl">Haru</div>

        <div className="text-sm">
          Next.jsとMicroCMSを使用したブログサイト構築チュートリアルです。技術ブログなど、すぐに運用できるようになっています。
        </div>
      </div>

      {/* 新着記事 */}
      <div className="space-y-5">
        <div className="font-bold border-l-4 border-black pl-2">新着記事</div>

        <div className="border text-sm">
          {isLoading ? (
            <div className="p-3 animate-pulse">Loading...</div>
          ) : (
            latestBlogs.map((blog, index, array) => (
              <Link
                href={`/blog/${blog.id}`}
                className={`grid grid-cols-3 hover:text-gray-500 group ${
                  index !== array.length - 1 ? "border-b" : ""
                }`}
                key={index}
              >
                <div className="col-span-1">
                  <div className="aspect-square relative overflow-hidden">
                    <Image
                      src={blog.image.url}
                      fill
                      alt="new blog"
                      className="object-cover transition-transform duration-100 ease-in-out group-hover:scale-105"
                      loading="lazy"
                      priority={false}
                      sizes="100%"
                    />
                  </div>
                </div>
                <div className="col-span-2">
                  <div className="p-5">{blog.title}</div>
                </div>
              </Link>
            ))
          )}
        </div>
      </div>

      {/* カテゴリ */}
      <div className="space-y-5">
        <div className="font-bold border-l-4 border-black pl-2">カテゴリ</div>

        <div className="border text-sm">
          {isLoading ? (
            <div className="p-3 animate-pulse">Loading...</div>
          ) : (
            categoryCounts.map((category, index, array) => (
              <Link
                href={`/category/${category.id}`}
                className={`p-3 flex items-center justify-between hover:text-gray-500 ${
                  index !== array.length - 1 ? "border-b" : ""
                }`}
                key={index}
              >
                <div>{category.name}</div>
                <div className="border py-1 px-4 text-sm">{category.count}</div>
              </Link>
            ))
          )}
        </div>
      </div>

      {/* アーカイブ */}
      <div className="space-y-5">
        <div className="font-bold border-l-4 border-black pl-2">アーカイブ</div>

        <div className="border text-sm">
          {isLoading ? (
            <div className="p-3 animate-pulse">Loading...</div>
          ) : (
            archiveMonths.map((archive, index, array) => {
              return (
                <Link
                  href={`/archive/${archive.year}/${archive.month}`}
                  className={`p-3 flex items-center justify-between hover:text-gray-500 ${
                    index !== array.length - 1 ? "border-b" : ""
                  }`}
                  key={index}
                >
                  <div>
                    {format(
                      new Date(archive.year, archive.month - 1),
                      "yyy年MM月"
                    )}
                  </div>
                  <div className="border py-1 px-4 text-sm">
                    {archive.count}
                  </div>
                </Link>
              )
            })
          )}
        </div>
      </div>
    </div>
  )
}

export default Sidebar

補足情報

以下のリポジトリのソースコードを参考に作成しました。
https://github.com/haruyasu/nextjs-microcms-blog-tutorial

回答 6件
login
回答するにはログインが必要です
はる@講師
10か月前

ご質問ありがとうございます。
allBlogsにmicroCMSから取得したデータが格納されてますでしょうか?
ご確認をお願いします。

  const allBlogs = await microcms.getList<BlogType>({
    endpoint: "blog",
    queries: {
      orders: "-publishedAt",
      limit: 1000,
    },
  })

  console.log("allBlogs: ", allBlogs.contents)
josei
10か月前

下記の行でコンソールエラー 400 (Bad Request)が出ていました。

const allBlogs = await microcms.getList<BlogType>({
1
はる@講師
10か月前

メインページでのブログ取得はうまくいっているのでしょうか?

app/(main)/page.tsx

// メインページ
const HomePage = async () => {
  const [latestBlogs, recommendedBlogs, specialBlogs] = await Promise.all([
    // 最新のブログ記事を取得
    microcms.getList<BlogType>({
      endpoint: "blog",
      queries: {
        orders: "-publishedAt",
        limit: 10,
      },
    }),
josei
10か月前

解決できました。
クエリパラメーターのlimit: 1000が問題だったようで上限値は100でして100にすればうまくいきました。
https://document.microcms.io/content-api/get-list-contents#h4cd61f9fa1

  const allBlogs = await microcms.getList<BlogType>({
    endpoint: "blog",
    queries: {
      orders: "-publishedAt",
      limit: 1000,
    },
  })
1
はる@講師
10か月前

ありがとうございます。
こちらのミスでした。申し訳ございませんでした。
Zennのチュートリアルは修正しておきます。
今後とも宜しくお願いいたします。

はる@講師
10か月前

Zennのチュートリアルを修正しました。
全件取得するためにgetAllContentsに変更しました。

  const allBlogs = await microcms.getAllContents<BlogType>({
    endpoint: "blog",
    queries: {
      orders: "-publishedAt",
    },
  })