feat: implement taxonomy data for categories, destinations, and tags with updated routing

This commit is contained in:
2026-03-19 17:19:44 +09:00
parent 78a554b672
commit beac1efd7f
12 changed files with 781 additions and 77 deletions
+101
View File
@@ -0,0 +1,101 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import { getCollection } from "astro:content";
import Headline from "@/components/ui/Headline.astro";
import PostItem from "@/components/blog/PostItem.astro";
import Pagination from "@/components/ui/Pagination.astro";
import type { Page } from "astro";
import type { CollectionEntry } from "astro:content";
import {
categoryMap,
getCategoryPath,
type TaxonomyItem,
} from "@/utils/taxonomy";
export async function getStaticPaths() {
const PAGE_SIZE = 6;
const allPosts = await getCollection("blog");
const posts = allPosts.filter((post) => post.data && post.data.pubDate);
const catSlugs = new Set<string>();
posts.forEach((post) =>
post.data.categories?.forEach((c) => catSlugs.add(c)),
);
return Array.from(catSlugs).flatMap((slug) => {
const catPosts = posts
.filter((post) => post.data.categories?.includes(slug))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
const fullPath = getCategoryPath(slug);
const cat = categoryMap.get(slug) ?? ({ slug, name: slug } as TaxonomyItem);
const totalPages = Math.max(1, Math.ceil(catPosts.length / PAGE_SIZE));
return Array.from({ length: totalPages }, (_, i) => {
const pageNum = i + 1;
return {
params: { rest: pageNum === 1 ? fullPath : `${fullPath}/${pageNum}` },
props: {
cat,
fullPath,
posts: catPosts.slice(i * PAGE_SIZE, (i + 1) * PAGE_SIZE),
currentPage: pageNum,
lastPage: totalPages,
total: catPosts.length,
prevUrl:
pageNum === 1
? undefined
: pageNum === 2
? `/category/${fullPath}`
: `/category/${fullPath}/${pageNum - 1}`,
nextUrl:
pageNum === totalPages
? undefined
: `/category/${fullPath}/${pageNum + 1}`,
},
};
});
});
}
const { cat, posts, currentPage, lastPage, total, prevUrl, nextUrl } =
Astro.props;
const page = {
currentPage,
lastPage,
url: { prev: prevUrl, next: nextUrl },
} as unknown as Page<CollectionEntry<"blog">>;
const metadata = {
title: cat.name,
description: cat.description ?? `Các bài viết trong danh mục ${cat.name}.`,
};
---
<BaseLayout metadata={metadata}>
<section class="relative px-4 py-8 sm:px-6 lg:px-8 md:py-12">
<div class="relative mx-auto max-w-5xl">
<Headline
tagline="Category"
title={cat.name}
subtitle={cat.description ?? `Hiển thị ${total} bài viết.`}
classes={{
container: "mb-12 text-center",
title:
"font-heading mb-4 text-4xl font-bold tracking-tight text-foreground md:text-6xl capitalized",
subtitle: "mx-auto max-w-3xl text-xl text-muted-foreground",
}}
/>
<div class="mb-8 text-center">
<a href="/blog" class="text-primary hover:underline">← Quay lại blog</a>
</div>
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{posts.map((post) => <PostItem post={post} />)}
</div>
<Pagination page={page} />
</div>
</section>
</BaseLayout>
@@ -5,6 +5,7 @@ import Headline from "@/components/ui/Headline.astro";
import PostItem from "@/components/blog/PostItem.astro";
import Pagination from "@/components/ui/Pagination.astro";
import type { GetStaticPathsOptions } from "astro";
import { categoryMap } from "@/utils/taxonomy";
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
const allPosts = await getCollection("blog");
@@ -30,9 +31,13 @@ export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
const { page } = Astro.props;
const { category } = Astro.params;
const categoryInfo = categoryMap.get(category!);
const displayName = categoryInfo?.name ?? category!;
const description = categoryInfo?.description;
const metadata = {
title: `Category: ${category}`,
description: `Articles in the ${category} category.`,
title: displayName,
description: description ?? `Các bài viết trong danh mục ${displayName}.`,
};
---
@@ -41,8 +46,8 @@ const metadata = {
<div class="relative mx-auto max-w-5xl">
<Headline
tagline="Category"
title={category}
subtitle={`Showing ${page.total} article${page.total === 1 ? "" : "s"}.`}
title={displayName}
subtitle={description ?? `Hiển thị ${page.total} bài viết.`}
classes={{
container: "mb-12 text-center",
title:
+102
View File
@@ -0,0 +1,102 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import { getCollection } from "astro:content";
import Headline from "@/components/ui/Headline.astro";
import PostItem from "@/components/blog/PostItem.astro";
import Pagination from "@/components/ui/Pagination.astro";
import type { Page } from "astro";
import type { CollectionEntry } from "astro:content";
import {
destinationMap,
getDestinationPath,
type TaxonomyItem,
} from "@/utils/taxonomy";
export async function getStaticPaths() {
const PAGE_SIZE = 6;
const allPosts = await getCollection("blog");
const posts = allPosts.filter((post) => post.data && post.data.pubDate);
const destSlugs = new Set<string>();
posts.forEach((post) =>
post.data.destination?.forEach((d) => destSlugs.add(d)),
);
return Array.from(destSlugs).flatMap((slug) => {
const destPosts = posts
.filter((post) => post.data.destination?.includes(slug))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
const fullPath = getDestinationPath(slug);
const dest =
destinationMap.get(slug) ?? ({ slug, name: slug } as TaxonomyItem);
const totalPages = Math.max(1, Math.ceil(destPosts.length / PAGE_SIZE));
return Array.from({ length: totalPages }, (_, i) => {
const pageNum = i + 1;
return {
params: { rest: pageNum === 1 ? fullPath : `${fullPath}/${pageNum}` },
props: {
dest,
fullPath,
posts: destPosts.slice(i * PAGE_SIZE, (i + 1) * PAGE_SIZE),
currentPage: pageNum,
lastPage: totalPages,
total: destPosts.length,
prevUrl:
pageNum === 1
? undefined
: pageNum === 2
? `/destination/${fullPath}`
: `/destination/${fullPath}/${pageNum - 1}`,
nextUrl:
pageNum === totalPages
? undefined
: `/destination/${fullPath}/${pageNum + 1}`,
},
};
});
});
}
const { dest, posts, currentPage, lastPage, total, prevUrl, nextUrl } =
Astro.props;
const page = {
currentPage,
lastPage,
url: { prev: prevUrl, next: nextUrl },
} as unknown as Page<CollectionEntry<"blog">>;
const metadata = {
title: dest.name,
description: dest.description ?? `Khám phá các bài viết về ${dest.name}.`,
};
---
<BaseLayout metadata={metadata}>
<section class="relative px-4 py-8 sm:px-6 lg:px-8 md:py-12">
<div class="relative mx-auto max-w-5xl">
<Headline
tagline="Destination"
title={dest.name}
subtitle={dest.description ?? `Hiển thị ${total} bài viết.`}
classes={{
container: "mb-12 text-center",
title:
"font-heading mb-4 text-4xl font-bold tracking-tight text-foreground md:text-6xl capitalized",
subtitle: "mx-auto max-w-3xl text-xl text-muted-foreground",
}}
/>
<div class="mb-8 text-center">
<a href="/blog" class="text-primary hover:underline">← Quay lại blog</a>
</div>
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{posts.map((post) => <PostItem post={post} />)}
</div>
<Pagination page={page} />
</div>
</section>
</BaseLayout>
@@ -1,64 +0,0 @@
---
import BaseLayout from "@/layouts/BaseLayout.astro";
import { getCollection } from "astro:content";
import Headline from "@/components/ui/Headline.astro";
import PostItem from "@/components/blog/PostItem.astro";
import Pagination from "@/components/ui/Pagination.astro";
import type { GetStaticPathsOptions } from "astro";
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
const allPosts = await getCollection("blog");
const posts = allPosts.filter((post) => post.data && post.data.pubDate);
const destinations = new Set<string>();
posts.forEach((post) => {
post.data.destination?.forEach((d) => destinations.add(d));
});
return Array.from(destinations).flatMap((destination) => {
const destinationPosts = posts
.filter((post) => post.data.destination?.includes(destination))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
return paginate(destinationPosts, {
params: { destination },
pageSize: 6,
});
});
}
const { page } = Astro.props;
const { destination } = Astro.params;
const metadata = {
title: `Destination: ${destination}`,
description: `Travel articles about ${destination}.`,
};
---
<BaseLayout metadata={metadata}>
<section class="relative px-4 py-8 sm:px-6 lg:px-8 md:py-12">
<div class="relative mx-auto max-w-5xl">
<Headline
tagline="Destination"
title={destination}
subtitle={`Showing ${page.total} article${page.total === 1 ? "" : "s"}.`}
classes={{
container: "mb-12 text-center",
title:
"font-heading mb-4 text-4xl font-bold tracking-tight text-foreground md:text-6xl capitalized",
subtitle: "mx-auto max-w-3xl text-xl text-muted-foreground",
}}
/>
<div class="mb-8 text-center">
<a href="/blog" class="text-primary hover:underline">← Quay lại blog</a>
</div>
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{page.data.map((post) => <PostItem post={post} />)}
</div>
<Pagination page={page} />
</div>
</section>
</BaseLayout>
+9 -4
View File
@@ -5,6 +5,7 @@ import Headline from "@/components/ui/Headline.astro";
import PostItem from "@/components/blog/PostItem.astro";
import Pagination from "@/components/ui/Pagination.astro";
import type { GetStaticPathsOptions } from "astro";
import { tagMap } from "@/utils/taxonomy";
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
const allPosts = await getCollection("blog");
@@ -30,9 +31,13 @@ export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
const { page } = Astro.props;
const { tag } = Astro.params;
const tagInfo = tagMap.get(tag!);
const displayName = tagInfo?.name ?? tag!;
const description = tagInfo?.description;
const metadata = {
title: `Posts tagged with '${tag}'`,
description: `Explore our articles about ${tag}.`,
title: displayName,
description: description ?? `Các bài viết được gắn thẻ ${displayName}.`,
};
---
@@ -41,8 +46,8 @@ const metadata = {
<div class="relative mx-auto max-w-5xl">
<Headline
tagline="Tag"
title={`#${tag}`}
subtitle={`Showing ${page.total} article${page.total === 1 ? "" : "s"}.`}
title={`#${displayName}`}
subtitle={description ?? `Hiển thị ${page.total} bài viết.`}
classes={{
container: "mb-12 text-center",
title: