feat: enhance blog structure with categories, tags, and localization updates

This commit is contained in:
Thuan Bui
2026-03-14 15:24:59 +07:00
parent ea37a4ece7
commit 6342b8a3aa
11 changed files with 66 additions and 41 deletions
+12 -3
View File
@@ -1,5 +1,6 @@
---
import Tags from "@/components/ui/Tags.astro";
import Categories from "@/components/ui/Categories.astro";
import { type CollectionEntry, render } from "astro:content";
export interface Props {
@@ -10,7 +11,7 @@ const { post } = Astro.props;
const { remarkPluginFrontmatter } = await render(post);
const readingTime = remarkPluginFrontmatter?.minutesRead
? `${Math.ceil(remarkPluginFrontmatter.minutesRead)} min read`
? `${Math.ceil(remarkPluginFrontmatter.minutesRead)} phút đọc`
: "";
---
@@ -34,7 +35,7 @@ const readingTime = remarkPluginFrontmatter?.minutesRead
{post.data.author}
</p>
<a
href={`/blog/${post.id}`}
href={`/${post.id}`}
class="mt-2 block before:absolute before:inset-0 before:z-10"
>
<p class="text-xl font-semibold text-foreground group-hover:underline">
@@ -55,7 +56,7 @@ const readingTime = remarkPluginFrontmatter?.minutesRead
<div class="flex space-x-1 text-sm text-muted-foreground">
<time datetime={post.data.pubDate.toISOString()}>
{
post.data.pubDate.toLocaleDateString("en-US", {
post.data.pubDate.toLocaleDateString("vi-VN", {
year: "numeric",
month: "long",
day: "numeric",
@@ -69,6 +70,14 @@ const readingTime = remarkPluginFrontmatter?.minutesRead
</div>
</div>
{
post.data.categories && (
<div class="mt-4 relative z-20 flex flex-wrap gap-2">
<Categories categories={post.data.categories} />
</div>
)
}
{
post.data.tags && (
<div class="mt-4 relative z-20">
+5 -12
View File
@@ -13,25 +13,18 @@ import { siteConfig } from "@/config/site";
</div>
<div class="flex items-center gap-6">
<a
href={siteConfig.socialLinks.twitter}
href={siteConfig.socialLinks.instagram}
target="_blank"
rel="noopener noreferrer"
class="text-muted-foreground transition-colors hover:text-primary"
>Twitter</a
>Instagram</a
>
<a
href={siteConfig.socialLinks.github}
href={siteConfig.socialLinks.facebook}
target="_blank"
rel="noopener noreferrer"
class="text-muted-foreground transition-colors hover:text-primary"
>GitHub</a
>
<a
href={siteConfig.socialLinks.discord}
target="_blank"
rel="noopener noreferrer"
class="text-muted-foreground transition-colors hover:text-primary"
>Discord</a
>Facebook</a
>
</div>
</div>
@@ -39,7 +32,7 @@ import { siteConfig } from "@/config/site";
class="mt-8 border-t border-border pt-8 text-center text-xs text-muted-foreground"
>
&copy; {new Date().getFullYear()}
{siteConfig.author}. All rights reserved.
{siteConfig.name}. All rights reserved.
</div>
</div>
</footer>
+1 -1
View File
@@ -28,7 +28,7 @@ if (type === "WebSite") {
"@type": "Person",
name: siteConfig.author,
},
sameAs: [siteConfig.socialLinks.twitter, siteConfig.socialLinks.github],
sameAs: [siteConfig.socialLinks.instagram, siteConfig.socialLinks.facebook],
};
} else if (type === "BlogPosting") {
schema = {
+25
View File
@@ -0,0 +1,25 @@
---
export interface Props {
categories: string[];
class?: string;
}
const { categories, class: className = "text-sm" } = Astro.props;
---
{
categories && Array.isArray(categories) && (
<ul class:list={["flex flex-wrap gap-2", className]}>
{categories.map((category) => (
<li class="inline-block relative">
<a
href={`/category/${category}`}
class="inline-block bg-primary/10 hover:bg-primary/20 text-primary hover:text-primary px-3 py-1 rounded-full border border-primary/20 transition-colors duration-200"
>
{category}
</a>
</li>
))}
</ul>
)
}
+1 -1
View File
@@ -28,7 +28,7 @@ const { prev, next } = url;
<span />
)}
<div class="flex items-center text-sm text-muted-foreground">
Página {currentPage} de {lastPage}
Trang {currentPage} / {lastPage}
</div>
{next ? (
<a
+1 -1
View File
@@ -13,7 +13,7 @@ const { tags, class: className = "text-sm" } = Astro.props;
{tags.map((tag) => (
<li class="inline-block relative">
<a
href={`/blog/tags/${tag}`}
href={`/tags/${tag}`}
class="inline-block bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground px-3 py-1 rounded-full border border-border transition-colors duration-200"
>
#{tag}
+8 -12
View File
@@ -2,25 +2,21 @@ import ogImage from "@/assets/og-image.png";
export const siteConfig = {
name: "Astro Starter Pro",
description:
"Starter template optimized for SEO and performance. A solid foundation to start your projects with best practices.",
url: "https://astrostarterpro.com",
lang: "en",
locale: "en_US",
author: "Devgelo",
twitter: "@Devgelo",
description: "Just another Astro blog",
url: "https://thuanbui.me",
lang: "vi",
locale: "vi_VN",
author: "Thuan Bui",
twitter: "@10h30",
ogImage: ogImage,
socialLinks: {
twitter: "https://twitter.com",
github: "https://github.com/devgelo-labs/astro-starter-pro",
discord: "https://discord.com",
instagram: "https://instagram.com/",
facebook: "https://facebook.com/",
},
navLinks: [
{ text: "Home", href: "/" },
{ text: "About", href: "/about" },
{ text: "Services", href: "/services" },
{ text: "Blog", href: "/blog" },
{ text: "Contact", href: "/contact" },
{ text: "Widgets", href: "/widgets" },
],
};
+2 -2
View File
@@ -5,12 +5,12 @@ const blog = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }),
schema: z.object({
title: z.string(),
description: z.string(),
description: z.string().optional(),
pubDate: z.coerce.date(),
author: z.string(),
image: z.string().optional(),
tags: z.array(z.string()).optional(),
category: z.string().optional(),
categories: z.array(z.string()).optional(),
}),
});
@@ -16,14 +16,14 @@ const { post } = Astro.props;
const { Content, remarkPluginFrontmatter } = await render(post);
const { title, description, pubDate, author, image } = post.data;
const formattedDate = pubDate.toLocaleDateString("es-ES", {
const formattedDate = pubDate.toLocaleDateString("vi-VN", {
year: "numeric",
month: "long",
day: "numeric",
});
const readingTime = remarkPluginFrontmatter?.minutesRead
? `${Math.ceil(remarkPluginFrontmatter.minutesRead)} min de lectura`
? `${Math.ceil(remarkPluginFrontmatter.minutesRead)} phút đọc`
: "";
const metadata = {
@@ -62,7 +62,11 @@ const metadata = {
{
image && (
<div class="reveal relative mb-8 h-96 w-full overflow-hidden rounded-xl shadow-lg">
<img src={image} alt={title} class="h-full w-full object-cover" />
<img
src={`/images/${image}`}
alt={title}
class="h-full w-full object-cover"
/>
</div>
)
}
@@ -10,9 +10,7 @@ export async function getStaticPaths() {
const categories = new Set();
posts.forEach((post) => {
if (post.data.category) {
categories.add(post.data.category);
}
post.data.categories?.forEach((tag) => categories.add(tag));
});
return Array.from(categories).map((category) => {
@@ -20,7 +18,7 @@ export async function getStaticPaths() {
params: { category: category as string },
props: {
category,
posts: posts.filter((post) => post.data.category === category),
posts: posts.filter((post) => post.data.categories === category),
},
};
});
@@ -50,7 +48,7 @@ const metadata = {
/>
<div class="mb-8 text-center">
<a href="/blog" class="text-primary hover:underline">← Back to blog</a>
<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">
@@ -48,7 +48,7 @@ const metadata = {
/>
<div class="mb-8 text-center">
<a href="/blog" class="text-primary hover:underline">← Back to blog</a>
<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">