Compare commits

...

10 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
zguiyang
45bdc497d8 refactor: naturalize English copy with developer voice
Remove AI-generated phrasing patterns and replace with conversational
developer language. Key changes:
- Replace "always-learning mindset" with authentic expressions
- Use "I built for myself" storytelling approach for projects
- Simplify complex phrases like "bridge comprehension breaks"
- Make method card descriptions more conversational
2026-03-17 15:12:14 +08:00
zguiyang
6327cf4d47 feat(hire): add payment terms (443) and remove mock labels
- Add payment terms section with 40% upfront, 40% mid-review, 20% completion
- Add payment FAQ entry
- Remove (Mock) labels from collaboration models and FAQ sections
2026-03-17 15:01:15 +08:00
zguiyang
8c7c6da03b refactor(now): restructure page with result-oriented content
- Replace Doing/Exploring/Shipping with Current Focus, Problems I'm Solving, Recent Outputs, Availability
- Focus on completed results rather than ongoing learning processes
- Add project status badges (Live/Coming Soon)
- Include abstract problem statements showing product thinking
- Add availability section with job preferences and project types
- Update date to auto-generate on each build
2026-03-17 13:37:58 +08:00
zguiyang
ff9dde98c7 feat(about): update narrative and capabilities with developer tone
Replaced formal descriptions with more natural developer voice for
about page narrative and core capabilities sections.
2026-03-17 12:42:43 +08:00
zguiyang
71d8af996a feat(projects): add cover images for Elynd and Linky with hover animation
- Added preview images for Elynd and Linky projects
- Added hover scale animation (scale-105) for cover images
2026-03-17 12:17:28 +08:00
zguiyang
014430e1b7 feat(projects): update Elynd details and add Linky project
- Revise Elynd from experimental AI workspace to AI English learning platform
- Add Linky as new indie project (self-hosted bookmark manager)
- Add status badges (completed/in-progress) to project cards
- Add Preview and GitHub action buttons to project cards
- Improve card layout with flexbox for better content alignment
2026-03-17 11:05:43 +08:00
37 changed files with 800 additions and 349 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,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

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

View File

