refactor(blog) refactor blog home page

This commit is contained in:
javayhu 2025-06-17 11:28:20 +08:00
parent 7d5f4a52a8
commit ebeacae587
9 changed files with 104 additions and 54 deletions

View File

@ -26,6 +26,8 @@ export const docs = defineDocs({
/**
* Changelog
*
* title is required, but description is optional in frontmatter
*/
export const changelog = defineCollections({
type: 'doc',
@ -39,6 +41,8 @@ export const changelog = defineCollections({
/**
* Pages, like privacy policy, terms of service, etc.
*
* title is required, but description is optional in frontmatter
*/
export const pages = defineCollections({
type: 'doc',
@ -52,39 +56,48 @@ export const pages = defineCollections({
/**
* Blog authors
*
* description is optional, but we must add it to the schema
* description is optional in frontmatter, but we must add it to the schema
*/
export const author = defineCollections({
type: 'doc',
dir: 'content/author',
schema: frontmatterSchema.extend({
schema: z.object({
name: z.string(),
avatar: z.string(),
description: z.string().optional(),
}),
});
/**
* Blog categories
*
* description is optional in frontmatter, but we must add it to the schema
*/
export const category = defineCollections({
type: 'doc',
dir: 'content/category',
schema: frontmatterSchema.extend({
schema: z.object({
name: z.string(),
description: z.string().optional(),
}),
});
/**
* Blog posts
*
* dtitle is required, but description is optional in frontmatter
*/
export const blog = defineCollections({
type: 'doc',
dir: 'content/blog',
schema: frontmatterSchema.extend({
image: z.string(),
date: z.string().date(),
published: z.boolean().default(true),
categories: z.array(z.string()),
author: z.string(),
}),
schema: (ctx) => {
// console.log('ctx', ctx); // {source, path}
return frontmatterSchema.extend({
image: z.string(),
date: z.string().date(),
published: z.boolean().default(true),
categories: z.array(z.string()),
author: z.string(),
});
},
});

View File

@ -1,24 +1,28 @@
import { BlogCategoryFilter } from '@/components/blog/blog-category-filter';
import Container from '@/components/layout/container';
import type { NextPageProps } from '@/types/next-page-props';
import { allCategories } from 'content-collections';
import { categorySource } from '@/lib/docs/source';
import { getTranslations } from 'next-intl/server';
import type { PropsWithChildren } from 'react';
interface BlogListLayoutProps extends PropsWithChildren, NextPageProps {}
interface BlogListLayoutProps extends PropsWithChildren {
params: Promise<{ locale: string }>;
}
export default async function BlogListLayout({
children,
params,
}: BlogListLayoutProps) {
const resolvedParams = await params;
const { locale } = resolvedParams;
const { locale } = await params;
const t = await getTranslations('BlogPage');
// Filter categories by locale
const categoryList = allCategories.filter(
(category) => category.locale === locale
);
const language = locale as string;
const categoryList = categorySource.getPages(language).map((category) => ({
slug: category.slugs[0],
name: category.data.name,
description: category.data.description || '',
}));
// console.log('categoryList', categoryList);
return (
<div className="mb-16">

View File

@ -1,20 +1,25 @@
import { Skeleton } from '@/components/ui/skeleton';
import { LocaleLink } from '@/i18n/navigation';
import { PLACEHOLDER_IMAGE } from '@/lib/constants';
import { type BlogType, authorSource, categorySource } from '@/lib/docs/source';
import { formatDate } from '@/lib/formatter';
import type { Post } from 'content-collections';
import Image from 'next/image';
interface BlogCardProps {
post: Post;
locale: string;
post: BlogType;
}
export default function BlogCard({ post }: BlogCardProps) {
const publishDate = post.date;
const date = formatDate(new Date(publishDate));
export default function BlogCard({ locale, post }: BlogCardProps) {
const { date, title, description, image, author, categories } = post.data;
const publishDate = formatDate(new Date(date));
const blogAuthor = authorSource.getPage([author], locale);
const blogCategories = categorySource
.getPages(locale)
.filter((category) => categories.includes(category.slugs[0] ?? ''));
// Extract the slug parts for the Link component
const slugParts = post.slugAsParams.split('/');
const slugParts = post.slugs[0].split('/');
// console.log('BlogCard, slugParts', slugParts);
return (
@ -22,27 +27,27 @@ export default function BlogCard({ post }: BlogCardProps) {
<div className="group flex flex-col border rounded-lg overflow-hidden h-full">
{/* Image container - fixed aspect ratio */}
<div className="group overflow-hidden relative aspect-16/9 w-full">
{post.image && (
{image && (
<div className="relative w-full h-full">
<Image
src={post.image}
alt={post.title || 'image for blog post'}
title={post.title || 'image for blog post'}
src={image}
alt={title || 'image for blog post'}
title={title || 'image for blog post'}
className="object-cover hover:scale-105 transition-transform duration-300"
placeholder="blur"
blurDataURL={PLACEHOLDER_IMAGE}
fill
/>
{post.categories && post.categories.length > 0 && (
{blogCategories && blogCategories.length > 0 && (
<div className="absolute left-2 bottom-2 opacity-100 transition-opacity duration-300">
<div className="flex flex-wrap gap-1">
{post.categories.map((category, index) => (
{blogCategories.map((category, index) => (
<span
key={`${category?.slug}-${index}`}
key={`${category?.slugs[0]}-${index}`}
className="text-xs font-medium text-white bg-black bg-opacity-50 px-2 py-1 rounded-md"
>
{category?.name}
{category?.data.name}
</span>
))}
</div>
@ -58,7 +63,7 @@ export default function BlogCard({ post }: BlogCardProps) {
{/* Post title */}
<h3 className="text-lg line-clamp-2 font-medium">
<span
className="bg-linear-to-r from-green-200 to-green-100
className="bg-linear-to-r from-green-200 to-green-100
bg-[length:0px_10px] bg-left-bottom bg-no-repeat
transition-[background-size]
duration-500
@ -66,15 +71,15 @@ export default function BlogCard({ post }: BlogCardProps) {
group-hover:bg-[length:100%_10px]
dark:from-purple-800 dark:to-purple-900"
>
{post.title}
{title}
</span>
</h3>
{/* Post excerpt */}
<div className="mt-2">
{post.description && (
{description && (
<p className="line-clamp-2 text-sm text-muted-foreground">
{post.description}
{description}
</p>
)}
</div>
@ -84,20 +89,20 @@ export default function BlogCard({ post }: BlogCardProps) {
<div className="mt-4 pt-4 border-t flex items-center justify-between space-x-4 text-muted-foreground">
<div className="flex items-center gap-2">
<div className="relative h-8 w-8 shrink-0">
{post?.author?.avatar && (
{blogAuthor?.data.avatar && (
<Image
src={post?.author?.avatar}
alt={`avatar for ${post?.author?.name}`}
src={blogAuthor?.data.avatar}
alt={`avatar for ${blogAuthor?.data.name}`}
className="rounded-full object-cover border"
fill
/>
)}
</div>
<span className="truncate text-sm">{post?.author?.name}</span>
<span className="truncate text-sm">{blogAuthor?.data.name}</span>
</div>
<time className="truncate text-sm" dateTime={date}>
{date}
{publishDate}
</time>
</div>
</div>

View File

@ -1,10 +1,10 @@
import Container from '@/components/layout/container';
import type { Category } from 'content-collections';
import type { BlogCategory } from '@/types/blog-types';
import { BlogCategoryListDesktop } from './blog-category-list-desktop';
import { BlogCategoryListMobile } from './blog-category-list-mobile';
interface BlogCategoryFilterProps {
categoryList: Category[];
categoryList: BlogCategory[];
}
export function BlogCategoryFilter({ categoryList }: BlogCategoryFilterProps) {

View File

@ -3,12 +3,12 @@
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { LocaleLink } from '@/i18n/navigation';
import { cn } from '@/lib/utils';
import type { Category } from 'content-collections';
import type { BlogCategory } from '@/types/blog-types';
import { useTranslations } from 'next-intl';
import { useParams } from 'next/navigation';
export type BlogCategoryListDesktopProps = {
categoryList: Category[];
categoryList: BlogCategory[];
};
export function BlogCategoryListDesktop({

View File

@ -9,14 +9,14 @@ import {
DrawerTitle,
DrawerTrigger,
} from '@/components/ui/drawer';
import type { Category } from 'content-collections';
import type { BlogCategory } from '@/types/blog-types';
import { LayoutListIcon } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useParams } from 'next/navigation';
import { useState } from 'react';
export type BlogCategoryListMobileProps = {
categoryList: Category[];
categoryList: BlogCategory[];
};
export function BlogCategoryListMobile({

View File

@ -1,15 +1,17 @@
import type { Post } from 'content-collections';
import type { BlogType } from '@/lib/docs/source';
import EmptyGrid from '../shared/empty-grid';
import CustomPagination from '../shared/pagination';
import BlogGrid from './blog-grid';
interface BlogGridWithPaginationProps {
posts: Post[];
locale: string;
posts: BlogType[];
totalPages: number;
routePrefix: string;
}
export default function BlogGridWithPagination({
locale,
posts,
totalPages,
routePrefix,
@ -19,7 +21,7 @@ export default function BlogGridWithPagination({
{posts.length === 0 && <EmptyGrid />}
{posts.length > 0 && (
<div>
<BlogGrid posts={posts} />
<BlogGrid locale={locale} posts={posts} />
<div className="mt-8 flex items-center justify-center">
<CustomPagination
routePrefix={routePrefix}

View File

@ -1,19 +1,20 @@
import BlogCard, { BlogCardSkeleton } from '@/components/blog/blog-card';
import { websiteConfig } from '@/config/website';
import type { Post } from 'content-collections';
import type { BlogType } from '@/lib/docs/source';
interface BlogGridProps {
posts: Post[];
locale: string;
posts: BlogType[];
}
export default function BlogGrid({ posts }: BlogGridProps) {
export default function BlogGrid({ locale, posts }: BlogGridProps) {
// console.log('BlogGrid, posts', posts);
return (
<div>
{posts?.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{posts.map((post) => (
<BlogCard key={post.slug} post={post} />
<BlogCard key={post.slugs[0]} locale={locale} post={post} />
))}
</div>
)}

25
src/types/blog-types.ts Normal file
View File

@ -0,0 +1,25 @@
/**
* Blog Category
*
* we can not pass CategoryType from server component to client component
* so we need to define a new type, and use it in the client component
*/
export type BlogCategory = {
slug: string;
name: string;
description: string;
};
export type BlogAuthor = {
slug: string;
name: string;
description: string;
avatar: string;
};
export type BlogPost = {
slug: string;
title: string;
description: string;
date: string;
};