feat(i18n): implement comprehensive blog post enhancements

- Add avatar to personal info in data.ts
- Remove redundant headings from blog posts
- Reorder imports in utils.ts for consistency
- Implement new blog layout components including:
  - PostMeta for displaying post metadata
  - TableOfContents for navigation
  - BlogNavigation for post pagination
  - ShareButtons for social sharing
  - AuthorCard for author information
- Enhance BlogPostLayout with:
  - Improved typography and spacing
  - Responsive sidebar layout
  - Dark mode support
  - Better code block styling
- Remove outdated i18n guide documentation
- Add comprehensive styling for all new components
This commit is contained in:
joyzhao
2025-06-17 19:37:36 +08:00
parent d22174e0dc
commit e5497e5e6d
18 changed files with 894 additions and 387 deletions

View File

@@ -0,0 +1,124 @@
---
const currentPath = Astro.url.pathname;
const isZh = currentPath.includes('/zh/');
const lang = isZh ? 'zh' : 'en';
const currentSlug = currentPath.split('/').filter(Boolean).pop();
const enPosts = await Astro.glob('../../pages/blog/posts/*.md');
const zhPosts = await Astro.glob('../../pages/zh/blog/posts/*.md');
const allPosts = isZh ? zhPosts : enPosts;
interface BlogPost {
title: string;
slug: string;
url: string;
frontmatter: {
title: string;
description?: string;
pubDate?: string;
date?: string;
readTime?: string;
tags?: string[];
category?: string;
};
}
const posts: BlogPost[] = allPosts.map((post) => {
const slug = post.url?.split('/').filter(Boolean).pop() || '';
return {
title: post.frontmatter.title,
slug: slug,
url: post.url || '',
frontmatter: {
title: post.frontmatter.title,
description: post.frontmatter.description,
pubDate: post.frontmatter.pubDate,
date: post.frontmatter.date || post.frontmatter.pubDate,
readTime: post.frontmatter.readTime,
tags: post.frontmatter.tags,
category: post.frontmatter.category
}
};
});
const sortedPosts = posts
.filter(post => post.frontmatter.date || post.frontmatter.pubDate)
.sort((a, b) => {
const dateA = new Date(a.frontmatter.date || a.frontmatter.pubDate || '').getTime();
const dateB = new Date(b.frontmatter.date || b.frontmatter.pubDate || '').getTime();
return dateB - dateA;
});
const currentIndex = sortedPosts.findIndex((post) => post.slug === currentSlug);
const nextPost = currentIndex > 0 ? sortedPosts[currentIndex - 1] : null;
const prevPost = currentIndex < sortedPosts.length - 1 ? sortedPosts[currentIndex + 1] : null;
const prevText = lang === 'zh' ? '上一篇' : 'Previous Post';
const nextText = lang === 'zh' ? '下一篇' : 'Next Post';
---
<nav class="mt-8 flex flex-row gap-2 w-full p-6 max-xl:p-3 max-lg:p-2" role="navigation" aria-label="Blog post navigation">
{prevPost && (
<a
href={prevPost.url}
style="width: -webkit-fill-available"
class="relative flex min-w-1/2 items-center justify-start gap-2 font-semibold dark:text-white text-black text-left text-pretty text-sm sm:text-base lg:text-lg leading-relaxed hover:text-purple-500 hover:[text-shadow:1px_1px_11px_rgba(168,85,247,0.7)] transition-all duration-300 before:absolute before:-top-6 before:left-0 before:text-xs sm:before:text-sm before:font-light before:content-['Previous_Post'] hover:before:text-purple-400"
tabindex="0"
aria-label={`${prevText}: ${prevPost.frontmatter.title}`}
>
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.85355 3.85355C7.04882 3.65829 7.04882 3.34171 6.85355 3.14645C6.65829 2.95118 6.34171 2.95118 6.14645 3.14645L2.14645 7.14645C1.95118 7.34171 1.95118 7.65829 2.14645 7.85355L6.14645 11.8536C6.34171 12.0488 6.65829 12.0488 6.85355 11.8536C7.04882 11.6583 7.04882 11.3417 6.85355 11.1464L3.20711 7.5L6.85355 3.85355ZM12.8536 3.85355C13.0488 3.65829 13.0488 3.34171 12.8536 3.14645C12.6583 2.95118 12.3417 2.95118 12.1464 3.14645L8.14645 7.14645C7.95118 7.34171 7.95118 7.65829 8.14645 7.85355L12.1464 11.8536C12.3417 12.0488 12.6583 12.0488 12.8536 11.8536C13.0488 11.6583 13.0488 11.3417 12.8536 11.1464L9.20711 7.5L12.8536 3.85355Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path>
</svg>
{prevPost.frontmatter.title}
</a>
)}
{nextPost && (
<a
href={nextPost.url}
style="width: -webkit-fill-available"
class="relative flex min-w-1/2 items-center justify-end gap-2 font-semibold dark:text-white text-black text-right text-pretty text-sm sm:text-base lg:text-lg leading-relaxed hover:text-purple-500 hover:[text-shadow:1px_1px_11px_rgba(168,85,247,0.7)] transition-all duration-300 before:absolute before:-top-6 before:right-0 before:text-xs sm:before:text-sm before:font-light before:content-['Next_Post'] hover:before:text-purple-400"
tabindex="0"
aria-label={`${nextText}: ${nextPost.frontmatter.title}`}
>
{nextPost.frontmatter.title}
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.14645 11.1464C1.95118 11.3417 1.95118 11.6583 2.14645 11.8536C2.34171 12.0488 2.65829 12.0488 2.85355 11.8536L6.85355 7.85355C7.04882 7.65829 7.04882 7.34171 6.85355 7.14645L2.85355 3.14645C2.65829 2.95118 2.34171 2.95118 2.14645 3.14645C1.95118 3.34171 1.95118 3.65829 2.14645 3.85355L5.79289 7.5L2.14645 11.1464ZM8.14645 11.1464C7.95118 11.3417 7.95118 11.6583 8.14645 11.8536C8.34171 12.0488 8.65829 12.0488 8.85355 11.8536L12.8536 7.85355C13.0488 7.65829 13.0488 7.34171 12.8536 7.14645L8.85355 3.14645C8.65829 2.95118 8.34171 2.95118 8.14645 3.14645C7.95118 3.34171 7.95118 3.65829 8.14645 3.85355L11.7929 7.5L8.14645 11.1464Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path>
</svg>
</a>
)}
</nav>
<script>
document.addEventListener('DOMContentLoaded', () => {
const navLinks = document.querySelectorAll('nav[aria-label="Blog post navigation"] a');
navLinks.forEach(link => {
link.addEventListener('keydown', (event) => {
const keyboardEvent = event as KeyboardEvent;
if (keyboardEvent.key === 'Enter' || keyboardEvent.key === ' ') {
event.preventDefault();
const href = (event.target as HTMLAnchorElement).href;
if (href) {
window.location.href = href;
}
}
});
});
});
</script>
<style>
nav a:hover {
transform: translateY(-2px);
}
nav a:focus {
outline: 2px solid rgba(168, 85, 247, 0.5);
outline-offset: 2px;
}
</style>

