ブログのサイドバーの新着記事、カテゴリー、アーカイブを表示したい。
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
ご質問ありがとうございます。
allBlogsにmicroCMSから取得したデータが格納されてますでしょうか?
ご確認をお願いします。
const allBlogs = await microcms.getList<BlogType>({
endpoint: "blog",
queries: {
orders: "-publishedAt",
limit: 1000,
},
})
console.log("allBlogs: ", allBlogs.contents)
下記の行でコンソールエラー 400 (Bad Request)が出ていました。
const allBlogs = await microcms.getList<BlogType>({
メインページでのブログ取得はうまくいっているのでしょうか?
app/(main)/page.tsx
// メインページ
const HomePage = async () => {
const [latestBlogs, recommendedBlogs, specialBlogs] = await Promise.all([
// 最新のブログ記事を取得
microcms.getList<BlogType>({
endpoint: "blog",
queries: {
orders: "-publishedAt",
limit: 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,
},
})