mirror of
https://github.com/10h30/blog-balodeplao.git
synced 2026-05-12 15:21:15 +09:00
feat: add Destinations component and integrate destination data in blog posts
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import Tags from "@/components/ui/Tags.astro";
|
import Tags from "@/components/ui/Tags.astro";
|
||||||
import Categories from "@/components/ui/Categories.astro";
|
import Categories from "@/components/ui/Categories.astro";
|
||||||
|
import Destinations from "@/components/ui/Destinations.astro";
|
||||||
import { type CollectionEntry, render } from "astro:content";
|
import { type CollectionEntry, render } from "astro:content";
|
||||||
import { toR2Url } from "@/utils/r2";
|
import { toR2Url } from "@/utils/r2";
|
||||||
import Picture from "@/components/ui/Picture.astro";
|
import Picture from "@/components/ui/Picture.astro";
|
||||||
@@ -89,5 +90,12 @@ const readingTime = remarkPluginFrontmatter?.minutesRead
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
post.data.destination && post.data.destination.length > 0 && (
|
||||||
|
<div class="mt-4 relative z-20">
|
||||||
|
<Destinations destinations={post.data.destination} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
export interface Props {
|
||||||
|
destinations: string[];
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { destinations, class: className = "text-sm" } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
destinations && Array.isArray(destinations) && (
|
||||||
|
<ul class:list={["flex flex-wrap gap-2", className]}>
|
||||||
|
{destinations.map((destination) => (
|
||||||
|
<li class="inline-block relative">
|
||||||
|
<a
|
||||||
|
href={`/destination/${destination}`}
|
||||||
|
class="inline-flex items-center gap-1 bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-700 dark:text-emerald-400 hover:text-emerald-800 dark:hover:text-emerald-300 px-3 py-1 rounded-full border border-emerald-500/20 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="w-3 h-3"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" />
|
||||||
|
<circle cx="12" cy="10" r="3" />
|
||||||
|
</svg>
|
||||||
|
{destination}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ const blog = defineCollection({
|
|||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
tags: z.array(z.string()).optional(),
|
tags: z.array(z.string()).optional(),
|
||||||
categories: z.array(z.string()).optional(),
|
categories: z.array(z.string()).optional(),
|
||||||
|
destination: z.array(z.string()).optional(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import BaseLayout from "@/layouts/BaseLayout.astro";
|
|||||||
import Schema from "@/components/seo/Schema.astro";
|
import Schema from "@/components/seo/Schema.astro";
|
||||||
import Tags from "@/components/ui/Tags.astro";
|
import Tags from "@/components/ui/Tags.astro";
|
||||||
import Categories from "@/components/ui/Categories.astro";
|
import Categories from "@/components/ui/Categories.astro";
|
||||||
|
import Destinations from "@/components/ui/Destinations.astro";
|
||||||
import { toR2Url } from "@/utils/r2";
|
import { toR2Url } from "@/utils/r2";
|
||||||
import Picture from "@/components/ui/Picture.astro";
|
import Picture from "@/components/ui/Picture.astro";
|
||||||
|
|
||||||
@@ -18,8 +19,16 @@ export async function getStaticPaths() {
|
|||||||
|
|
||||||
const { post } = Astro.props;
|
const { post } = Astro.props;
|
||||||
const { Content, remarkPluginFrontmatter } = await render(post);
|
const { Content, remarkPluginFrontmatter } = await render(post);
|
||||||
const { title, description, pubDate, author, image, categories, tags } =
|
const {
|
||||||
post.data;
|
title,
|
||||||
|
description,
|
||||||
|
pubDate,
|
||||||
|
author,
|
||||||
|
image,
|
||||||
|
categories,
|
||||||
|
tags,
|
||||||
|
destination,
|
||||||
|
} = post.data;
|
||||||
const coverImage = toR2Url(image);
|
const coverImage = toR2Url(image);
|
||||||
|
|
||||||
const formattedDate = pubDate.toLocaleDateString("vi-VN", {
|
const formattedDate = pubDate.toLocaleDateString("vi-VN", {
|
||||||
@@ -70,6 +79,13 @@ const metadata = {
|
|||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
{
|
||||||
|
destination && destination.length > 0 && (
|
||||||
|
<div class="flex flex-wrap justify-center gap-2 mt-4">
|
||||||
|
<Destinations destinations={destination} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
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>
|
||||||
Reference in New Issue
Block a user