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`.
This commit is contained in:
zguiyang
2026-03-17 16:42:00 +08:00
parent 45bdc497d8
commit eb6bef3726
20 changed files with 97 additions and 148 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

View File

@@ -17,7 +17,7 @@ export default defineConfig({
}, },
}, },
integrations: [ integrations: [
react(), react(),
sitemap({ sitemap({
i18n: { i18n: {
defaultLocale: 'en', defaultLocale: 'en',
@@ -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.

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";
@@ -9,7 +8,7 @@ import { type FooterProps } from "@/types";
export default function Footer({ lang: propLang }: FooterProps) { export default function Footer({ lang: propLang }: FooterProps) {
const [lang, setLang] = useState<Lang>(propLang || defaultLang); const [lang, setLang] = useState<Lang>(propLang || defaultLang);
useEffect(() => { useEffect(() => {
const htmlLang = document.documentElement.lang as Lang; const htmlLang = document.documentElement.lang as Lang;
if (htmlLang && (!propLang || htmlLang !== lang)) { if (htmlLang && (!propLang || htmlLang !== lang)) {
@@ -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,50 +116,48 @@ 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(
"flex items-center gap-2 transition-colors duration-150 px-3 py-2 rounded-md hover:bg-primary/5", "flex items-center gap-2 transition-colors duration-150 px-3 py-2 rounded-md hover:bg-primary/5",
active active
? "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>
<ThemeToggle />
</motion.div>
{/* Mobile Menu Button */} <div>
<motion.button <ThemeToggle />
className="md:hidden p-2 text-foreground transition-all duration-150 hover:bg-foreground/10 active:bg-foreground/20 rounded-md" </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} 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>
@@ -171,15 +166,15 @@ export default function GlassHeader({ lang: propLang }: GlassHeaderProps) {
isMenuOpen ? 'max-h-80 opacity-100' : 'max-h-0 opacity-0' 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 ${ <div className={`py-4 px-4 border-t border-border/10 glass-effect shadow-xl ${
isScrolled isScrolled
? 'bg-white/90 dark:bg-black/80' ? 'bg-white/90 dark:bg-black/80'
: 'bg-white/95 dark:bg-black/85' : 'bg-white/95 dark:bg-black/85'
}`}> }`}>
<nav className="flex flex-col space-y-3 text-sm font-medium"> <nav className="flex flex-col space-y-3 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(
@@ -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,19 +3,18 @@ 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]) => ({
code: code as Lang, code: code as Lang,
name, name,
icon: code === 'en' ? '🇬🇧' : code === 'zh' ? '🇨🇳' : '🌐' icon: code === 'en' ? '🇬🇧' : code === 'zh' ? '🇨🇳' : '🌐'
})); }));
export default function LanguageSwitcher({ lang: propLang }: LanguageSwitcherProps) { export default function LanguageSwitcher({ lang: propLang }: LanguageSwitcherProps) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [currentLang, setCurrentLang] = useState<Lang>(propLang || defaultLang); const [currentLang, setCurrentLang] = useState<Lang>(propLang || defaultLang);
useEffect(() => { useEffect(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const pathLang = window.location.pathname.startsWith("/zh") ? "zh" : "en"; const pathLang = window.location.pathname.startsWith("/zh") ? "zh" : "en";
@@ -32,10 +31,10 @@ export default function LanguageSwitcher({ lang: propLang }: LanguageSwitcherPro
} }
} }
}, [propLang, currentLang]); }, [propLang, currentLang]);
const [selectedLanguage, setSelectedLanguage] = useState(() => { const [selectedLanguage, setSelectedLanguage] = useState(() => {
return availableLanguages.find(l => l.code === currentLang) || return availableLanguages.find(l => l.code === currentLang) ||
availableLanguages.find(l => l.code === defaultLang) || availableLanguages.find(l => l.code === defaultLang) ||
availableLanguages[0]; availableLanguages[0];
}); });
@@ -65,45 +64,38 @@ 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 && ( <div
<motion.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"
initial={{ opacity: 0, y: -10 }} >
animate={{ opacity: 1, y: 0 }} <div className="p-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
exit={{ opacity: 0, y: -10 }} {availableLanguages.map((lang) => (
transition={{ duration: 0.2 }} <button
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" key={lang.code}
> onClick={() => handleSelectLanguage(lang)}
<div className="p-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu"> 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"
{availableLanguages.map((lang) => ( role="menuitem"
<button >
key={lang.code} <span className="flex items-center">
onClick={() => handleSelectLanguage(lang)} <span className="mr-2">{lang.icon}</span>
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" {lang.name}
role="menuitem" </span>
> {selectedLanguage.code === lang.code && <Check size={16} className="text-primary" />}
<span className="flex items-center"> </button>
<span className="mr-2">{lang.icon}</span> ))}
{lang.name} </div>
</span> </div>
{selectedLanguage.code === lang.code && <Check size={16} className="text-primary" />} )}
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div> </div>
); );
} }

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

@@ -23,6 +23,9 @@ 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} /> <meta name="description" content={description} />
<title>{title} | {t("site.title")}</title> <title>{title} | {t("site.title")}</title>
@@ -46,7 +49,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>

View File

@@ -33,7 +33,7 @@ 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 class="min-h-screen pt-24 pb-20">
<Container> <Container>
@@ -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

@@ -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

@@ -33,7 +33,7 @@ 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 class="min-h-screen pt-24 pb-20">
<Container> <Container>
@@ -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

@@ -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');
} }