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:
126
src/components/AuthorCard.tsx
Normal file
126
src/components/AuthorCard.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import type { Lang } from '../i18n/utils';
|
||||
|
||||
interface AuthorCardProps {
|
||||
lang: Lang;
|
||||
author?: {
|
||||
name: string;
|
||||
bio?: string;
|
||||
avatar?: string;
|
||||
website?: string;
|
||||
twitter?: string;
|
||||
github?: string;
|
||||
linkedin?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function AuthorCard({ lang, author }: AuthorCardProps) {
|
||||
// Default author info
|
||||
const defaultAuthor = {
|
||||
name: 'Zhao Guiyang',
|
||||
bio: lang === 'zh'
|
||||
? '全栈开发者,专注于现代Web技术和用户体验设计。热爱分享技术见解和最佳实践。'
|
||||
: 'Full-stack developer passionate about modern web technologies and user experience design. Love sharing technical insights and best practices.',
|
||||
avatar: 'https://avatars.githubusercontent.com/u/24975063?v=4', // You can replace with actual avatar
|
||||
website: 'https://zhaoguiyang.com',
|
||||
github: 'https://github.com/zhaoguiyang',
|
||||
twitter: 'https://twitter.com/zhaoguiyang',
|
||||
linkedin: 'https://linkedin.com/in/zhaoguiyang'
|
||||
};
|
||||
|
||||
const authorInfo = author || defaultAuthor;
|
||||
const aboutText = lang === 'zh' ? '关于作者' : 'About the Author';
|
||||
const websiteText = lang === 'zh' ? '个人网站' : 'Website';
|
||||
|
||||
return (
|
||||
<div className="bg-card/50 backdrop-blur-sm rounded-2xl border border-border p-6 relative">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
{/* Avatar */}
|
||||
<div className="flex-shrink-0 mb-4">
|
||||
<div className="w-20 h-20 bg-gradient-to-br from-purple-500 to-blue-500 rounded-full flex items-center justify-center text-white font-bold text-2xl shadow-lg border-4 border-background">
|
||||
{authorInfo.avatar ? (
|
||||
<img
|
||||
src={authorInfo.avatar}
|
||||
alt={authorInfo.name}
|
||||
className="w-full h-full rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
authorInfo.name.charAt(0).toUpperCase()
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Author Info */}
|
||||
<div className="flex-1">
|
||||
<h4 className="text-lg sm:text-xl font-semibold text-foreground mb-4">
|
||||
{authorInfo.name}
|
||||
</h4>
|
||||
|
||||
{authorInfo.bio && (
|
||||
<p className="text-muted-foreground text-sm sm:text-base mb-6 leading-relaxed max-w-md">
|
||||
{authorInfo.bio}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Social Links */}
|
||||
<div className="flex items-center justify-center space-x-4">
|
||||
{authorInfo.website && (
|
||||
<a
|
||||
href={authorInfo.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-8 h-8 rounded-full bg-muted text-muted-foreground hover:bg-purple-500/10 hover:text-purple-500 transition-all duration-200"
|
||||
title={websiteText}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9m0 9c-5 0-9-4-9-9s4-9 9-9" />
|
||||
</svg>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{authorInfo.github && (
|
||||
<a
|
||||
href={authorInfo.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-8 h-8 rounded-full bg-muted text-muted-foreground hover:bg-gray-500/10 hover:text-gray-700 dark:hover:text-gray-300 transition-all duration-200"
|
||||
title="GitHub"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{authorInfo.twitter && (
|
||||
<a
|
||||
href={authorInfo.twitter}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-8 h-8 rounded-full bg-muted text-muted-foreground hover:bg-blue-500/10 hover:text-blue-500 transition-all duration-200"
|
||||
title="Twitter"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
|
||||
</svg>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{authorInfo.linkedin && (
|
||||
<a
|
||||
href={authorInfo.linkedin}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-8 h-8 rounded-full bg-muted text-muted-foreground hover:bg-blue-600/10 hover:text-blue-600 transition-all duration-200"
|
||||
title="LinkedIn"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||
</svg>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
122
src/components/ShareButtons.tsx
Normal file
122
src/components/ShareButtons.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { useState } from 'react';
|
||||
import type { Lang } from '../i18n/utils';
|
||||
|
||||
interface ShareButtonsProps {
|
||||
lang: Lang;
|
||||
title: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export default function ShareButtons({ lang, title, url }: ShareButtonsProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const shareUrl = url || (typeof window !== 'undefined' ? window.location.href : '');
|
||||
const shareText = lang === 'zh' ? '分享这篇文章' : 'Share this article';
|
||||
const copyText = lang === 'zh' ? '复制链接' : 'Copy link';
|
||||
const copiedText = lang === 'zh' ? '已复制!' : 'Copied!';
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy link:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const shareLinks = [
|
||||
{
|
||||
name: 'Twitter',
|
||||
url: `https://twitter.com/intent/tweet?text=${encodeURIComponent(title)}&url=${encodeURIComponent(shareUrl)}`,
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
|
||||
</svg>
|
||||
),
|
||||
color: 'hover:bg-blue-500/10 hover:text-blue-500'
|
||||
},
|
||||
{
|
||||
name: 'LinkedIn',
|
||||
url: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`,
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||
</svg>
|
||||
),
|
||||
color: 'hover:bg-blue-600/10 hover:text-blue-600'
|
||||
},
|
||||
{
|
||||
name: 'Facebook',
|
||||
url: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`,
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||
</svg>
|
||||
),
|
||||
color: 'hover:bg-blue-700/10 hover:text-blue-700'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-card/50 backdrop-blur-sm rounded-2xl border border-border p-6">
|
||||
<h3 className="text-lg font-semibold text-foreground mb-4 flex items-center">
|
||||
<svg className="w-5 h-5 mr-2 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z" />
|
||||
</svg>
|
||||
{shareText}
|
||||
</h3>
|
||||
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{/* Social Share Buttons */}
|
||||
{shareLinks.map((link) => (
|
||||
<a
|
||||
key={link.name}
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`
|
||||
flex items-center justify-center w-12 h-12 rounded-full
|
||||
bg-muted text-muted-foreground transition-all duration-200
|
||||
${link.color}
|
||||
`}
|
||||
title={`Share on ${link.name}`}
|
||||
>
|
||||
{link.icon}
|
||||
</a>
|
||||
))}
|
||||
|
||||
{/* Copy Link Button */}
|
||||
<button
|
||||
onClick={handleCopyLink}
|
||||
className="
|
||||
flex items-center justify-center w-12 h-12 rounded-full
|
||||
bg-muted text-muted-foreground transition-all duration-200
|
||||
hover:bg-purple-500/10 hover:text-purple-500
|
||||
relative
|
||||
"
|
||||
title={copyText}
|
||||
>
|
||||
{copied ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Copy Success Message */}
|
||||
{copied && (
|
||||
<div className="mt-3 text-sm text-purple-500 flex items-center">
|
||||
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
{copiedText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
142
src/components/blog/PostMeta.astro
Normal file
142
src/components/blog/PostMeta.astro
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
import { type Lang } from '@/i18n/utils';
|
||||
import { useTranslations } from '@/i18n/utils';
|
||||
|
||||
export interface Props {
|
||||
lang: Lang;
|
||||
publishDate?: string;
|
||||
readingTime?: number;
|
||||
tags?: string[];
|
||||
category?: string | string[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
lang,
|
||||
publishDate,
|
||||
readingTime,
|
||||
tags,
|
||||
category,
|
||||
className = ''
|
||||
} = Astro.props;
|
||||
|
||||
const t = useTranslations(lang);
|
||||
|
||||
/**
|
||||
* Format date according to locale
|
||||
*/
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString(lang === 'zh' ? 'zh-CN' : 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get reading time text based on language
|
||||
*/
|
||||
const getReadingTimeText = (minutes: number) => {
|
||||
if (lang === 'zh') {
|
||||
return `${minutes} 分钟阅读`;
|
||||
}
|
||||
return `${minutes} min read`;
|
||||
};
|
||||
---
|
||||
|
||||
<div class={`space-y-6 ${className}`} data-component="PostMeta">
|
||||
|
||||
<!-- Primary Meta Info: Date and Reading Time -->
|
||||
<div class="flex flex-wrap items-center gap-4 text-sm md:text-base text-muted-foreground">
|
||||
<!-- Publish Date -->
|
||||
{publishDate && (
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 md:w-5 md:h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||||
<line x1="8" y1="2" x2="8" y2="6"></line>
|
||||
<line x1="3" y1="10" x2="21" y2="10"></line>
|
||||
</svg>
|
||||
<time datetime={publishDate} class="font-medium">
|
||||
{formatDate(publishDate)}
|
||||
</time>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- Reading Time -->
|
||||
{readingTime && (
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 md:w-5 md:h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12,6 12,12 16,14"></polyline>
|
||||
</svg>
|
||||
<span class="font-medium">{getReadingTimeText(readingTime)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<!-- Secondary Meta Info: Category and Tags -->
|
||||
{(category || (tags && tags.length > 0)) && (
|
||||
<div class="space-y-3">
|
||||
|
||||
<!-- Category Section -->
|
||||
{category && (
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||
分类
|
||||
</div>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
{Array.isArray(category) ? (
|
||||
category.map((cat) => (
|
||||
<span class="inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-gradient-to-r from-purple-500 to-purple-600 text-white shadow-sm hover:shadow-md transition-all duration-200 hover:scale-105">
|
||||
{cat}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span class="inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-gradient-to-r from-purple-500 to-purple-600 text-white shadow-sm hover:shadow-md transition-all duration-200 hover:scale-105">
|
||||
{category}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- Tags Section -->
|
||||
{tags && tags.length > 0 && (
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||
标签
|
||||
</div>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
{tags.map((tag) => (
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 transition-colors border border-gray-200 dark:border-gray-600">
|
||||
# {tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Ensure smooth transitions for hover effects */
|
||||
.transition-colors {
|
||||
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 640px) {
|
||||
.flex-wrap {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
124
src/components/layout/BlogNavigation.astro
Normal file
124
src/components/layout/BlogNavigation.astro
Normal 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>
|
||||
141
src/components/layout/TableOfContents.astro
Normal file
141
src/components/layout/TableOfContents.astro
Normal 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>
|
||||
@@ -31,9 +31,12 @@ const lang = Astro.currentLocale as Lang || defaultLang;
|
||||
/>
|
||||
</head>
|
||||
<body class="min-h-screen bg-background font-sans antialiased selection:bg-purple-500/20 selection:text-purple-500">
|
||||
<div
|
||||
class="fixed inset-0 -z-10 h-full w-full bg-background bg-[radial-gradient(ellipse_80%_80%_at_50%_-20%,rgba(120,119,198,0.3),rgba(255,255,255,0))]"
|
||||
>
|
||||
<!-- Enhanced background with gradient overlay for better visual consistency -->
|
||||
<div class="fixed inset-0 -z-10 h-full w-full bg-background">
|
||||
<!-- Base radial gradient -->
|
||||
<div class="absolute inset-0 bg-[radial-gradient(ellipse_80%_80%_at_50%_-20%,rgba(120,119,198,0.3),rgba(255,255,255,0))] dark:bg-[radial-gradient(ellipse_80%_80%_at_50%_-20%,rgba(120,119,198,0.2),rgba(0,0,0,0))]"></div>
|
||||
<!-- Additional subtle gradient for blog pages -->
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-purple-50/30 via-transparent to-blue-50/20 dark:from-purple-950/20 dark:via-transparent dark:to-blue-950/10"></div>
|
||||
</div>
|
||||
<!-- Glass Header with navigation -->
|
||||
<GlassHeader lang={lang} client:load />
|
||||
|
||||
@@ -1,24 +1,49 @@
|
||||
---
|
||||
import type { MarkdownLayoutProps } from 'astro';
|
||||
import { type Lang } from '@/i18n/utils';
|
||||
import { defaultLang } from '@/i18n/ui';
|
||||
import GlassHeader from '@/components/GlassHeader';
|
||||
import Footer from '@/components/Footer';
|
||||
import AuthorCard from '@/components/AuthorCard';
|
||||
import TableOfContents from '@/components/layout/TableOfContents.astro';
|
||||
import BlogNavigation from '@/components/layout/BlogNavigation.astro';
|
||||
import PostMeta from '@/components/blog/PostMeta.astro';
|
||||
|
||||
import "../styles/global.css";
|
||||
|
||||
export interface Props {
|
||||
// Define the frontmatter structure
|
||||
interface FrontmatterProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
publishDate?: string;
|
||||
date?: string; // Alternative field name for publish date
|
||||
author?: string;
|
||||
tags?: string[];
|
||||
category?: string | string[];
|
||||
readingTime?: number;
|
||||
readTime?: string; // Alternative field name for reading time
|
||||
}
|
||||
|
||||
// Use Astro's MarkdownLayoutProps for proper type safety
|
||||
export type Props = MarkdownLayoutProps<FrontmatterProps>;
|
||||
|
||||
// Access frontmatter data correctly for markdown layouts
|
||||
const { frontmatter } = Astro.props;
|
||||
const {
|
||||
title,
|
||||
description = 'Explore my latest thoughts on coding, tech trends, and developer life.',
|
||||
description,
|
||||
publishDate,
|
||||
author = 'Zhao Guiyang'
|
||||
} = Astro.props;
|
||||
date,
|
||||
tags,
|
||||
category,
|
||||
readTime,
|
||||
} = frontmatter;
|
||||
|
||||
const lang = Astro.currentLocale as Lang || defaultLang;
|
||||
|
||||
// Handle different field names for backward compatibility
|
||||
const finalPublishDate = publishDate || date;
|
||||
const finalReadingTime = readTime ? parseInt(readTime.replace(/\D/g, '')) : undefined;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
@@ -45,33 +70,63 @@ const lang = Astro.currentLocale as Lang || defaultLang;
|
||||
<!-- Glass Header with navigation -->
|
||||
<GlassHeader lang={lang} client:load />
|
||||
|
||||
|
||||
|
||||
<!-- Main content with proper spacing for fixed header -->
|
||||
<div class="pt-16">
|
||||
<main class="container mx-auto px-4 py-8 max-w-4xl">
|
||||
<!-- Blog post header -->
|
||||
<header class="mb-8 text-center">
|
||||
<h1 class="text-4xl font-bold text-foreground mb-4">{title}</h1>
|
||||
{publishDate && (
|
||||
<div class="text-muted-foreground mb-2">
|
||||
<time datetime={publishDate}>{new Date(publishDate).toLocaleDateString(lang === 'zh' ? 'zh-CN' : 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}</time>
|
||||
</div>
|
||||
)}
|
||||
{author && (
|
||||
<div class="text-muted-foreground">
|
||||
By {author}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<!-- Blog post content with typography styles -->
|
||||
<article class="prose prose-lg dark:prose-invert max-w-none prose-headings:text-foreground prose-p:text-muted-foreground prose-strong:text-foreground prose-code:text-foreground prose-pre:bg-muted prose-blockquote:border-l-primary prose-blockquote:text-muted-foreground prose-a:text-primary hover:prose-a:text-primary/80 prose-li:text-muted-foreground prose-img:rounded-lg prose-img:shadow-lg">
|
||||
<slot />
|
||||
</article>
|
||||
</main>
|
||||
<div class="pt-16 sm:pt-20">
|
||||
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="grid grid-cols-1 xl:grid-cols-4 gap-6 lg:gap-8">
|
||||
<!-- Main Content -->
|
||||
<main class="xl:col-span-3 order-2 xl:order-1">
|
||||
<!-- Blog post header -->
|
||||
<header class="mb-10">
|
||||
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-foreground mb-6 leading-tight tracking-tight">
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
{description && (
|
||||
<p class="text-lg sm:text-xl text-muted-foreground mb-8 leading-relaxed max-w-4xl">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<PostMeta
|
||||
lang={lang}
|
||||
publishDate={finalPublishDate}
|
||||
readingTime={finalReadingTime}
|
||||
tags={tags}
|
||||
category={category}
|
||||
className="justify-start"
|
||||
/>
|
||||
</header>
|
||||
|
||||
<!-- Blog post content with typography styles -->
|
||||
<article class="prose prose-lg dark:prose-invert max-w-none prose-headings:scroll-mt-24 prose-headings:font-bold prose-headings:tracking-tight prose-h1:text-2xl sm:prose-h1:text-3xl prose-h2:text-xl sm:prose-h2:text-2xl prose-h3:text-lg sm:prose-h3:text-xl prose-h4:text-base sm:prose-h4:text-lg prose-p:text-base sm:prose-p:text-lg prose-p:leading-relaxed prose-p:mb-6 prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-strong:text-foreground prose-code:text-sm prose-code:px-2 prose-code:py-1 prose-code:rounded prose-pre:bg-muted prose-pre:border prose-pre:text-sm prose-blockquote:border-l-primary prose-blockquote:bg-muted/30 prose-blockquote:text-base prose-li:text-base sm:prose-li:text-lg prose-li:leading-relaxed">
|
||||
<slot />
|
||||
</article>
|
||||
|
||||
<!-- Blog Navigation -->
|
||||
<div class="mt-8 sm:mt-12">
|
||||
<BlogNavigation />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside class="xl:col-span-1 order-1 xl:order-2">
|
||||
<div class="xl:sticky xl:top-24 space-y-6 sm:space-y-8 xl:space-y-12 xl:max-h-[calc(100vh-6rem)] xl:overflow-y-auto">
|
||||
<!-- Table of Contents -->
|
||||
<div class="bg-card/50 backdrop-blur-sm rounded-2xl border border-border h-auto xl:h-[400px] lg:h-[500px] overflow-y-auto">
|
||||
<TableOfContents lang={lang} />
|
||||
</div>
|
||||
|
||||
<!-- Author Card -->
|
||||
<AuthorCard lang={lang} />
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
@@ -122,4 +177,147 @@ const lang = Astro.currentLocale as Lang || defaultLang;
|
||||
background-color var(--transition-standard),
|
||||
color var(--transition-standard);
|
||||
}
|
||||
|
||||
/* Enhanced prose styles for better readability */
|
||||
.prose {
|
||||
line-height: 1.75;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.prose {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.prose h1,
|
||||
.prose h2,
|
||||
.prose h3,
|
||||
.prose h4 {
|
||||
scroll-margin-top: 6rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.025em;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Hide the first H1 in markdown content to avoid duplicate titles */
|
||||
.prose h1:first-of-type {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.prose h1 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.prose h1 {
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.prose h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.prose h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.prose h4 {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.prose h4 {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
.prose p {
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.prose li {
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.prose img {
|
||||
margin: 2rem auto;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.prose pre {
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid hsl(var(--border));
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.prose code {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.125rem 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.prose blockquote {
|
||||
border-left: 4px solid hsl(var(--primary));
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
border-radius: 0 0.5rem 0.5rem 0;
|
||||
padding: 1rem 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.prose blockquote {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive sidebar */
|
||||
@media (max-width: 1023px) {
|
||||
aside {
|
||||
order: -1;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.sticky {
|
||||
position: static !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Line clamp utilities */
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
export const personalInfo = {
|
||||
name: "Joy Zhao",
|
||||
location: "Shanghai, China",
|
||||
avatar:"https://avatars.githubusercontent.com/u/24975063?v=4",
|
||||
email: "zhaoguiyang18@gmail.com",
|
||||
github: "https://github.com/zguiyang",
|
||||
linkedin: "https://linkedin.com/in/zhaoguiyang"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
|
||||
@@ -5,12 +5,11 @@ image: "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=400&h=250
|
||||
date: "May 10, 2025"
|
||||
readTime: "5 min read"
|
||||
tags: ["React", "JavaScript", "Frontend"]
|
||||
category: ["React", "Frontend"]
|
||||
slug: "mastering-react-hooks"
|
||||
layout: "../../../layouts/BlogPostLayout.astro"
|
||||
---
|
||||
|
||||
# Mastering React Hooks: A Deep Dive
|
||||
|
||||

|
||||
|
||||
React Hooks have revolutionized the way we write React components, allowing us to use state and other React features in functional components. In this comprehensive guide, we'll explore the most important hooks and learn how to use them effectively.
|
||||
|
||||
@@ -9,8 +9,6 @@ slug: "modern-ui-tailwind"
|
||||
layout: "../../../layouts/BlogPostLayout.astro"
|
||||
---
|
||||
|
||||
# Building Modern UIs with Tailwind CSS
|
||||
|
||||

|
||||
|
||||
Tailwind CSS has revolutionized the way developers approach styling web applications. By providing a comprehensive set of utility classes, it enables rapid development of beautiful, responsive user interfaces without writing custom CSS. Let's explore how to leverage Tailwind CSS to build modern UIs.
|
||||
|
||||
@@ -9,8 +9,6 @@ slug: "scaling-nodejs-docker"
|
||||
layout: "../../../layouts/BlogPostLayout.astro"
|
||||
---
|
||||
|
||||
# Scaling Node.js Apps with Docker
|
||||
|
||||

|
||||
|
||||
Docker has revolutionized how we deploy and scale applications. When combined with Node.js, it provides a powerful platform for building scalable, maintainable applications. In this guide, we'll explore how to containerize Node.js applications and scale them effectively.
|
||||
|
||||
@@ -9,8 +9,6 @@ slug: "typescript-best-practices"
|
||||
layout: "../../../layouts/BlogPostLayout.astro"
|
||||
---
|
||||
|
||||
# TypeScript Best Practices for Large Projects
|
||||
|
||||

|
||||
|
||||
TypeScript has become the de facto standard for building large-scale JavaScript applications. Its static type system helps catch errors early, improves code maintainability, and enhances developer productivity. In this comprehensive guide, we'll explore essential TypeScript best practices for enterprise-level projects.
|
||||
@@ -88,7 +86,7 @@ Start with a robust `tsconfig.json`:
|
||||
|
||||
Organize your project for scalability:
|
||||
|
||||
```
|
||||
```markdown
|
||||
src/
|
||||
├── components/ # Reusable UI components
|
||||
│ ├── common/ # Shared components
|
||||
|
||||
@@ -9,8 +9,6 @@ slug: "mastering-react-hooks"
|
||||
layout: "../../../../layouts/BlogPostLayout.astro"
|
||||
---
|
||||
|
||||
# 精通 React Hooks:深入探索
|
||||
|
||||

|
||||
|
||||
React Hooks 彻底改变了我们编写 React 组件的方式,让我们能够在函数组件中使用状态和其他 React 特性。在这个全面的指南中,我们将探索最重要的 hooks 并学习如何有效地使用它们。
|
||||
|
||||
@@ -9,8 +9,6 @@ slug: "modern-ui-tailwind"
|
||||
layout: "../../../../layouts/BlogPostLayout.astro"
|
||||
---
|
||||
|
||||
# 使用 Tailwind CSS 构建现代 UI
|
||||
|
||||

|
||||
|
||||
Tailwind CSS 已经彻底改变了我们构建用户界面的方式。作为一个实用优先的 CSS 框架,它提供了低级实用类,让你可以直接在标记中构建完全自定义的设计。在本指南中,我们将探索如何使用 Tailwind CSS 创建现代、响应式的 UI 组件。
|
||||
|
||||
@@ -9,8 +9,6 @@ slug: "scaling-nodejs-docker"
|
||||
layout: "../../../../layouts/BlogPostLayout.astro"
|
||||
---
|
||||
|
||||
# 使用 Docker 扩展 Node.js 应用
|
||||
|
||||

|
||||
|
||||
Docker 彻底改变了我们部署和扩展应用程序的方式。当与 Node.js 结合使用时,它提供了一个强大的平台来构建可扩展、可维护的应用程序。在本指南中,我们将探索如何容器化 Node.js 应用程序并有效地扩展它们。
|
||||
|
||||
@@ -9,8 +9,6 @@ slug: "typescript-best-practices"
|
||||
layout: "../../../../layouts/BlogPostLayout.astro"
|
||||
---
|
||||
|
||||
# TypeScript 在大型项目中的最佳实践
|
||||
|
||||

|
||||
|
||||
TypeScript 已经成为现代 JavaScript 开发的标准选择,特别是在大型项目中。它提供了静态类型检查、更好的 IDE 支持和增强的代码可维护性。在本指南中,我们将探索在企业级应用中使用 TypeScript 的最佳实践。
|
||||
@@ -29,7 +27,7 @@ TypeScript 为大型项目带来了显著优势:
|
||||
|
||||
### 1. 推荐的项目结构
|
||||
|
||||
```
|
||||
```markdown
|
||||
src/
|
||||
├── components/ # 可重用组件
|
||||
│ ├── ui/ # 基础 UI 组件
|
||||
|
||||
Reference in New Issue
Block a user