View File

@@ -0,0 +1,141 @@
---
import type { Lang } from '../../i18n/utils';
interface Props {
lang: Lang;
}
const { lang } = Astro.props;
const title = lang === 'zh' ? '目录' : 'Table of Contents';
---
<div class="max-xl:hidden">
<div id="nav-content" class="sticky w-72 top-14 max-h-[calc(100svh-3.5rem)] overflow-x-hidden">
<div class="flex flex-col gap-3 p-4">
<h3 class="dark:text-zinc-400 text-blacktext/90 font-bold tracking-wide text-sm sm:text-base uppercase flex items-center mb-4">
<svg class="w-4 h-4 sm:w-5 sm:h-5 mr-2 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
</svg>
{title}
</h3>
<div class="text-neutral-500 dark:text-neutral-300">
<ul id="toc-list" class="leading-relaxed text-sm sm:text-base border-l dark:border-neutral-500/20 border-blacktext/20">
</ul>
</div>
</div>
</div>
</div>
<script>
/**
* Initialize table of contents functionality
* Extracts headings from article content and creates navigation links
*/
document.addEventListener("DOMContentLoaded", function () {
const tocList = document.getElementById("toc-list");
const content = document.querySelector("article");
if (!tocList || !content) return;
// Extract h1, h2, h3, h4 headings from article content
const headers = content.querySelectorAll("h1, h2, h3, h4");
let currentUl = tocList;
/**
* Process each heading and create corresponding TOC entry
*/
headers.forEach((header, index) => {
// Generate ID if not present
if (!header.id) {
header.id = header.textContent?.trim().toLowerCase().replace(/\s+/g, "-") + "-" + index;
}
const li = document.createElement("li");
const link = document.createElement("a");
link.href = `#${header.id}`;
link.textContent = header.textContent?.trim() || header.id;
// Apply styling based on heading level
const level = parseInt(header.tagName.charAt(1));
link.classList.add(
"block", "w-full", "text-left", "py-2", "px-3", "rounded-lg", "text-sm",
"transition-all", "duration-200", "border-l", "border-transparent",
"text-muted-foreground", "hover:text-foreground", "hover:bg-muted/50"
);
// Add indentation based on heading level
if (level === 1) {
link.classList.add("font-semibold");
} else if (level === 2) {
link.classList.add("ml-3");
} else if (level === 3) {
link.classList.add("ml-6");
} else if (level === 4) {
link.classList.add("ml-9");
}
li.appendChild(link);
tocList.appendChild(li);
/**
* Handle smooth scroll on link click
*/
link.addEventListener("click", function (e) {
e.preventDefault();
const targetElement = document.getElementById(header.id);
if (targetElement) {
targetElement.scrollIntoView({ behavior: "smooth", block: "start" });
}
});
});
/**
* Set up intersection observer for active section tracking
*/
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const id = entry.target.getAttribute("id");
const link = document.querySelector(`#toc-list a[href="#${id}"]`);
if (entry.isIntersecting) {
// Remove active state from all links
document.querySelectorAll("#toc-list a").forEach((el) => {
el.classList.remove(
"bg-purple-500/10", "text-purple-500", "border-l-2", "border-purple-500"
);
el.classList.add("text-muted-foreground");
});
// Add active state to current link
if (link) {
link.classList.remove("text-muted-foreground");
link.classList.add(
"bg-purple-500/10", "text-purple-500", "border-l-2", "border-purple-500"
);
}
}
});
},
{
rootMargin: "-20% 0% -35% 0%",
threshold: 0
}
);
// Observe all headings for intersection
headers.forEach((header) => observer.observe(header));
});
</script>
<style>
/* Additional styling for smooth transitions */
#toc-list a:hover {
transform: translateX(2px);
}
#toc-list a:focus {
outline: 2px solid rgba(168, 85, 247, 0.5);
outline-offset: 2px;
}
</style>