Compare commits

...

4 Commits

Author SHA1 Message Date
zguiyang
d255d0634b fix(blog): reduce list-to-post navigation lag 2026-03-18 08:41:22 +08:00
zguiyang
d9a5945b66 fix(comments): align Waline theme colors with site blue/orange theme
- Replace purple theme (#8B5CF6, #EC4899) with blue (#2563EB) and orange (#F97316)
- Use solid color for submit button instead of gradient
2026-03-18 08:24:56 +08:00
zguiyang
340c3db383 feat: add SEO infrastructure, 404 page, accessibility and performance optimizations
- Add robots.txt for search engine crawling
- Enhance Layout.astro with complete SEO meta tags (OG, Twitter Card, canonical)
- Create custom 404 page with bilingual support
- Add skip link for accessibility
- Add main-content id to all major pages for keyboard navigation
- Add lazy loading to blog list and author card images
2026-03-18 08:20:13 +08:00
zguiyang
eb6bef3726 refactor: remove "Services" pages and migrate content to focused alternatives
- Removed `/services` and `/zh/services` pages. Migrated content to updated pages: `/uses`, `/about#contact-card`, and `/hire`.
- Removed Framer Motion for better performance and simpler animations in `LanguageSwitcher`, `GlassHeader`, and other components.
- Updated font sources to WOFF2 for better compression and added preload links for critical fonts.
- Optimized Vite configuration with manual chunking for React libraries.
- Replaced `client:load` with `client:idle` for non-critical client-side components like `GlassHeader`, `Footer`, and `BackToTop`.
2026-03-17 16:42:00 +08:00
35 changed files with 253 additions and 192 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

View File

@@ -31,6 +31,16 @@ export default defineConfig({
], ],
vite: { vite: {
plugins: [tailwindcss()], plugins: [tailwindcss()],
build: {
cssCodeSplit: true,
rollupOptions: {
output: {
manualChunks: {
'vendor-react': ['react', 'react-dom'],
}
}
}
}
}, },
i18n: { i18n: {
// The default locale to fall back to if a page isn't available in the active locale // The default locale to fall back to if a page isn't available in the active locale

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

13
public/robots.txt Normal file
View File

@@ -0,0 +1,13 @@
# robots.txt for zhaoguiyang.com
# https://zhaoguiyang.com
User-agent: *
Allow: /
# Sitemap
Sitemap: https://zhaoguiyang.com/sitemap-index.xml
# Disallow admin/private areas (if any)
Disallow: /api/
Disallow: /_astro/
Disallow: /assets/

View File

@@ -29,6 +29,8 @@ export default function AuthorCard({ lang, author }: AuthorCardProps) {
<img <img
src={authorInfo.avatar} src={authorInfo.avatar}
alt={authorInfo.name} alt={authorInfo.name}
loading="lazy"
decoding="async"
className="w-full h-full rounded-full object-cover" className="w-full h-full rounded-full object-cover"
/> />
) : ( ) : (

View File

@@ -1,7 +1,6 @@
import { useTranslations } from "@/i18n/utils"; import { useTranslations } from "@/i18n/utils";
import type { Lang } from "@/types/i18n"; import type { Lang } from "@/types/i18n";
import { personalInfo } from "@/lib/data/index"; import { personalInfo } from "@/lib/data/index";
import { motion } from "framer-motion";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { defaultLang } from "@/i18n/ui"; import { defaultLang } from "@/i18n/ui";
import Container from "./ui/Container.tsx"; import Container from "./ui/Container.tsx";
@@ -21,60 +20,38 @@ export default function Footer({ lang: propLang }: FooterProps) {
return ( return (
<footer className="border-t border-primary/10 py-6 bg-gradient-to-b from-background to-muted/20 backdrop-blur-sm"> <footer className="border-t border-primary/10 py-6 bg-gradient-to-b from-background to-muted/20 backdrop-blur-sm">
<Container> <Container>
<motion.div <div
className="flex flex-col md:flex-row justify-between items-center" className="flex flex-col md:flex-row justify-between items-center"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
> >
<motion.div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<motion.p <p
className="text-sm text-muted-foreground text-center md:text-left" className="text-sm text-muted-foreground text-center md:text-left hover:scale-[1.01] transition-transform"
whileHover={{ scale: 1.01 }}
> >
&copy; {new Date().getFullYear()} { personalInfo.name }. {t('footer.rights')} &copy; {new Date().getFullYear()} { personalInfo.name }. {t('footer.rights')}
</motion.p> </p>
<motion.p <p
className="text-sm text-primary font-medium text-center md:text-left" className="text-sm text-primary font-medium text-center md:text-left hover:scale-[1.01] transition-transform"
whileHover={{ scale: 1.01 }}
> >
{lang === 'zh' ? '正在招聘远程工程师或寻求项目合作?欢迎联系我。' : 'Hiring for a remote engineering role or looking for project collaboration? Lets connect.'} {lang === 'zh' ? '正在招聘远程工程师或寻求项目合作?欢迎联系我。' : 'Hiring for a remote engineering role or looking for project collaboration? Lets connect.'}
</motion.p> </p>
</motion.div> </div>
<motion.p <p
className="text-sm text-muted-foreground mt-2 md:mt-0 text-center md:text-left" className="text-sm text-muted-foreground mt-2 md:mt-0 text-center md:text-left hover:scale-[1.01] transition-transform"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ delay: 0.2, duration: 0.5 }}
viewport={{ once: true }}
whileHover={{ scale: 1.01 }}
> >
Built with{" "} Built with{" "}
<motion.span <span
className="inline-block" className="inline-block hover:rotate-360 transition-transform duration-500"
initial={{ rotate: 0 }}
whileHover={{ rotate: 360 }}
transition={{ duration: 0.5 }}
> >
💻 💻
</motion.span>{" "} </span>{" "}
and{" "} and{" "}
<motion.span <span
className="inline-block" className="inline-block animate-pulse"
animate={{
scale: [1, 1.2, 1],
}}
transition={{
repeat: Infinity,
repeatType: "reverse",
duration: 1.5,
}}
> >
</motion.span> </span>
</motion.p> </p>
</motion.div> </div>
</Container> </Container>
</footer> </footer>
); );

View File

@@ -8,7 +8,6 @@ import { useState, useEffect } from "react";
import { Menu, X, Home, Rocket, PenTool, User, Briefcase, Clock3 } from "lucide-react"; import { Menu, X, Home, Rocket, PenTool, User, Briefcase, Clock3 } from "lucide-react";
import { defaultLang } from "@/i18n/ui"; import { defaultLang } from "@/i18n/ui";
import { type GlassHeaderProps } from "@/types"; import { type GlassHeaderProps } from "@/types";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export default function GlassHeader({ lang: propLang }: GlassHeaderProps) { export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
@@ -98,7 +97,7 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
}; };
return ( return (
<motion.header <header
className={`fixed top-0 z-50 w-full transition-all duration-300 ${ className={`fixed top-0 z-50 w-full transition-all duration-300 ${
isScrolled isScrolled
? 'backdrop-blur-xl backdrop-saturate-150 bg-white/70 dark:bg-black/70 border-b border-border/50 shadow-lg shadow-black/5 dark:shadow-primary/5' ? 'backdrop-blur-xl backdrop-saturate-150 bg-white/70 dark:bg-black/70 border-b border-border/50 shadow-lg shadow-black/5 dark:shadow-primary/5'
@@ -106,10 +105,8 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
}`} }`}
> >
<Container className="p-4 flex justify-between items-center"> <Container className="p-4 flex justify-between items-center">
<motion.a <a
whileHover={{ scale: 1.05 }} className="flex items-center text-lg font-medium transition-colors duration-150 hover:text-foreground/80 hover:scale-105 active:scale-95"
whileTap={{ scale: 0.95 }}
className="flex items-center text-lg font-medium transition-colors duration-150 hover:text-foreground/80"
href={getLocalizedPath('/', lang)} href={getLocalizedPath('/', lang)}
> >
<div className="w-6 h-6 mr-2 flex items-center justify-center"> <div className="w-6 h-6 mr-2 flex items-center justify-center">
@@ -119,14 +116,14 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
</svg> </svg>
</div> </div>
<span className="gradient-text font-bold">{personalInfo.name}</span> <span className="gradient-text font-bold">{personalInfo.name}</span>
</motion.a> </a>
{/* Desktop Navigation */} {/* Desktop Navigation */}
<nav className="hidden md:flex items-center space-x-6 text-sm font-medium"> <nav className="hidden md:flex items-center space-x-6 text-sm font-medium">
{navItems.map((item) => { {navItems.map((item) => {
const active = isActive(item.href); const active = isActive(item.href);
return ( return (
<motion.a <a
key={item.key} key={item.key}
href={item.href} href={item.href}
className={cn( className={cn(
@@ -135,34 +132,32 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
? "text-primary bg-primary/5 font-semibold" ? "text-primary bg-primary/5 font-semibold"
: "text-foreground/70 hover:text-primary" : "text-foreground/70 hover:text-primary"
)} )}
whileHover={{ y: -2 }}
> >
<item.icon size={16} /> <item.icon size={16} />
{t(item.key as any)} {t(item.key as any)}
</motion.a> </a>
); );
})} })}
</nav> </nav>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
{/* Language Switcher added here */} {/* Language Switcher added here */}
<motion.div> <div>
<LanguageSwitcher lang={lang} /> <LanguageSwitcher lang={lang} />
</motion.div> </div>
<motion.div> <div>
<ThemeToggle /> <ThemeToggle />
</motion.div> </div>
{/* Mobile Menu Button */} {/* Mobile Menu Button */}
<motion.button <button
className="md:hidden p-2 text-foreground transition-all duration-150 hover:bg-foreground/10 active:bg-foreground/20 rounded-md" className="md:hidden p-2 text-foreground transition-all duration-150 hover:bg-foreground/10 active:bg-foreground/20 rounded-md active:scale-90"
onClick={toggleMenu} onClick={toggleMenu}
aria-label="Toggle menu" aria-label="Toggle menu"
whileTap={{ scale: 0.9 }}
> >
{isMenuOpen ? <X size={24} /> : <Menu size={24} />} {isMenuOpen ? <X size={24} /> : <Menu size={24} />}
</motion.button> </button>
</div> </div>
</Container> </Container>
@@ -179,7 +174,7 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
{navItems.map((item) => { {navItems.map((item) => {
const active = isActive(item.href); const active = isActive(item.href);
return ( return (
<motion.a <a
key={item.key} key={item.key}
href={item.href} href={item.href}
className={cn( className={cn(
@@ -192,12 +187,12 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
> >
<item.icon size={18} /> <item.icon size={18} />
{t(item.key as any)} {t(item.key as any)}
</motion.a> </a>
); );
})} })}
</nav> </nav>
</div> </div>
</div> </div>
</motion.header> </header>
); );
} }

View File

@@ -3,7 +3,6 @@ import { getLocalizedPath } from "@/i18n/utils";
import type { Lang } from "@/types/i18n"; import type { Lang } from "@/types/i18n";
import { languages as i18nLanguages, defaultLang } from "@/i18n/ui"; import { languages as i18nLanguages, defaultLang } from "@/i18n/ui";
import { Check, ChevronDown } from "lucide-react"; import { Check, ChevronDown } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { type LanguageSwitcherProps } from "@/types"; import { type LanguageSwitcherProps } from "@/types";
const availableLanguages = Object.entries(i18nLanguages).map(([code, name]) => ({ const availableLanguages = Object.entries(i18nLanguages).map(([code, name]) => ({
@@ -65,25 +64,19 @@ export default function LanguageSwitcher({ lang: propLang }: LanguageSwitcherPro
return ( return (
<div className="relative"> <div className="relative">
<motion.button <button
onClick={toggleOpen} onClick={toggleOpen}
className="flex items-center p-2 rounded-md hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-sm" className="flex items-center p-2 rounded-md hover:bg-accent hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-sm active:scale-95"
aria-label={`Change language, current language is ${selectedLanguage.name}`} aria-label={`Change language, current language is ${selectedLanguage.name}`}
whileTap={{ scale: 0.95 }}
> >
<span className="mr-1.5">{selectedLanguage.icon}</span> <span className="mr-1.5">{selectedLanguage.icon}</span>
<span className="hidden sm:inline">{selectedLanguage.name}</span> <span className="hidden sm:inline">{selectedLanguage.name}</span>
<span className="sm:hidden">{selectedLanguage.code.toUpperCase()}</span> <span className="sm:hidden">{selectedLanguage.code.toUpperCase()}</span>
<ChevronDown size={16} className={`ml-1.5 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} /> <ChevronDown size={16} className={`ml-1.5 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} />
</motion.button> </button>
<AnimatePresence>
{isOpen && ( {isOpen && (
<motion.div <div
initial={{ opacity: 0, y: -10 }} className="absolute right-0 mt-2 w-40 origin-top-right rounded-md bg-popover text-popover-foreground shadow-md focus:outline-none z-50 border border-border/20 animate-in fade-in slide-in-from-top-2"
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
className="absolute right-0 mt-2 w-40 origin-top-right rounded-md bg-popover text-popover-foreground shadow-md focus:outline-none z-50 border border-border/20"
> >
<div className="p-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu"> <div className="p-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
{availableLanguages.map((lang) => ( {availableLanguages.map((lang) => (
@@ -101,9 +94,8 @@ export default function LanguageSwitcher({ lang: propLang }: LanguageSwitcherPro
</button> </button>
))} ))}
</div> </div>
</motion.div> </div>
)} )}
</AnimatePresence>
</div> </div>
); );
} }

View File

@@ -83,6 +83,8 @@ const readMoreText = lang === 'zh' ? '阅读更多' : 'Read More';
<img <img
src={post.image} src={post.image}
alt={post.title} alt={post.title}
loading="lazy"
decoding="async"
class="w-full h-48 md:h-full object-cover group-hover:scale-110 transition-transform duration-300" class="w-full h-48 md:h-full object-cover group-hover:scale-110 transition-transform duration-300"
/> />
<div class="absolute inset-0 bg-gradient-to-t md:bg-gradient-to-r from-background/50 to-transparent"></div> <div class="absolute inset-0 bg-gradient-to-t md:bg-gradient-to-r from-background/50 to-transparent"></div>
@@ -92,7 +94,7 @@ const readMoreText = lang === 'zh' ? '阅读更多' : 'Read More';
<div class="p-6 flex-1 flex flex-col justify-between"> <div class="p-6 flex-1 flex flex-col justify-between">
<div> <div>
<h2 class="text-xl font-bold text-card-foreground mb-3 group-hover:text-primary transition-colors duration-200"> <h2 class="text-xl font-bold text-card-foreground mb-3 group-hover:text-primary transition-colors duration-200">
<a href={`${postBaseUrl}${post.slug}`}> <a href={`${postBaseUrl}${post.slug}`} data-astro-reload>
{post.title} {post.title}
</a> </a>
</h2> </h2>
@@ -135,6 +137,7 @@ const readMoreText = lang === 'zh' ? '阅读更多' : 'Read More';
<a <a
href={`${postBaseUrl}${post.slug}`} href={`${postBaseUrl}${post.slug}`}
data-astro-reload
class="text-primary hover:text-primary font-medium flex items-center group" class="text-primary hover:text-primary font-medium flex items-center group"
> >
{readMoreText} {readMoreText}

View File

@@ -13,12 +13,11 @@ export default function Comments({ lang = 'en', ...props }: CommentsProps) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const walineLang = lang === 'zh' ? 'zh-CN' : 'en'; const walineLang = lang === 'zh' ? 'zh-CN' : 'en';
const getTheme = () => (document.documentElement.classList.contains('dark') ? 'html.dark' : false);
useEffect(() => { useEffect(() => {
if (!containerRef.current) return; if (!containerRef.current) return;
const getTheme = () => document.documentElement.classList.contains('dark') ? 'html.dark' : false;
const pathname = window.location.pathname; const pathname = window.location.pathname;
const postsMatch = pathname.match(/\/posts\/([^/]+)/); const postsMatch = pathname.match(/\/posts\/([^/]+)/);
const path = postsMatch ? postsMatch[1] : pathname; const path = postsMatch ? postsMatch[1] : pathname;
@@ -34,18 +33,6 @@ export default function Comments({ lang = 'en', ...props }: CommentsProps) {
path, path,
}); });
return () => walineInstanceRef.current?.destroy();
}, []);
useEffect(() => {
const getTheme = () => document.documentElement.classList.contains('dark') ? 'html.dark' : false;
walineInstanceRef.current?.update({
...props,
lang: walineLang,
dark: getTheme(),
});
const handleThemeChange = () => { const handleThemeChange = () => {
walineInstanceRef.current?.update({ walineInstanceRef.current?.update({
...props, ...props,
@@ -57,8 +44,22 @@ export default function Comments({ lang = 'en', ...props }: CommentsProps) {
const observer = new MutationObserver(handleThemeChange); const observer = new MutationObserver(handleThemeChange);
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
return () => observer.disconnect(); return () => {
}, [lang, props, walineLang]); observer.disconnect();
const instance = walineInstanceRef.current;
walineInstanceRef.current = null;
if (instance) {
try {
instance.destroy();
} catch {
if (containerRef.current) {
containerRef.current.innerHTML = '';
}
}
}
};
}, [props, walineLang]);
return <div ref={containerRef} className="waline-container" />; return <div ref={containerRef} className="waline-container" />;
} }

View File

@@ -19,7 +19,7 @@ const title = lang === 'zh' ? '目录' : 'Table of Contents';
</svg> </svg>
{title} {title}
</h3> </h3>
<ScrollArea className="w-full" client:load> <ScrollArea className="w-full" client:visible>
<div class="text-neutral-500 dark:text-neutral-300 pr-4"> <div class="text-neutral-500 dark:text-neutral-300 pr-4">
<ul id="toc-list" class="leading-relaxed text-sm sm:text-base border-l dark:border-neutral-500/20 border-blacktext/20 mt-4"> <ul id="toc-list" class="leading-relaxed text-sm sm:text-base border-l dark:border-neutral-500/20 border-blacktext/20 mt-4">
</ul> </ul>

View File

@@ -107,7 +107,7 @@ const finalReadingTime = readTime ? parseInt(readTime.replace(/\D/g, '')) : unde
<!-- Comments Section --> <!-- Comments Section -->
<div class="mt-10 sm:mt-16 border-t border-border pt-10"> <div class="mt-10 sm:mt-16 border-t border-border pt-10">
<Comments client:only="react" lang={lang} /> <Comments client:visible lang={lang} />
</div> </div>
<!-- Author Card moved to bottom with enhanced styling --> <!-- Author Card moved to bottom with enhanced styling -->

View File

@@ -4,17 +4,27 @@ import BackToTop from "@/components/ui/back-to-top";
import { useTranslations } from "@/i18n/utils"; import { useTranslations } from "@/i18n/utils";
import type { Lang } from "@/types/i18n"; import type { Lang } from "@/types/i18n";
import { defaultLang } from "@/i18n/ui"; import { defaultLang } from "@/i18n/ui";
import { personalInfo } from "@/lib/data";
import "../styles/global.css"; import "../styles/global.css";
interface Props { interface Props {
title?: string; title?: string;
description?: string; description?: string;
image?: string;
articleDate?: string;
} }
const lang = Astro.currentLocale as Lang || defaultLang; const lang = Astro.currentLocale as Lang || defaultLang;
const { title = "Joey Z. - Portfolio", description = "Engineering-focused personal website" } = const isZh = lang === 'zh';
Astro.props;
const t = useTranslations(lang); const t = useTranslations(lang);
const { title, description, image, articleDate } = Astro.props;
const siteTitle = t("site.title");
const fullTitle = title ? `${title} | ${siteTitle}` : siteTitle;
const siteUrl = "https://zhaoguiyang.com";
const defaultImage = "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=1200&h=630&fit=crop";
const currentUrl = Astro.url.href;
const ogImage = image || defaultImage;
--- ---
<!doctype html> <!doctype html>
@@ -23,9 +33,35 @@ const t = useTranslations(lang);
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<!-- Preload critical fonts - using WOFF2 for better compression -->
<link rel="preload" href="/fonts/archivo-700.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/fonts/space-grotesk-400.woff2" as="font" type="font/woff2" crossorigin />
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<meta name="description" content={description} />
<title>{title} | {t("site.title")}</title> <!-- Basic SEO -->
<meta name="description" content={description || "AI Full-stack Engineer - 8 years building enterprise systems, financial platforms, and blockchain infrastructure."} />
<link rel="canonical" href={currentUrl} />
<!-- Open Graph / Facebook -->
<meta property="og:type" content={articleDate ? "article" : "website"} />
<meta property="og:url" content={currentUrl} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description || "AI Full-stack Engineer - 8 years building enterprise systems, financial platforms, and blockchain infrastructure."} />
<meta property="og:image" content={ogImage} />
<meta property="og:site_name" content={siteTitle} />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content={currentUrl} />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={description || "AI Full-stack Engineer - 8 years building enterprise systems, financial platforms, and blockchain infrastructure."} />
<meta name="twitter:image" content={ogImage} />
<!-- Article specific -->
{articleDate && <meta property="article:published_time" content={articleDate} />}
<title>{fullTitle}</title>
<!-- View Transitions for smooth page transitions --> <!-- View Transitions for smooth page transitions -->
<ClientRouter /> <ClientRouter />
<meta name="view-transition" content="same-origin" /> <meta name="view-transition" content="same-origin" />
@@ -38,6 +74,14 @@ const t = useTranslations(lang);
<body <body
class="min-h-screen bg-background font-sans antialiased selection:bg-primary/20 selection:text-primary" class="min-h-screen bg-background font-sans antialiased selection:bg-primary/20 selection:text-primary"
> >
<!-- Skip link for accessibility -->
<a
href="#main-content"
class="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-lg focus:font-semibold focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
>
{isZh ? '跳到主要内容' : 'Skip to main content'}
</a>
<div <div
class="fixed inset-0 -z-10 h-full w-full bg-background" class="fixed inset-0 -z-10 h-full w-full bg-background"
> >
@@ -46,7 +90,7 @@ const t = useTranslations(lang);
<div class="absolute inset-0 bg-[radial-gradient(circle_at_20%_80%,rgba(249,115,22,0.05),transparent_40%)]"></div> <div class="absolute inset-0 bg-[radial-gradient(circle_at_20%_80%,rgba(249,115,22,0.05),transparent_40%)]"></div>
</div> </div>
<slot /> <slot />
<BackToTop client:load /> <BackToTop client:idle />
</body> </body>
</html> </html>

52
src/pages/404.astro Normal file
View File

@@ -0,0 +1,52 @@
---
import Layout from '@/layouts/Layout.astro';
import GlassHeader from '@/components/GlassHeader';
import Footer from '@/components/Footer';
import Container from '@/components/ui/Container.astro';
import { getLocalizedPath } from '@/i18n/utils';
import type { Lang } from '@/types/i18n';
import { defaultLang } from '@/i18n/ui';
const lang = (Astro.currentLocale as Lang) || defaultLang;
const isZh = lang === 'zh';
const homePath = getLocalizedPath('/', lang);
---
<Layout
title={isZh ? '页面未找到' : 'Page Not Found'}
description={isZh ? '抱歉,您访问的页面不存在。' : 'Sorry, the page you are looking for does not exist.'}
>
<GlassHeader lang={lang} client:load transition:persist="header" />
<main id="main-content" class="min-h-screen pt-24 pb-20">
<Container>
<div class="flex flex-col items-center justify-center min-h-[60vh] text-center">
<h1 class="text-9xl font-bold text-primary">404</h1>
<h2 class="text-2xl font-semibold mt-4">
{isZh ? '页面未找到' : 'Page Not Found'}
</h2>
<p class="text-muted-foreground mt-4 max-w-md">
{isZh
? '抱歉,您访问的页面不存在或已被移除。'
: 'Sorry, the page you are looking for does not exist or has been moved.'}
</p>
<div class="mt-8 flex flex-col sm:flex-row gap-4">
<a
href={homePath}
class="inline-flex items-center justify-center px-6 py-3 text-sm font-semibold rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
>
{isZh ? '返回首页' : 'Go Home'}
</a>
<a
href={isZh ? '/zh/blog' : '/blog'}
class="inline-flex items-center justify-center px-6 py-3 text-sm font-semibold rounded-lg border border-border hover:bg-muted transition-colors"
>
{isZh ? '浏览博客' : 'Browse Blog'}
</a>
</div>
</div>
</Container>
</main>
<Footer lang={lang} client:load />
</Layout>

View File

@@ -23,7 +23,7 @@ const prefix = isZh ? '/zh' : '';
<Layout title={isZh ? '关于' : 'About'}> <Layout title={isZh ? '关于' : 'About'}>
<GlassHeader lang={lang} client:load transition:persist="header" /> <GlassHeader lang={lang} client:load transition:persist="header" />
<main class="min-h-screen pt-24 pb-20"> <main id="main-content" class="min-h-screen pt-24 pb-20">
<Container> <Container>
<section class="page-content-main" id="overview"> <section class="page-content-main" id="overview">
<div class="mb-6 flex flex-wrap gap-2 text-sm"> <div class="mb-6 flex flex-wrap gap-2 text-sm">

View File

@@ -20,7 +20,7 @@ const isZh = lang === 'zh';
<Layout title={isZh ? '合作' : 'Hire'}> <Layout title={isZh ? '合作' : 'Hire'}>
<GlassHeader lang={lang} client:load transition:persist="header" /> <GlassHeader lang={lang} client:load transition:persist="header" />
<main class="min-h-screen pt-24 pb-20"> <main id="main-content" class="min-h-screen pt-24 pb-20">
<Container> <Container>
<section class="page-content-main"> <section class="page-content-main">
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '合作方式' : 'Work With Me'}</h1> <h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '合作方式' : 'Work With Me'}</h1>

View File

@@ -33,9 +33,9 @@ const latestPosts = sortPostsByDate(
--- ---
<Layout title={isZh ? '首页' : 'Home'}> <Layout title={isZh ? '首页' : 'Home'}>
<GlassHeader lang={lang} client:load transition:persist="header" /> <GlassHeader lang={lang} client:idle transition:persist="header" />
<main class="min-h-screen pt-24 pb-20"> <main id="main-content" class="min-h-screen pt-24 pb-20">
<Container> <Container>
<section class="page-content-main space-y-6"> <section class="page-content-main space-y-6">
<p class="text-sm font-semibold uppercase tracking-[0.2em] text-primary">{isZh ? 'TERMINAL PROFILE' : 'TERMINAL PROFILE'}</p> <p class="text-sm font-semibold uppercase tracking-[0.2em] text-primary">{isZh ? 'TERMINAL PROFILE' : 'TERMINAL PROFILE'}</p>
@@ -180,5 +180,5 @@ const latestPosts = sortPostsByDate(
</Container> </Container>
</main> </main>
<Footer lang={lang} client:load /> <Footer lang={lang} client:idle />
</Layout> </Layout>

View File

@@ -54,7 +54,7 @@ const today = new Date().toISOString().split('T')[0];
<Layout title="Now"> <Layout title="Now">
<GlassHeader lang={lang} client:load transition:persist="header" /> <GlassHeader lang={lang} client:load transition:persist="header" />
<main class="min-h-screen pt-24 pb-20"> <main id="main-content" class="min-h-screen pt-24 pb-20">
<Container> <Container>
<section class="page-content-main"> <section class="page-content-main">
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">Now</h1> <h1 class="text-4xl font-bold tracking-tight sm:text-5xl">Now</h1>

View File

@@ -20,7 +20,7 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
<Layout title={isZh ? '项目' : 'Projects'}> <Layout title={isZh ? '项目' : 'Projects'}>
<GlassHeader lang={lang} client:load transition:persist="header" /> <GlassHeader lang={lang} client:load transition:persist="header" />
<main class="min-h-screen pt-24 pb-20"> <main id="main-content" class="min-h-screen pt-24 pb-20">
<Container> <Container>
<section class="page-content-main"> <section class="page-content-main">
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '项目经历' : 'Project Experience'}</h1> <h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '项目经历' : 'Project Experience'}</h1>

View File

@@ -1,14 +0,0 @@
---
title: "Moved to Uses"
description: "This page has moved."
layout: "../layouts/AboutLayout.astro"
---
# This page moved
The old **Services** page has been replaced by focused pages:
- [Uses](/uses)
- [About (Contact Card)](/about#contact-card)
If you are hiring for a remote role, please start from [Hire](/hire) and use the contact card on that page.

View File

@@ -23,7 +23,7 @@ const prefix = isZh ? '/zh' : '';
<Layout title={isZh ? '关于' : 'About'}> <Layout title={isZh ? '关于' : 'About'}>
<GlassHeader lang={lang} client:load transition:persist="header" /> <GlassHeader lang={lang} client:load transition:persist="header" />
<main class="min-h-screen pt-24 pb-20"> <main id="main-content" class="min-h-screen pt-24 pb-20">
<Container> <Container>
<section class="page-content-main" id="overview"> <section class="page-content-main" id="overview">
<div class="mb-6 flex flex-wrap gap-2 text-sm"> <div class="mb-6 flex flex-wrap gap-2 text-sm">

View File

@@ -20,7 +20,7 @@ const isZh = lang === 'zh';
<Layout title={isZh ? '合作' : 'Hire'}> <Layout title={isZh ? '合作' : 'Hire'}>
<GlassHeader lang={lang} client:load transition:persist="header" /> <GlassHeader lang={lang} client:load transition:persist="header" />
<main class="min-h-screen pt-24 pb-20"> <main id="main-content" class="min-h-screen pt-24 pb-20">
<Container> <Container>
<section class="page-content-main"> <section class="page-content-main">
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '合作方式' : 'Work With Me'}</h1> <h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '合作方式' : 'Work With Me'}</h1>

View File

@@ -33,9 +33,9 @@ const latestPosts = sortPostsByDate(
--- ---
<Layout title={isZh ? '首页' : 'Home'}> <Layout title={isZh ? '首页' : 'Home'}>
<GlassHeader lang={lang} client:load transition:persist="header" /> <GlassHeader lang={lang} client:idle transition:persist="header" />
<main class="min-h-screen pt-24 pb-20"> <main id="main-content" class="min-h-screen pt-24 pb-20">
<Container> <Container>
<section class="page-content-main space-y-6"> <section class="page-content-main space-y-6">
<p class="text-sm font-semibold uppercase tracking-[0.2em] text-primary">{isZh ? 'TERMINAL PROFILE' : 'TERMINAL PROFILE'}</p> <p class="text-sm font-semibold uppercase tracking-[0.2em] text-primary">{isZh ? 'TERMINAL PROFILE' : 'TERMINAL PROFILE'}</p>
@@ -180,5 +180,5 @@ const latestPosts = sortPostsByDate(
</Container> </Container>
</main> </main>
<Footer lang={lang} client:load /> <Footer lang={lang} client:idle />
</Layout> </Layout>

View File

@@ -54,7 +54,7 @@ const today = new Date().toISOString().split('T')[0];
<Layout title="现在"> <Layout title="现在">
<GlassHeader lang={lang} client:load transition:persist="header" /> <GlassHeader lang={lang} client:load transition:persist="header" />
<main class="min-h-screen pt-24 pb-20"> <main id="main-content" class="min-h-screen pt-24 pb-20">
<Container> <Container>
<section class="page-content-main"> <section class="page-content-main">
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">现在</h1> <h1 class="text-4xl font-bold tracking-tight sm:text-5xl">现在</h1>

View File

@@ -20,7 +20,7 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
<Layout title={isZh ? '项目' : 'Projects'}> <Layout title={isZh ? '项目' : 'Projects'}>
<GlassHeader lang={lang} client:load transition:persist="header" /> <GlassHeader lang={lang} client:load transition:persist="header" />
<main class="min-h-screen pt-24 pb-20"> <main id="main-content" class="min-h-screen pt-24 pb-20">
<Container> <Container>
<section class="page-content-main"> <section class="page-content-main">
<h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '项目经历' : 'Project Experience'}</h1> <h1 class="text-4xl font-bold tracking-tight sm:text-5xl">{isZh ? '项目经历' : 'Project Experience'}</h1>

View File

@@ -1,14 +0,0 @@
---
title: "页面已迁移"
description: "该页面已迁移到新的结构。"
layout: "../../layouts/AboutLayout.astro"
---
# 页面已迁移
原 **服务** 页面已拆分为更清晰的页面:
- [工具](/zh/uses)
- [关于(联系卡片)](/zh/about#contact-card)
如需沟通远程岗位,请优先查看 [合作](/zh/hire),并使用页面内的联系卡片。

View File

@@ -5,7 +5,7 @@
font-weight: 400; font-weight: 400;
font-stretch: normal; font-stretch: normal;
font-display: swap; font-display: swap;
src: url('./fonts/archivo-400.ttf') format('truetype'); src: url('/fonts/archivo-400.woff2') format('woff2');
} }
@font-face { @font-face {
font-family: 'Archivo'; font-family: 'Archivo';
@@ -13,7 +13,7 @@
font-weight: 700; font-weight: 700;
font-stretch: normal; font-stretch: normal;
font-display: swap; font-display: swap;
src: url('./fonts/archivo-700.ttf') format('truetype'); src: url('/fonts/archivo-700.woff2') format('woff2');
} }
/* Local Fonts - Space Grotesk */ /* Local Fonts - Space Grotesk */
@@ -22,12 +22,12 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
src: url('./fonts/space-grotesk-400.ttf') format('truetype'); src: url('/fonts/space-grotesk-400.woff2') format('woff2');
} }
@font-face { @font-face {
font-family: 'Space Grotesk'; font-family: 'Space Grotesk';
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
src: url('./fonts/space-grotesk-700.ttf') format('truetype'); src: url('/fonts/space-grotesk-700.woff2') format('woff2');
} }

View File

@@ -5,9 +5,9 @@
/* ========== 基础变量覆盖 ========== */ /* ========== 基础变量覆盖 ========== */
:root { :root {
/* 主题色 - 使用网站的紫色渐变accent */ /* 主题色 - 使用网站的蓝色主题色 */
--waline-theme-color: #8B5CF6; --waline-theme-color: #2563EB;
--waline-active-color: #EC4899; --waline-active-color: #F97316;
/* 背景色 */ /* 背景色 */
--waline-bg-color: oklch(1 0 0); --waline-bg-color: oklch(1 0 0);
@@ -23,7 +23,7 @@
--waline-border-color: oklch(0.87 0 0); --waline-border-color: oklch(0.87 0 0);
/* 其他颜色 */ /* 其他颜色 */
--waline-badge-color: #8B5CF6; --waline-badge-color: #2563EB;
--waline-info-bg-color: oklch(0.97 0 0); --waline-info-bg-color: oklch(0.97 0 0);
--waline-info-color: oklch(0.55 0 0); --waline-info-color: oklch(0.55 0 0);
--waline-bq-color: oklch(0.93 0 0); --waline-bq-color: oklch(0.93 0 0);
@@ -109,18 +109,18 @@
} }
.wl-btn.primary { .wl-btn.primary {
background: linear-gradient(135deg, #8B5CF6, #EC4899) !important; background: #2563EB !important;
border: none !important; border: none !important;
color: white !important; color: white !important;
font-weight: 500 !important; font-weight: 500 !important;
} }
.wl-btn.primary:hover { .wl-btn.primary:hover {
background: linear-gradient(135deg, #7C3AED, #DB2777) !important; background: #1D4ED8 !important;
border: none !important; border: none !important;
color: white !important; color: white !important;
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3); box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
} }
/* ========== 头部标签页 ========== */ /* ========== 头部标签页 ========== */
@@ -156,7 +156,7 @@
/* ========== 徽章样式 ========== */ /* ========== 徽章样式 ========== */
.wl-badge { .wl-badge {
background: linear-gradient(135deg, #8B5CF6, #EC4899) !important; background: linear-gradient(135deg, #2563EB, #F97316) !important;
border: none !important; border: none !important;
color: white !important; color: white !important;
font-size: 0.7em !important; font-size: 0.7em !important;