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