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: [
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.

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

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

@@ -23,6 +23,9 @@ 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>
@@ -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>
<slot />
<BackToTop client:load />
<BackToTop client:idle />
</body>
</html>

View File

@@ -33,7 +33,7 @@ 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">
<Container>
@@ -180,5 +180,5 @@ const latestPosts = sortPostsByDate(
</Container>
</main>
<Footer lang={lang} client:load />
<Footer lang={lang} client:idle />
</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'}>
<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">
<Container>
@@ -180,5 +180,5 @@ const latestPosts = sortPostsByDate(
</Container>
</main>
<Footer lang={lang} client:load />
<Footer lang={lang} client:idle />
</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-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');
}