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

@@ -17,7 +17,7 @@ export default defineConfig({
},
},
integrations: [
react(),
react(),
sitemap({
i18n: {
defaultLocale: 'en',
@@ -31,6 +31,16 @@ export default defineConfig({
],
vite: {
plugins: [tailwindcss()],
build: {
cssCodeSplit: true,
rollupOptions: {
output: {
manualChunks: {
'vendor-react': ['react', 'react-dom'],
}
}
}
}
},
i18n: {
// 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

@@ -26,9 +26,11 @@ export default function AuthorCard({ lang, author }: AuthorCardProps) {
<div className="flex-shrink-0 mb-4">
<div className="w-20 h-20 bg-gradient-to-br from-primary 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}
<img
src={authorInfo.avatar}
alt={authorInfo.name}
loading="lazy"
decoding="async"
className="w-full h-full rounded-full object-cover"
/>
) : (

View File

@@ -1,7 +1,6 @@
import { useTranslations } from "@/i18n/utils";
import type { Lang } from "@/types/i18n";
import { personalInfo } from "@/lib/data/index";
import { motion } from "framer-motion";
import { useState, useEffect } from "react";
import { defaultLang } from "@/i18n/ui";
import Container from "./ui/Container.tsx";
@@ -9,7 +8,7 @@ import { type FooterProps } from "@/types";
export default function Footer({ lang: propLang }: FooterProps) {
const [lang, setLang] = useState<Lang>(propLang || defaultLang);
useEffect(() => {
const htmlLang = document.documentElement.lang as Lang;
if (htmlLang && (!propLang || htmlLang !== lang)) {
@@ -21,60 +20,38 @@ export default function Footer({ lang: propLang }: FooterProps) {
return (
<footer className="border-t border-primary/10 py-6 bg-gradient-to-b from-background to-muted/20 backdrop-blur-sm">
<Container>
<motion.div
<div
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">
<motion.p
className="text-sm text-muted-foreground text-center md:text-left"
whileHover={{ scale: 1.01 }}
<div className="flex flex-col gap-2">
<p
className="text-sm text-muted-foreground text-center md:text-left hover:scale-[1.01] transition-transform"
>
&copy; {new Date().getFullYear()} { personalInfo.name }. {t('footer.rights')}
</motion.p>
<motion.p
className="text-sm text-primary font-medium text-center md:text-left"
whileHover={{ scale: 1.01 }}
</p>
<p
className="text-sm text-primary font-medium text-center md:text-left hover:scale-[1.01] transition-transform"
>
{lang === 'zh' ? '正在招聘远程工程师或寻求项目合作?欢迎联系我。' : 'Hiring for a remote engineering role or looking for project collaboration? Lets connect.'}
</motion.p>
</motion.div>
<motion.p
className="text-sm text-muted-foreground mt-2 md:mt-0 text-center md:text-left"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ delay: 0.2, duration: 0.5 }}
viewport={{ once: true }}
whileHover={{ scale: 1.01 }}
</p>
</div>
<p
className="text-sm text-muted-foreground mt-2 md:mt-0 text-center md:text-left hover:scale-[1.01] transition-transform"
>
Built with{" "}
<motion.span
className="inline-block"
initial={{ rotate: 0 }}
whileHover={{ rotate: 360 }}
transition={{ duration: 0.5 }}
<span
className="inline-block hover:rotate-360 transition-transform duration-500"
>
💻
</motion.span>{" "}
</span>{" "}
and{" "}
<motion.span
className="inline-block"
animate={{
scale: [1, 1.2, 1],
}}
transition={{
repeat: Infinity,
repeatType: "reverse",
duration: 1.5,
}}
<span
className="inline-block animate-pulse"
>
</motion.span>
</motion.p>
</motion.div>
</span>
</p>
</div>
</Container>
</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 { defaultLang } from "@/i18n/ui";
import { type GlassHeaderProps } from "@/types";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
@@ -98,7 +97,7 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
};
return (
<motion.header
<header
className={`fixed top-0 z-50 w-full transition-all duration-300 ${
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'
@@ -106,10 +105,8 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
}`}
>
<Container className="p-4 flex justify-between items-center">
<motion.a
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="flex items-center text-lg font-medium transition-colors duration-150 hover:text-foreground/80"
<a
className="flex items-center text-lg font-medium transition-colors duration-150 hover:text-foreground/80 hover:scale-105 active:scale-95"
href={getLocalizedPath('/', lang)}
>
<div className="w-6 h-6 mr-2 flex items-center justify-center">
@@ -119,50 +116,48 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
</svg>
</div>
<span className="gradient-text font-bold">{personalInfo.name}</span>
</motion.a>
</a>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center space-x-6 text-sm font-medium">
{navItems.map((item) => {
const active = isActive(item.href);
return (
<motion.a
<a
key={item.key}
href={item.href}
className={cn(
"flex items-center gap-2 transition-colors duration-150 px-3 py-2 rounded-md hover:bg-primary/5",
active
? "text-primary bg-primary/5 font-semibold"
active
? "text-primary bg-primary/5 font-semibold"
: "text-foreground/70 hover:text-primary"
)}
whileHover={{ y: -2 }}
>
<item.icon size={16} />
{t(item.key as any)}
</motion.a>
</a>
);
})}
</nav>
<div className="flex items-center space-x-3">
{/* Language Switcher added here */}
<motion.div>
<div>
<LanguageSwitcher lang={lang} />
</motion.div>
<motion.div>
<ThemeToggle />
</motion.div>
</div>
{/* Mobile Menu Button */}
<motion.button
className="md:hidden p-2 text-foreground transition-all duration-150 hover:bg-foreground/10 active:bg-foreground/20 rounded-md"
<div>
<ThemeToggle />
</div>
{/* Mobile Menu Button */}
<button
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}
aria-label="Toggle menu"
whileTap={{ scale: 0.9 }}
>
{isMenuOpen ? <X size={24} /> : <Menu size={24} />}
</motion.button>
</button>
</div>
</Container>
@@ -171,15 +166,15 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
isMenuOpen ? 'max-h-80 opacity-100' : 'max-h-0 opacity-0'
}`}>
<div className={`py-4 px-4 border-t border-border/10 glass-effect shadow-xl ${
isScrolled
? 'bg-white/90 dark:bg-black/80'
isScrolled
? 'bg-white/90 dark:bg-black/80'
: 'bg-white/95 dark:bg-black/85'
}`}>
<nav className="flex flex-col space-y-3 text-sm font-medium">
{navItems.map((item) => {
const active = isActive(item.href);
return (
<motion.a
<a
key={item.key}
href={item.href}
className={cn(
@@ -192,12 +187,12 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
>
<item.icon size={18} />
{t(item.key as any)}
</motion.a>
</a>
);
})}
</nav>
</div>
</div>
</motion.header>
</header>
);
}

View File

@@ -3,19 +3,18 @@ import { getLocalizedPath } from "@/i18n/utils";
import type { Lang } from "@/types/i18n";
import { languages as i18nLanguages, defaultLang } from "@/i18n/ui";
import { Check, ChevronDown } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { type LanguageSwitcherProps } from "@/types";
const availableLanguages = Object.entries(i18nLanguages).map(([code, name]) => ({
code: code as Lang,
name,
icon: code === 'en' ? '🇬🇧' : code === 'zh' ? '🇨🇳' : '🌐'
icon: code === 'en' ? '🇬🇧' : code === 'zh' ? '🇨🇳' : '🌐'
}));
export default function LanguageSwitcher({ lang: propLang }: LanguageSwitcherProps) {
const [isOpen, setIsOpen] = useState(false);
const [currentLang, setCurrentLang] = useState<Lang>(propLang || defaultLang);
useEffect(() => {
if (typeof window !== "undefined") {
const pathLang = window.location.pathname.startsWith("/zh") ? "zh" : "en";
@@ -32,10 +31,10 @@ export default function LanguageSwitcher({ lang: propLang }: LanguageSwitcherPro
}
}
}, [propLang, currentLang]);
const [selectedLanguage, setSelectedLanguage] = useState(() => {
return availableLanguages.find(l => l.code === currentLang) ||
availableLanguages.find(l => l.code === defaultLang) ||
return availableLanguages.find(l => l.code === currentLang) ||
availableLanguages.find(l => l.code === defaultLang) ||
availableLanguages[0];
});
@@ -65,45 +64,38 @@ export default function LanguageSwitcher({ lang: propLang }: LanguageSwitcherPro
return (
<div className="relative">
<motion.button
<button
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}`}
whileTap={{ scale: 0.95 }}
>
<span className="mr-1.5">{selectedLanguage.icon}</span>
<span className="hidden sm:inline">{selectedLanguage.name}</span>
<span className="sm:hidden">{selectedLanguage.code.toUpperCase()}</span>
<ChevronDown size={16} className={`ml-1.5 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} />
</motion.button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -10 }}
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">
{availableLanguages.map((lang) => (
<button
key={lang.code}
onClick={() => handleSelectLanguage(lang)}
className="w-full text-left px-3 py-1.5 text-sm rounded-sm hover:bg-accent hover:text-accent-foreground flex items-center justify-between cursor-pointer focus:bg-accent focus:outline-none"
role="menuitem"
>
<span className="flex items-center">
<span className="mr-2">{lang.icon}</span>
{lang.name}
</span>
{selectedLanguage.code === lang.code && <Check size={16} className="text-primary" />}
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</button>
{isOpen && (
<div
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"
>
<div className="p-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
{availableLanguages.map((lang) => (
<button
key={lang.code}
onClick={() => handleSelectLanguage(lang)}
className="w-full text-left px-3 py-1.5 text-sm rounded-sm hover:bg-accent hover:text-accent-foreground flex items-center justify-between cursor-pointer focus:bg-accent focus:outline-none"
role="menuitem"
>
<span className="flex items-center">
<span className="mr-2">{lang.icon}</span>
{lang.name}
</span>
{selectedLanguage.code === lang.code && <Check size={16} className="text-primary" />}
</button>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -80,9 +80,11 @@ const readMoreText = lang === 'zh' ? '阅读更多' : 'Read More';
<div class="flex flex-col md:flex-row">
{/* Featured Image */}
<div class="relative overflow-hidden md:w-80 md:flex-shrink-0">
<img
src={post.image}
<img
src={post.image}
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"
/>
<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>
<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}
</a>
</h2>
@@ -135,6 +137,7 @@ const readMoreText = lang === 'zh' ? '阅读更多' : 'Read More';
<a
href={`${postBaseUrl}${post.slug}`}
data-astro-reload
class="text-primary hover:text-primary font-medium flex items-center group"
>
{readMoreText}

View File

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

View File

@@ -19,7 +19,7 @@ const title = lang === 'zh' ? '目录' : 'Table of Contents';
</svg>
{title}
</h3>
<ScrollArea className="w-full" client:load>
<ScrollArea className="w-full" client:visible>
<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>

View File

@@ -107,7 +107,7 @@ const finalReadingTime = readTime ? parseInt(readTime.replace(/\D/g, '')) : unde
<!-- Comments Section -->
<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>
<!-- 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 type { Lang } from "@/types/i18n";
import { defaultLang } from "@/i18n/ui";
import { personalInfo } from "@/lib/data";
import "../styles/global.css";
interface Props {
title?: string;
description?: string;
image?: string;
articleDate?: string;
}
const lang = Astro.currentLocale as Lang || defaultLang;
const { title = "Joey Z. - Portfolio", description = "Engineering-focused personal website" } =
Astro.props;
const isZh = lang === 'zh';
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>
@@ -23,9 +33,35 @@ const t = useTranslations(lang);
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<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="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 -->
<ClientRouter />
<meta name="view-transition" content="same-origin" />
@@ -38,6 +74,14 @@ const t = useTranslations(lang);
<body
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
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>
<slot />
<BackToTop client:load />
<BackToTop client:idle />
</body>
</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'}>
<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>
<section class="page-content-main" id="overview">
<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'}>
<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>
<section class="page-content-main">
<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'}>
<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>
<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>
@@ -180,5 +180,5 @@ const latestPosts = sortPostsByDate(
</Container>
</main>
<Footer lang={lang} client:load />
<Footer lang={lang} client:idle />
</Layout>

View File

@@ -54,7 +54,7 @@ const today = new Date().toISOString().split('T')[0];
<Layout title="Now">
<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>
<section class="page-content-main">
<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'}>
<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>
<section class="page-content-main">
<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'}>
<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>
<section class="page-content-main" id="overview">
<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'}>
<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>
<section class="page-content-main">
<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'}>
<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>
<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>
@@ -180,5 +180,5 @@ const latestPosts = sortPostsByDate(
</Container>
</main>
<Footer lang={lang} client:load />
<Footer lang={lang} client:idle />
</Layout>

View File

@@ -54,7 +54,7 @@ const today = new Date().toISOString().split('T')[0];
<Layout title="现在">
<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>
<section class="page-content-main">
<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'}>
<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>
<section class="page-content-main">
<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-stretch: normal;
font-display: swap;
src: url('./fonts/archivo-400.ttf') format('truetype');
src: url('/fonts/archivo-400.woff2') format('woff2');
}
@font-face {
font-family: 'Archivo';
@@ -13,7 +13,7 @@
font-weight: 700;
font-stretch: normal;
font-display: swap;
src: url('./fonts/archivo-700.ttf') format('truetype');
src: url('/fonts/archivo-700.woff2') format('woff2');
}
/* Local Fonts - Space Grotesk */
@@ -22,12 +22,12 @@
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('./fonts/space-grotesk-400.ttf') format('truetype');
src: url('/fonts/space-grotesk-400.woff2') format('woff2');
}
@font-face {
font-family: 'Space Grotesk';
font-style: normal;
font-weight: 700;
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 {
/* 主题色 - 使用网站的紫色渐变accent */
--waline-theme-color: #8B5CF6;
--waline-active-color: #EC4899;
/* 主题色 - 使用网站的蓝色主题色 */
--waline-theme-color: #2563EB;
--waline-active-color: #F97316;
/* 背景色 */
--waline-bg-color: oklch(1 0 0);
@@ -23,7 +23,7 @@
--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-color: oklch(0.55 0 0);
--waline-bq-color: oklch(0.93 0 0);
@@ -109,18 +109,18 @@
}
.wl-btn.primary {
background: linear-gradient(135deg, #8B5CF6, #EC4899) !important;
background: #2563EB !important;
border: none !important;
color: white !important;
font-weight: 500 !important;
}
.wl-btn.primary:hover {
background: linear-gradient(135deg, #7C3AED, #DB2777) !important;
background: #1D4ED8 !important;
border: none !important;
color: white !important;
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 {
background: linear-gradient(135deg, #8B5CF6, #EC4899) !important;
background: linear-gradient(135deg, #2563EB, #F97316) !important;
border: none !important;
color: white !important;
font-size: 0.7em !important;