@@ -44,7 +44,7 @@ export const heroPrinciples: LocalizedText[] = [
zh: '目前寻求远程工作机会,如你正在招聘,欢迎联系我。', zh: '目前寻求远程工作机会,如你正在招聘,欢迎联系我。',
}, },
{ {
en: 'Evolving from frontend toward full-stack, and building as an AI-era product engineer with an always-learning mindset.', en: 'Started as a frontend dev, now building full-stack skills while embracing AI as a productivity tool.',
zh: '我正在从前端走向全栈,持续进化为 AI 时代的产品工程师,不给自己设限。', zh: '我正在从前端走向全栈,持续进化为 AI 时代的产品工程师,不给自己设限。',
}, },
]; ];
@@ -90,15 +90,15 @@ export const brandProofPoints: LocalizedText[] = [
zh: '8 年企业级、金融科技、区块链系统经验', zh: '8 年企业级、金融科技、区块链系统经验',
}, },
{ {
en: 'Hands-on lead for architecture, core modules, and engineering workflows', en: 'Led architecture and core modules for multiple products',
zh: '可主导架构、核心模块和工程化流程落地', zh: '可主导架构、核心模块和工程化流程落地',
}, },
{ {
en: 'Strong in complex interactions, visual systems, and high-volume data workflows', en: 'Good at complex interactions, visual systems, and data-heavy workflows',
zh: '擅长复杂交互、可视化系统和大数据量业务流程', zh: '擅长复杂交互、可视化系统和大数据量业务流程',
}, },
{ {
en: 'Combines AI tooling with delivery to improve speed and clarity', en: 'Using AI tools to speed up development and understand codebases faster',
zh: '将 AI 工具用于研发提效、项目理解与文档沉淀', zh: '将 AI 工具用于研发提效、项目理解与文档沉淀',
}, },
]; ];
@@ -110,7 +110,7 @@ export const brandMethodCards: LocalizedCard[] = [
zh: '先定义问题', zh: '先定义问题',
}, },
description: { description: {
en: 'I align business context, technical boundaries, and delivery constraints before implementation.', en: 'I make sure we understand the problem before writing code — business context, technical limits, and timeline all need to be clear.',
zh: '编码之前先对齐业务上下文、技术边界和交付约束。', zh: '编码之前先对齐业务上下文、技术边界和交付约束。',
}, },
}, },
@@ -120,7 +120,7 @@ export const brandMethodCards: LocalizedCard[] = [
zh: '为迭代设计架构', zh: '为迭代设计架构',
}, },
description: { description: {
en: 'I build module boundaries and component conventions so systems can evolve safely.', en: 'I design module boundaries and component patterns so the system can grow without becoming a mess.',
zh: '通过模块边界与组件规范,确保系统能稳定演进。', zh: '通过模块边界与组件规范,确保系统能稳定演进。',
}, },
}, },
@@ -130,7 +130,7 @@ export const brandMethodCards: LocalizedCard[] = [
zh: '透明化执行', zh: '透明化执行',
}, },
description: { description: {
en: 'I keep progress visible via milestones, docs, and explicit decision records.', en: 'I share progress regularly — milestones, docs, and decisions are all visible to everyone involved.',
zh: '通过里程碑、文档和决策记录,让推进过程可视、可追踪。', zh: '通过里程碑、文档和决策记录,让推进过程可视、可追踪。',
}, },
}, },
@@ -267,35 +267,35 @@ export const brandTimeline: TimelineItem[] = [
export const aboutNarrative: LocalizedText[] = [ export const aboutNarrative: LocalizedText[] = [
{ {
en: 'I focus on complex business systems where architecture and long-term maintainability are critical.', en: '8 years of frontend experience, mostly working on complex business systems — fintech, enterprise tools, and products with heavy interactions. I care about code that survives the first release.',
zh: '我长期专注复杂业务系统,核心关注点是架构质量和长期可维护性。', zh: '做了 8 年前端,大部分时间在跟复杂业务系统打交道——金融、企业后台、交互密集型产品。代码要能经得起后续迭代,这是我关心的。',
}, },
{ {
en: 'My strengths combine frontend architecture, engineering processes, and cross-role collaboration.', en: 'Spent a lot of time building from scratch and extracting reusable patterns. Led frontend architecture for 3+ core products, set up internal component libraries, and pushed engineering standards across teams.',
zh: '我的优势在于前端架构能力、工程化推进能力和跨角色协作。', zh: '从 0 到 1 搭过不少项目,也从业务里抽过不少通用组件。带过 3+ 核心产品的前端架构,做过内部组件库,推过团队工程规范。',
}, },
{ {
en: 'This website is my long-term knowledge base for projects, methods, and practical lessons.', en: 'Recently started bringing AI into my workflow — not to chase trends, but because it genuinely helps me read code, analyze logic, and write docs faster.',
zh: '这个网站会持续沉淀项目案例、工作方法与可复用经验。', zh: '这两年在把 AI 融入日常开发流程,不是赶潮流,是真的拿它来帮忙读代码、分析逻辑、写文档。确实省时间。',
}, },
]; ];
export const aboutCapabilities: LocalizedText[] = [ export const aboutCapabilities: LocalizedText[] = [
{ {
en: 'Complex frontend system architecture and module governance', en: 'Frontend architecture & module design for complex products',
zh: '复杂前端系统架构与模块治理', zh: '复杂产品的模块设计与前端架构',
}, },
{ {
en: 'Data visualization and real-time interaction system implementation', en: 'Component libraries & engineering standardization (Vite / Webpack)',
zh: '数据可视化与实时交互系统开发', zh: '组件封装与工程化体系建设',
}, },
{ {
en: 'From-zero-to-one product frontend setup and engineering standardization', en: 'Complex interactions, data visualization, and real-time systems',
zh: '从 0 到 1 前端体系建设与工程规范落地', zh: '复杂交互、可视化与实时数据处理',
}, },
{ {
en: 'AI-assisted code understanding, documentation, and troubleshooting', en: 'AI-enhanced development workflow & cross-team collaboration',
zh: 'AI 辅助代码理解、文档沉淀与问题排查', zh: 'AI 提效开发与跨角色协作',
}, },
]; ];
@@ -382,6 +382,16 @@ export const collaborationProcess: LocalizedText[] = [
]; ];
export const collaborationFaq: QAItem[] = [ export const collaborationFaq: QAItem[] = [
{
question: {
en: 'Payment terms?',
zh: '付款方式?',
},
answer: {
en: '40% upfront, 40% at mid-point review, 20% upon completion.',
zh: '40% 预付款40% 中期验收20% 尾款。',
},
},
{ {
question: { question: {
en: 'Do you only do project-based work?', en: 'Do you only do project-based work?',

View File

@@ -159,32 +159,109 @@ const sharedCases: Project[] = [
}, },
{ {
id: 'elynd', id: 'elynd',
featured: false, featured: true,
type: 'experiment', type: 'product',
status: 'building', status: 'building',
role: 'Founder & Developer', role: 'Founder & Developer',
impact: 'Exploring AI-assisted development workflows in production-like scenarios', impact: 'Built an AI-powered English reading and learning platform for language learners',
systemType: 'AI-assisted workspace', systemType: 'AI-powered English reading & learning platform',
context: 'An open workspace product exploring practical AI collaboration in software development.', context: 'An AI-assisted English reading learning tool that combines reading, listening, word lookup, and AI Q&A to help users practice low-barrier language input.',
title: 'Elynd', title: 'Elynd - AI English Learning Platform',
icon: 'Zap', icon: 'BookOpen',
color: 'purple', color: 'purple',
image: { image: {
bg: 'from-purple-500/20 to-blue-500/20', bg: 'from-purple-500/20 to-blue-500/20',
hover: 'from-purple-500/30 to-blue-500/30', hover: 'from-purple-500/30 to-blue-500/30',
text: 'text-purple-500', text: 'text-purple-500',
}, },
challenges: ['Designing useful AI workflows without adding process burden'], challenges: [
responsibilities: ['Product exploration and end-to-end implementation'], 'Designing a smooth graded reading experience with content difficulty adaptation',
outcomes: ['Validated several practical AI-assisted development patterns'], 'Implementing real-time TTS audio playback with Azure Speech SDK',
description: [ 'Building an efficient word lookup system with instant definitions',
'An open AI workspace for builders.', 'Creating AI-powered Q&A that understands reading context',
'Used as an R&D track, not the primary professional narrative.',
], ],
coverImage: '', responsibilities: [
coverImageAlt: 'Elynd project cover', 'End-to-end product development from concept to launch',
tech: ['TypeScript', 'React', 'AI Workflow', 'Open Source'], 'Designed and implemented backend API with AdonisJS v6',
link: '#', 'Built frontend with Vue 3, TypeScript, and modern UI components',
'Integrated OpenAI SDK for AI conversation features',
'Implemented EPUB import and content parsing pipeline',
],
outcomes: [
'Launched a complete learning platform with 4 core features',
'Achieved smooth reading experience with progress tracking',
'Integrated Azure TTS for immersive listening practice',
'Enabled AI-powered interactive learning with context awareness',
],
description: [
'AI-powered English reading and learning platform.',
'Combines reading, listening, word lookup, and AI Q&A for comprehensive language learning.',
],
tech: [
'AdonisJS v6',
'PostgreSQL',
'Redis',
'Vue 3',
'TypeScript',
'Pinia',
'Reka UI',
'shadcn-vue',
'Tailwind CSS 4',
'OpenAI SDK',
'Azure Speech SDK',
'pnpm workspace',
],
link: 'https://github.com/zguiyang/elynd',
coverImage: 'https://cloud.zgyk.cc/f/VoFa/elynd-home.png',
coverImageAlt: 'Elynd Dashboard Preview',
links: {
github: 'https://github.com/zguiyang/elynd',
demo: 'https://elynd.zhaoguiyang.com/',
},
},
{
id: 'linky',
featured: true,
type: 'product',
status: 'completed',
role: 'Founder & Developer',
impact: 'Built a self-hosted knowledge management tool for bookmarks and notes',
systemType: 'Personal knowledge management system',
context: 'A simple, self-hosted tool for organizing bookmarks and writing notes with full user data control.',
title: 'Linky',
icon: 'Link',
color: 'cyan',
image: {
bg: 'from-cyan-500/20 to-blue-500/20',
hover: 'from-cyan-500/30 to-blue-500/30',
text: 'text-cyan-500',
},
challenges: [
'Designing intuitive bookmark organization with tags',
'Implementing AI-powered auto-tagging feature',
'Building self-hosted authentication system',
],
responsibilities: [
'End-to-end product development',
'Backend API design with AdonisJS',
'Frontend implementation with Nuxt',
],
outcomes: [
'Launched self-hosted version with full user data ownership',
'Integrated AI auto-tagging with OpenAI compatible APIs',
],
description: [
'A self-hosted bookmark manager and note-taking tool.',
'Focus on privacy, simplicity, and AI-assisted organization.',
],
tech: ['AdonisJS', 'Nuxt', 'PostgreSQL', 'Vue', 'Pinia', 'Tailwind CSS'],
link: 'https://github.com/zguiyang/linky',
coverImage: 'https://cloud.zgyk.cc/f/nYHM/linky-home.png',
coverImageAlt: 'Linky Dashboard Preview',
links: {
github: 'https://github.com/zguiyang/linky',
demo: 'https://linky.zhaoguiyang.com/',
},
}, },
]; ];
@@ -246,15 +323,74 @@ export const projects = {
{ {
...sharedCases[4], ...sharedCases[4],
role: '创始人 & 开发者', role: '创始人 & 开发者',
impact: '在真实场景中探索 AI 协作开发工作流', impact: '打造 AI 辅助英语阅读学习平台,帮助语言学习者提升阅读能力',
systemType: 'AI 协作工作空间', systemType: 'AI 驱动英语阅读学习平台',
context: '探索 AI 在软件工程中可落地协作方式的开放产品。', context: 'AI 辅助的英语阅读学习工具,通过「阅读 + 听读 + 查词 + AI 提问」四位一体帮助用户完成低门槛的语言输入练习。',
title: 'Elynd', title: 'Elynd - AI 英语学习平台',
challenges: ['在不增加流程负担前提下设计有效 AI 工作流'], challenges: [
responsibilities: ['产品探索与端到端实现'], '设计流畅的分级阅读体验,实现内容难度自适应',
outcomes: ['验证多种可实践的 AI 协作开发模式'], '集成 Azure Speech SDK 实现实时 TTS 音频播放',
description: ['一个面向构建者的开放 AI 工作空间。', '作为研发探索方向,不作为职业主叙事。'], '构建高效的即点查词系统,支持即时释义显示',
tech: ['TypeScript', 'React', 'AI 协作', '开源'], '打造理解阅读上下文的 AI 智能问答功能',
],
responsibilities: [
'从概念到上线的端到端产品开发',
'使用 AdonisJS v6 设计与实现后端 API',
'使用 Vue 3、TypeScript 和现代 UI 组件构建前端',
'集成 OpenAI SDK 实现 AI 对话功能',
'实现 EPUB 导入与内容解析流程',
],
outcomes: [
'上线具备 4 大核心功能的完整学习平台',
'实现流畅阅读体验与学习进度跟踪',
'集成 Azure TTS 实现沉浸式听读练习',
'支持上下文感知的 AI 智能交互学习',
],
description: [
'AI 驱动的英语阅读学习平台。',
'融合阅读、听读、查词、AI 问答,一站式语言学习体验。',
],
tech: [
'AdonisJS v6',
'PostgreSQL',
'Redis',
'Vue 3',
'TypeScript',
'Pinia',
'Reka UI',
'shadcn-vue',
'Tailwind CSS 4',
'OpenAI SDK',
'Azure Speech SDK',
'pnpm workspace',
],
},
{
...sharedCases[5],
role: '创始人 & 开发者',
impact: '打造自托管的书签管理与笔记工具,让用户完全掌控自己的数据',
systemType: '个人知识管理系统',
context: '简单、自托管的工具,用于组织书签和写笔记,数据完全由用户自己掌控。',
title: 'Linky',
challenges: [
'设计直观的书签组织系统,支持标签化管理',
'实现 AI 自动标签功能',
'构建自托管认证系统',
],
responsibilities: [
'端到端产品开发',
'使用 AdonisJS 设计后端 API',
'使用 Nuxt 实现前端',
],
outcomes: [
'上线自托管版本,用户完全拥有数据主权',
'集成 AI 自动标签功能,支持 OpenAI 兼容 API',
],
description: [
'自托管的书签管理器和笔记工具。',
'注重隐私、简洁和 AI 辅助整理。',
],
tech: ['AdonisJS', 'Nuxt', 'PostgreSQL', 'Vue', 'Pinia', 'Tailwind CSS'],
}, },
], ],
}; };

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>
@@ -32,7 +32,7 @@ const isZh = lang === 'zh';
</section> </section>
<section class="page-content-main mt-10"> <section class="page-content-main mt-10">
<h2 class="text-2xl font-bold tracking-tight">{isZh ? '合作模型Mock' : 'Collaboration Models (Mock)'}</h2> <h2 class="text-2xl font-bold tracking-tight">{isZh ? '合作模型' : 'Collaboration Models'}</h2>
<div class="mt-4 grid gap-4 md:grid-cols-3"> <div class="mt-4 grid gap-4 md:grid-cols-3">
{collaborationModels.map((model) => ( {collaborationModels.map((model) => (
<article class="page-surface p-5"> <article class="page-surface p-5">
@@ -69,7 +69,25 @@ const isZh = lang === 'zh';
</section> </section>
<section class="page-content-main mt-6 page-surface p-6"> <section class="page-content-main mt-6 page-surface p-6">
<h2 class="text-xl font-bold">{isZh ? '常见问题Mock' : 'FAQ (Mock)'}</h2> <h2 class="text-xl font-bold">{isZh ? '付款方式' : 'Payment Terms'}</h2>
<div class="mt-4 flex flex-wrap gap-3">
<div class="flex-1 min-w-[120px] rounded-lg border border-border/70 p-4 text-center">
<p class="text-2xl font-bold text-primary">40%</p>
<p class="text-sm text-muted-foreground">{isZh ? '预付款' : 'Upfront'}</p>
</div>
<div class="flex-1 min-w-[120px] rounded-lg border border-border/70 p-4 text-center">
<p class="text-2xl font-bold text-primary">40%</p>
<p class="text-sm text-muted-foreground">{isZh ? '中期验收' : 'Mid Review'}</p>
</div>
<div class="flex-1 min-w-[120px] rounded-lg border border-border/70 p-4 text-center">
<p class="text-2xl font-bold text-primary">20%</p>
<p class="text-sm text-muted-foreground">{isZh ? '尾款' : 'Completion'}</p>
</div>
</div>
</section>
<section class="page-content-main mt-6 page-surface p-6">
<h2 class="text-xl font-bold">{isZh ? '常见问题' : 'FAQ'}</h2>
<div class="mt-4 space-y-4"> <div class="mt-4 space-y-4">
{collaborationFaq.map((item) => ( {collaborationFaq.map((item) => (
<div class="rounded-md border border-border/70 p-4"> <div class="rounded-md border border-border/70 p-4">

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

@@ -9,55 +9,122 @@ import { defaultLang } from '@/i18n/ui';
const lang = (Astro.currentLocale as Lang) || defaultLang; const lang = (Astro.currentLocale as Lang) || defaultLang;
const isZh = lang === 'zh'; const isZh = lang === 'zh';
const doing = isZh const focuses = [
? ['推进工程化案例化的个人站升级', '优化远程协作下的交付流程与文档标准', '持续打磨 AI 协作开发实践'] {
: ['Upgrading this portfolio into an engineering case-study site', 'Improving remote-first delivery workflow and documentation quality', 'Refining practical AI-assisted development workflows']; title: 'Cloud Bookmark Tool',
status: 'Live',
desc: 'A self-hosted bookmark manager I built for myself — then realized others might want it too. Handles AI tagging, content extraction, and search.',
},
{
title: 'ELYND English Learning Tool',
status: 'Coming Soon',
desc: 'Reading English articles with synced audio, instant word lookup, and AI chat to ask questions about what I am reading.',
},
];
const exploring = isZh const problems = [
? ['复杂权限系统的前端边界设计', '大规模业务表单与状态流管理', '更稳定的跨团队异步协作机制'] {
: ['Frontend boundary design for complex permission systems', 'Scalable state management for large business forms', 'More stable async collaboration practices across teams']; title: 'The "Understanding Gap" in Language Learning',
desc: 'Vocabulary memorization lacks context, original texts are hard to understand, and there is no instant feedback when stuck.',
},
{
title: 'Productizing AI Capabilities',
desc: 'Making AI useful in real products, not just demos: tagging, text processing, and conversation that actually help.',
},
{
title: 'UX Design for AI-Powered Tools',
desc: 'Adding AI without making things complicated — keeping tools simple while powerful.',
},
];
const shipping = isZh const outputs = [
? ['工程案例库结构重构', '导航与路由转化路径优化', '双语文案统一与信息层级简化'] 'Cloud Bookmark Tool v1.0 launched',
: ['Refactoring project pages into engineering case studies', 'Optimizing navigation and conversion routes', 'Unifying EN/ZH copy and simplifying information hierarchy']; 'ELYND coming soon',
];
const availability = {
jobs: 'Remote / Full-stack / AI',
projects: 'Freelance / Subcontracting / Independent development',
preferences: 'Tool products / AI integration / Web applications',
};
const today = new Date().toISOString().split('T')[0];
--- ---
<Layout title={isZh ? '现在' : '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">{isZh ? '现在' : 'Now'}</h1> <h1 class="text-4xl font-bold tracking-tight sm:text-5xl">Now</h1>
<p class="mt-4 text-lg text-muted-foreground">{isZh ? '我当前的工作重点与近期交付。' : 'What I am focusing on right now and what I am shipping recently.'}</p> <p class="mt-4 text-lg text-muted-foreground">What I am working on and what I have shipped.</p>
</section> </section>
<section class="page-content-main mt-10 grid gap-6 md:grid-cols-3"> <!-- Current Focus -->
<article class="page-surface p-6"> <section class="page-content-main mt-10">
<h2 class="text-lg font-bold">{isZh ? '在做什么' : 'Doing'}</h2> <h2 class="text-xl font-bold">Current Focus</h2>
<ul class="mt-4 space-y-2 text-sm text-muted-foreground"> <div class="mt-4 space-y-4">
{doing.map((item) => <li>• {item}</li>)} {focuses.map((item) => (
</ul> <article class="page-surface p-5">
</article> <div class="flex items-center gap-3">
<h3 class="text-base font-bold">{item.title}</h3>
<article class="page-surface p-6"> <span class="rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary">{item.status}</span>
<h2 class="text-lg font-bold">{isZh ? '在研究什么' : 'Exploring'}</h2> </div>
<ul class="mt-4 space-y-2 text-sm text-muted-foreground"> <p class="mt-2 text-sm text-muted-foreground">{item.desc}</p>
{exploring.map((item) => <li>• {item}</li>)} </article>
</ul> ))}
</article> </div>
<article class="page-surface p-6">
<h2 class="text-lg font-bold">{isZh ? '最近交付' : 'Shipping'}</h2>
<ul class="mt-4 space-y-2 text-sm text-muted-foreground">
{shipping.map((item) => <li>• {item}</li>)}
</ul>
</article>
</section> </section>
<section class="page-content-main mt-8 page-surface p-6"> <!-- Problems I'm Solving -->
<section class="page-content-main mt-8">
<h2 class="text-xl font-bold">Problems I'm Solving</h2>
<div class="mt-4 space-y-3">
{problems.map((item) => (
<article class="rounded-lg border border-border/70 p-4">
<h3 class="text-sm font-semibold">{item.title}</h3>
<p class="mt-1.5 text-sm text-muted-foreground">{item.desc}</p>
</article>
))}
</div>
</section>
<!-- Recent Outputs -->
<section class="page-content-main mt-8">
<h2 class="text-xl font-bold">Recent Outputs</h2>
<ul class="mt-4 space-y-2">
{outputs.map((item) => (
<li class="flex items-center gap-2 text-sm">
<span class="h-1.5 w-1.5 rounded-full bg-accent"></span>
{item}
</li>
))}
</ul>
</section>
<!-- Availability -->
<section class="page-content-main mt-8 page-surface p-5">
<h2 class="text-xl font-bold">Availability</h2>
<div class="mt-4 grid gap-3 text-sm md:grid-cols-3">
<div>
<span class="font-medium">Open to: </span>
<span class="text-muted-foreground">{availability.jobs}</span>
</div>
<div>
<span class="font-medium">Projects: </span>
<span class="text-muted-foreground">{availability.projects}</span>
</div>
<div>
<span class="font-medium">Preferred: </span>
<span class="text-muted-foreground">{availability.preferences}</span>
</div>
</div>
</section>
<section class="page-content-main mt-8 page-surface p-4">
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
{isZh ? '最后更新2026-03-16' : 'Last updated: 2026-03-16'} Last updated: {today}
</p> </p>
</section> </section>
</Container> </Container>

View File

@@ -11,7 +11,7 @@ const lang = (Astro.currentLocale as Lang) || defaultLang;
const isZh = lang === 'zh'; const isZh = lang === 'zh';
const pageProjects = projects[lang].map((project) => ({ const pageProjects = projects[lang].map((project) => ({
...project, ...project,
category: project.id === 'elynd' ? 'indie' : 'enterprise', category: project.id === 'elynd' || project.id === 'linky' ? 'indie' : 'enterprise',
})); }));
const enterpriseProjects = pageProjects.filter((project) => project.category === 'enterprise'); const enterpriseProjects = pageProjects.filter((project) => project.category === 'enterprise');
const indieProjects = pageProjects.filter((project) => project.category === 'indie'); const indieProjects = pageProjects.filter((project) => project.category === 'indie');
@@ -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>
@@ -66,13 +66,13 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
</div> </div>
<div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3"> <div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
{indieProjects.map((project) => ( {indieProjects.map((project) => (
<article class="page-surface overflow-hidden"> <article class="page-surface h-full overflow-hidden flex flex-col">
<div class="relative aspect-[16/9] overflow-hidden border-b border-border/70 bg-muted/30"> <div class="relative aspect-[16/9] overflow-hidden border-b border-border/70 bg-muted/30">
{project.coverImage ? ( {project.coverImage ? (
<img <img
src={project.coverImage} src={project.coverImage}
alt={project.coverImageAlt || project.title} alt={project.coverImageAlt || project.title}
class="h-full w-full object-cover" class="h-full w-full object-cover transition-transform duration-300 ease-out hover:scale-105"
loading="lazy" loading="lazy"
decoding="async" decoding="async"
/> />
@@ -83,22 +83,59 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
<p class="mt-1 text-sm text-foreground/80">{project.systemType}</p> <p class="mt-1 text-sm text-foreground/80">{project.systemType}</p>
</div> </div>
)} )}
</div> {/* Status Badge */}
<div class="p-5"> <div class="absolute right-3 top-3">
<p class="text-xs font-semibold uppercase tracking-wider text-primary/80">{isZh ? '独立项目' : 'Independent'}</p> <span
<h3 class="mt-2 text-lg font-bold">{project.title}</h3> class={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ${
<p class="text-sm text-muted-foreground">{project.context}</p> project.status === 'completed'
<div class="mt-4 flex flex-wrap gap-2"> ? 'bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30'
{project.tech.slice(0, 5).map((tech) => ( : 'bg-amber-500/20 text-amber-600 dark:text-amber-400 border border-amber-500/30'
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span> }`}
))} >
{project.status === 'completed' ? (isZh ? '已完成' : 'Completed') : (isZh ? '开发中' : 'In Progress')}
</span>
</div>
</div>
<div class="flex flex-1 flex-col p-5">
<div class="flex flex-1 flex-col pb-4">
<h3 class="text-lg font-bold">{project.title}</h3>
{project.context && <p class="mt-2 text-sm text-muted-foreground">{project.context}</p>}
<div class="mt-4 flex flex-wrap gap-2">
{project.tech.slice(0, 5).map((tech) => (
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span>
))}
</div>
</div>
{/* Action Buttons */}
<div class="mt-auto flex gap-3 border-t border-border/50 pt-5">
{project.links?.demo && (
<a
href={project.links.demo}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 rounded-md bg-primary px-3.5 py-2 text-sm font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4">
<path d="M12.232 4.232a2.5 2.5 0 0 1 3.536 3.536l-1.225 1.224a.75.75 0 0 0 1.061 1.06l1.224-1.224a4 4 0 0 0-5.656-5.656l-3 3a4 4 0 0 0 .225 5.865.75.75 0 0 0 .977-1.139 2.5 2.5 0 0 1-.142-3.635l3-3Z" />
<path d="M11.603 7.963a.75.75 0 0 0-.977 1.139 2.5 2.5 0 0 1 .142 3.635l-3 3a4 4 0 0 0 5.656 5.656l1.224-1.224a.75.75 0 0 0-1.06-1.06l-1.224 1.224a2.5 2.5 0 0 1-3.536-3.536l1.225-1.224a.75.75 0 0 0-1.061-1.06l-1.224 1.224a4 4 0 0 0-5.656-5.656l-3 3a4 4 0 0 0-.225 5.865Z" />
</svg>
{isZh ? '预览' : 'Preview'}
</a>
)}
{project.links?.github && (
<a
href={project.links.github}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3.5 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-muted"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
GitHub
</a>
)}
</div> </div>
<p class="mt-4 text-sm font-semibold text-foreground/90">{isZh ? '结果:' : 'Outcome:'} {project.outcomes?.[0] ?? project.impact}</p>
{project.link !== '#' && (
<a href={project.link} target="_blank" rel="noopener noreferrer" class="mt-3 inline-flex text-sm font-semibold text-primary hover:text-primary/80">
{isZh ? '查看项目' : 'Open Project'}
</a>
)}
</div> </div>
</article> </article>
))} ))}

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>
@@ -32,7 +32,7 @@ const isZh = lang === 'zh';
</section> </section>
<section class="page-content-main mt-10"> <section class="page-content-main mt-10">
<h2 class="text-2xl font-bold tracking-tight">{isZh ? '合作模型Mock' : 'Collaboration Models (Mock)'}</h2> <h2 class="text-2xl font-bold tracking-tight">{isZh ? '合作模型' : 'Collaboration Models'}</h2>
<div class="mt-4 grid gap-4 md:grid-cols-3"> <div class="mt-4 grid gap-4 md:grid-cols-3">
{collaborationModels.map((model) => ( {collaborationModels.map((model) => (
<article class="page-surface p-5"> <article class="page-surface p-5">
@@ -69,7 +69,25 @@ const isZh = lang === 'zh';
</section> </section>
<section class="page-content-main mt-6 page-surface p-6"> <section class="page-content-main mt-6 page-surface p-6">
<h2 class="text-xl font-bold">{isZh ? '常见问题Mock' : 'FAQ (Mock)'}</h2> <h2 class="text-xl font-bold">{isZh ? '付款方式' : 'Payment Terms'}</h2>
<div class="mt-4 flex flex-wrap gap-3">
<div class="flex-1 min-w-[120px] rounded-lg border border-border/70 p-4 text-center">
<p class="text-2xl font-bold text-primary">40%</p>
<p class="text-sm text-muted-foreground">{isZh ? '预付款' : 'Upfront'}</p>
</div>
<div class="flex-1 min-w-[120px] rounded-lg border border-border/70 p-4 text-center">
<p class="text-2xl font-bold text-primary">40%</p>
<p class="text-sm text-muted-foreground">{isZh ? '中期验收' : 'Mid Review'}</p>
</div>
<div class="flex-1 min-w-[120px] rounded-lg border border-border/70 p-4 text-center">
<p class="text-2xl font-bold text-primary">20%</p>
<p class="text-sm text-muted-foreground">{isZh ? '尾款' : 'Completion'}</p>
</div>
</div>
</section>
<section class="page-content-main mt-6 page-surface p-6">
<h2 class="text-xl font-bold">{isZh ? '常见问题' : 'FAQ'}</h2>
<div class="mt-4 space-y-4"> <div class="mt-4 space-y-4">
{collaborationFaq.map((item) => ( {collaborationFaq.map((item) => (
<div class="rounded-md border border-border/70 p-4"> <div class="rounded-md border border-border/70 p-4">

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

@@ -9,55 +9,122 @@ import { defaultLang } from '@/i18n/ui';
const lang = (Astro.currentLocale as Lang) || defaultLang; const lang = (Astro.currentLocale as Lang) || defaultLang;
const isZh = lang === 'zh'; const isZh = lang === 'zh';
const doing = isZh const focuses = [
? ['推进工程化案例化的个人站升级', '优化远程协作下的交付流程与文档标准', '持续打磨 AI 协作开发实践'] {
: ['Upgrading this portfolio into an engineering case-study site', 'Improving remote-first delivery workflow and documentation quality', 'Refining practical AI-assisted development workflows']; title: '云端书签工具',
status: '已上线',
desc: '面向开发者与信息工作者的轻量级自部署工具,数据完全可控。支持 AI 自动标签、内容识别与语义搜索。',
},
{
title: 'ELYND 英语学习工具',
status: '即将上线',
desc: '基于阅读场景的英语学习产品,提供电子书与同步音频。支持点击单词/句子即时解释AI 对话辅助理解。',
},
];
const exploring = isZh const problems = [
? ['复杂权限系统的前端边界设计', '大规模业务表单与状态流管理', '更稳定的跨团队异步协作机制'] {
: ['Frontend boundary design for complex permission systems', 'Scalable state management for large business forms', 'More stable async collaboration practices across teams']; title: '英语学习中的「理解断点」问题',
desc: '背单词脱离语境、阅读原文有障碍、遇到问题缺乏即时反馈——想解决学习过程中的理解断裂。',
},
{
title: 'AI 能力的产品化落地',
desc: '不只是 Demo 层面的展示,而是在真实产品中接入 AI 标签、文本处理、对话能力。',
},
{
title: '工具型 AI 产品的用户体验',
desc: '如何在保持工具简洁性的同时,融入 AI 能力而不造成负担。',
},
];
const shipping = isZh const outputs = [
? ['工程案例库结构重构', '导航与路由转化路径优化', '双语文案统一与信息层级简化'] '云端书签工具 v1.0 上线',
: ['Refactoring project pages into engineering case studies', 'Optimizing navigation and conversion routes', 'Unifying EN/ZH copy and simplifying information hierarchy']; 'ELYND 即将发布',
];
const availability = {
jobs: '远程 / 全栈 / AI 方向',
projects: '外包 / 分包 / 独立项目开发',
preferences: '工具类产品 / AI 能力接入 / Web 应用',
};
const today = new Date().toISOString().split('T')[0];
--- ---
<Layout title={isZh ? '现在' : 'Now'}> <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">{isZh ? '现在' : 'Now'}</h1> <h1 class="text-4xl font-bold tracking-tight sm:text-5xl">现在</h1>
<p class="mt-4 text-lg text-muted-foreground">{isZh ? '我当前的工作重点与近期交付。' : 'What I am focusing on right now and what I am shipping recently.'}</p> <p class="mt-4 text-lg text-muted-foreground">我当前的工作状态与产出。</p>
</section> </section>
<section class="page-content-main mt-10 grid gap-6 md:grid-cols-3"> <!-- 当前主线 -->
<article class="page-surface p-6"> <section class="page-content-main mt-10">
<h2 class="text-lg font-bold">{isZh ? '在做什么' : 'Doing'}</h2> <h2 class="text-xl font-bold">当前主线</h2>
<ul class="mt-4 space-y-2 text-sm text-muted-foreground"> <div class="mt-4 space-y-4">
{doing.map((item) => <li>• {item}</li>)} {focuses.map((item) => (
</ul> <article class="page-surface p-5">
</article> <div class="flex items-center gap-3">
<h3 class="text-base font-bold">{item.title}</h3>
<article class="page-surface p-6"> <span class="rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary">{item.status}</span>
<h2 class="text-lg font-bold">{isZh ? '在研究什么' : 'Exploring'}</h2> </div>
<ul class="mt-4 space-y-2 text-sm text-muted-foreground"> <p class="mt-2 text-sm text-muted-foreground">{item.desc}</p>
{exploring.map((item) => <li>• {item}</li>)} </article>
</ul> ))}
</article> </div>
<article class="page-surface p-6">
<h2 class="text-lg font-bold">{isZh ? '最近交付' : 'Shipping'}</h2>
<ul class="mt-4 space-y-2 text-sm text-muted-foreground">
{shipping.map((item) => <li>• {item}</li>)}
</ul>
</article>
</section> </section>
<section class="page-content-main mt-8 page-surface p-6"> <!-- 当前关注的问题 -->
<section class="page-content-main mt-8">
<h2 class="text-xl font-bold">当前关注的问题</h2>
<div class="mt-4 space-y-3">
{problems.map((item) => (
<article class="rounded-lg border border-border/70 p-4">
<h3 class="text-sm font-semibold">{item.title}</h3>
<p class="mt-1.5 text-sm text-muted-foreground">{item.desc}</p>
</article>
))}
</div>
</section>
<!-- 近期产出 -->
<section class="page-content-main mt-8">
<h2 class="text-xl font-bold">近期产出</h2>
<ul class="mt-4 space-y-2">
{outputs.map((item) => (
<li class="flex items-center gap-2 text-sm">
<span class="h-1.5 w-1.5 rounded-full bg-accent"></span>
{item}
</li>
))}
</ul>
</section>
<!-- 当前状态 -->
<section class="page-content-main mt-8 page-surface p-5">
<h2 class="text-xl font-bold">当前状态</h2>
<div class="mt-4 grid gap-3 text-sm md:grid-cols-3">
<div>
<span class="font-medium">求职:</span>
<span class="text-muted-foreground">{availability.jobs}</span>
</div>
<div>
<span class="font-medium">接单:</span>
<span class="text-muted-foreground">{availability.projects}</span>
</div>
<div>
<span class="font-medium">偏好:</span>
<span class="text-muted-foreground">{availability.preferences}</span>
</div>
</div>
</section>
<section class="page-content-main mt-8 page-surface p-4">
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">
{isZh ? '最后更新2026-03-16' : 'Last updated: 2026-03-16'} 最后更新:{today}
</p> </p>
</section> </section>
</Container> </Container>

View File

@@ -11,7 +11,7 @@ const lang = (Astro.currentLocale as Lang) || defaultLang;
const isZh = lang === 'zh'; const isZh = lang === 'zh';
const pageProjects = projects[lang].map((project) => ({ const pageProjects = projects[lang].map((project) => ({
...project, ...project,
category: project.id === 'elynd' ? 'indie' : 'enterprise', category: project.id === 'elynd' || project.id === 'linky' ? 'indie' : 'enterprise',
})); }));
const enterpriseProjects = pageProjects.filter((project) => project.category === 'enterprise'); const enterpriseProjects = pageProjects.filter((project) => project.category === 'enterprise');
const indieProjects = pageProjects.filter((project) => project.category === 'indie'); const indieProjects = pageProjects.filter((project) => project.category === 'indie');
@@ -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>
@@ -66,13 +66,13 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
</div> </div>
<div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3"> <div class="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
{indieProjects.map((project) => ( {indieProjects.map((project) => (
<article class="page-surface overflow-hidden"> <article class="page-surface h-full overflow-hidden flex flex-col">
<div class="relative aspect-[16/9] overflow-hidden border-b border-border/70 bg-muted/30"> <div class="relative aspect-[16/9] overflow-hidden border-b border-border/70 bg-muted/30">
{project.coverImage ? ( {project.coverImage ? (
<img <img
src={project.coverImage} src={project.coverImage}
alt={project.coverImageAlt || project.title} alt={project.coverImageAlt || project.title}
class="h-full w-full object-cover" class="h-full w-full object-cover transition-transform duration-300 ease-out hover:scale-105"
loading="lazy" loading="lazy"
decoding="async" decoding="async"
/> />
@@ -83,22 +83,59 @@ const indieProjects = pageProjects.filter((project) => project.category === 'ind
<p class="mt-1 text-sm text-foreground/80">{project.systemType}</p> <p class="mt-1 text-sm text-foreground/80">{project.systemType}</p>
</div> </div>
)} )}
</div> {/* Status Badge */}
<div class="p-5"> <div class="absolute right-3 top-3">
<p class="text-xs font-semibold uppercase tracking-wider text-primary/80">{isZh ? '独立项目' : 'Independent'}</p> <span
<h3 class="mt-2 text-lg font-bold">{project.title}</h3> class={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ${
<p class="text-sm text-muted-foreground">{project.context}</p> project.status === 'completed'
<div class="mt-4 flex flex-wrap gap-2"> ? 'bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30'
{project.tech.slice(0, 5).map((tech) => ( : 'bg-amber-500/20 text-amber-600 dark:text-amber-400 border border-amber-500/30'
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span> }`}
))} >
{project.status === 'completed' ? (isZh ? '已完成' : 'Completed') : (isZh ? '开发中' : 'In Progress')}
</span>
</div>
</div>
<div class="flex flex-1 flex-col p-5">
<div class="flex flex-1 flex-col pb-4">
<h3 class="text-lg font-bold">{project.title}</h3>
{project.context && <p class="mt-2 text-sm text-muted-foreground">{project.context}</p>}
<div class="mt-4 flex flex-wrap gap-2">
{project.tech.slice(0, 5).map((tech) => (
<span class="rounded-md border border-border bg-muted/50 px-2.5 py-1 text-xs font-medium">{tech}</span>
))}
</div>
</div>
{/* Action Buttons */}
<div class="mt-auto flex gap-3 border-t border-border/50 pt-5">
{project.links?.demo && (
<a
href={project.links.demo}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 rounded-md bg-primary px-3.5 py-2 text-sm font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4">
<path d="M12.232 4.232a2.5 2.5 0 0 1 3.536 3.536l-1.225 1.224a.75.75 0 0 0 1.061 1.06l1.224-1.224a4 4 0 0 0-5.656-5.656l-3 3a4 4 0 0 0 .225 5.865.75.75 0 0 0 .977-1.139 2.5 2.5 0 0 1-.142-3.635l3-3Z" />
<path d="M11.603 7.963a.75.75 0 0 0-.977 1.139 2.5 2.5 0 0 1 .142 3.635l-3 3a4 4 0 0 0 5.656 5.656l1.224-1.224a.75.75 0 0 0-1.06-1.06l-1.224 1.224a2.5 2.5 0 0 1-3.536-3.536l1.225-1.224a.75.75 0 0 0-1.061-1.06l-1.224 1.224a4 4 0 0 0-5.656-5.656l-3 3a4 4 0 0 0-.225 5.865Z" />
</svg>
{isZh ? '预览' : 'Preview'}
</a>
)}
{project.links?.github && (
<a
href={project.links.github}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3.5 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-muted"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-4 w-4">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
GitHub
</a>
)}
</div> </div>
<p class="mt-4 text-sm font-semibold text-foreground/90">{isZh ? '结果:' : 'Outcome:'} {project.outcomes?.[0] ?? project.impact}</p>
{project.link !== '#' && (
<a href={project.link} target="_blank" rel="noopener noreferrer" class="mt-3 inline-flex text-sm font-semibold text-primary hover:text-primary/80">
{isZh ? '查看项目' : 'Open Project'}
</a>
)}
</div> </div>
</article> </article>
))} ))}

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;