diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..69bc5d7
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,15 @@
+node_modules
+npm-debug.log
+.git
+.gitignore
+README.md
+.env
+.env.local
+.env.*.local
+.next
+.DS_Store
+*.log
+infra
+.claude
+.vscode
+.idea
diff --git a/.forgejo/workflows/build-push.yml b/.forgejo/workflows/build-push.yml
new file mode 100644
index 0000000..5904eeb
--- /dev/null
+++ b/.forgejo/workflows/build-push.yml
@@ -0,0 +1,58 @@
+name: Build & Push Portfolio Image
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - 'app/**'
+ - 'components/**'
+ - 'lib/**'
+ - 'public/**'
+ - 'styles/**'
+ - 'package.json'
+ - 'pnpm-lock.yaml'
+ - 'next.config.js'
+ - 'tsconfig.json'
+ - 'Dockerfile'
+ - '.dockerignore'
+
+jobs:
+ build-push:
+ runs-on: node
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to Forgejo Registry
+ uses: docker/login-action@v3
+ with:
+ registry: forgejo.riotpiao.com
+ username: ${{ secrets.FORGEJO_REGISTRY_USER }}
+ password: ${{ secrets.FORGEJO_REGISTRY_TOKEN }}
+
+ - name: Extract metadata
+ id: meta
+ run: |
+ echo "timestamp=$(date -u +'%Y%m%d-%H%M%S')" >> $GITHUB_OUTPUT
+ echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ push: true
+ tags: |
+ forgejo.riotpiao.com/rock/portfolio:latest
+ forgejo.riotpiao.com/rock/portfolio:${{ steps.meta.outputs.sha_short }}
+ platforms: linux/amd64
+ cache-from: type=registry,ref=forgejo.riotpiao.com/rock/portfolio:buildcache
+ cache-to: type=registry,ref=forgejo.riotpiao.com/rock/portfolio:buildcache,mode=max
+
+ - name: Verify image pushed
+ run: |
+ echo "✓ Image pushed to forgejo.riotpiao.com/rock/portfolio:latest"
+ echo "✓ Image tagged with SHA: ${{ steps.meta.outputs.sha_short }}"
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 0000000..1db92b2
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,341 @@
+# Portfolio Design System
+
+## Color Palette
+
+### Primary Colors
+- **Blue**: `blue-600` (primary actions, links, accents)
+- **Purple**: `purple-600` (gradients, secondary accents)
+- **Green**: `green-500` (live status, success)
+
+### Neutral Colors
+- **Light Mode**:
+ - Background: `white`, `gray-50`
+ - Text Primary: `gray-900`
+ - Text Secondary: `gray-600`, `gray-700`
+ - Borders: `gray-200`, `gray-300`
+
+- **Dark Mode**:
+ - Background: `gray-950`, `gray-900`
+ - Text Primary: `white`
+ - Text Secondary: `gray-400`, `gray-300`
+ - Borders: `gray-800`, `gray-700`
+
+### Status Colors
+- Live: `green-100/900` bg, `green-800/200` text
+- Building: `blue-100/900` bg, `blue-800/200` text
+- Planning: `gray-100/800` bg, `gray-800/200` text
+
+### Timeline Colors (gradients)
+- riotpiao.com: `from-green-500 to-green-600`
+- RBC: `from-red-500 to-red-600`
+- AWS: `from-orange-500 to-orange-600`
+- Titus: `from-cyan-500 to-cyan-600`
+- NAV Canada: `from-blue-500 to-blue-600`
+
+---
+
+## Typography
+
+### Font Sizes
+- Hero Title: `text-4xl md:text-5xl font-bold`
+- Section Title: `text-4xl font-bold`
+- Card Title: `text-2xl font-bold` or `text-xl font-bold`
+- Subtitle: `text-xl font-medium` or `text-lg`
+- Body: `text-sm` or `text-base`
+- Caption: `text-xs`
+- Mono/Code: `font-mono text-xs`
+
+### Font Weights
+- Bold: `font-bold` (titles, emphasis)
+- Semibold: `font-semibold` (subtitles, labels)
+- Medium: `font-medium` (secondary text)
+
+---
+
+## Spacing
+
+### Layout
+- Max Width: `max-w-6xl` (main container), `max-w-4xl` (content), `max-w-3xl` (cards), `max-w-5xl` (hero image)
+- Padding: `px-6` (horizontal), `py-20` or `py-24` (section vertical)
+- Gap: `gap-8` (cards), `gap-6` (nav items), `gap-4` (buttons), `gap-2` (tags)
+
+### Component Spacing
+- Section margin bottom: `mb-16`
+- Card padding: `p-6`
+- Button padding: `px-8 py-3` (large), `px-4 py-2` (medium), `px-3 py-1` (small)
+
+---
+
+## Components
+
+### Buttons
+
+**Primary Button**
+```jsx
+className="bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-lg font-semibold transition"
+```
+
+**Secondary/Outline Button**
+```jsx
+className="border-2 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-800 px-4 py-2 rounded-lg font-semibold transition"
+```
+
+**Language Toggle**
+```jsx
+className="px-3 py-1 text-sm font-semibold rounded border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800"
+```
+
+**Icon Button**
+```jsx
+className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded"
+```
+
+### Cards
+
+**Standard Card**
+```jsx
+className="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 p-6 hover:shadow-lg transition-shadow"
+```
+
+**Project Showcase Card**
+```jsx
+className="bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 overflow-hidden hover:shadow-lg transition-shadow"
+```
+
+### Tags/Pills
+
+**Skill Tag**
+```jsx
+className="text-xs px-3 py-1 rounded-full bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300"
+```
+
+**Status Badge**
+```jsx
+// Live
+className="px-3 py-1 rounded-full text-xs font-semibold bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200"
+// Building
+className="px-3 py-1 rounded-full text-xs font-semibold bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200"
+```
+
+### Dropdowns
+
+**Dropdown Container**
+```jsx
+className="absolute right-0 mt-2 w-64 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg shadow-lg p-4 space-y-3"
+```
+
+### Modals
+
+**Modal Overlay**
+```jsx
+className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4"
+```
+
+**Modal Content**
+```jsx
+className="bg-white dark:bg-gray-900 rounded-lg shadow-2xl max-w-2xl w-full max-h-96 overflow-y-auto"
+```
+
+**Modal Header (Gradient)**
+```jsx
+className="sticky top-0 bg-gradient-to-r from-blue-600 to-purple-600 px-6 py-6 flex justify-between items-center"
+```
+
+---
+
+## Animations (Framer Motion)
+
+### Fade In Up
+```jsx
+initial={{ opacity: 0, y: 20 }}
+animate={{ opacity: 1, y: 0 }}
+transition={{ duration: 0.8, delay: 0.2 }}
+```
+
+### Scale In
+```jsx
+initial={{ scale: 0, opacity: 0 }}
+animate={{ scale: 1, opacity: 1 }}
+transition={{ duration: 0.8, delay: 0.2 }}
+```
+
+### Stagger Children
+```jsx
+variants={{
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: { staggerChildren: 0.15 },
+ },
+}}
+```
+
+### Role Text Rotation
+```jsx
+initial={{ opacity: 0, y: 20, scale: 0.95 }}
+animate={{ opacity: 1, y: 0, scale: 1 }}
+exit={{ opacity: 0, y: -20, scale: 0.95 }}
+transition={{ duration: 0.6, ease: 'easeInOut' }}
+```
+
+### Background Pulse
+```jsx
+animate={{
+ scale: [1, 1.15, 1],
+ opacity: [0.25, 0.4, 0.25],
+}}
+transition={{
+ duration: 6,
+ repeat: Infinity,
+ ease: 'easeInOut',
+}}
+```
+
+---
+
+## Gradients
+
+### Hero Background
+```jsx
+className="bg-gradient-to-br from-blue-50 to-purple-50 dark:from-gray-950 dark:to-gray-900"
+```
+
+### Timeline Line
+```jsx
+className="bg-gradient-to-b from-blue-500 via-purple-500 to-green-500"
+```
+
+### Stat Highlight
+```jsx
+className="bg-gradient-to-r from-blue-50 to-purple-50 dark:from-gray-800 dark:to-gray-800"
+```
+
+### Modal Header
+```jsx
+className="bg-gradient-to-r from-blue-600 to-purple-600"
+```
+
+---
+
+## Z-Index Layers
+
+- `z-0`: Background images
+- `z-10`: Content overlays (avatar, text)
+- `z-20`: Avatar + rotating text group
+- `z-40`: Terminal button
+- `z-50`: Header, Modals
+
+---
+
+## Responsive Breakpoints
+
+- Mobile: Default (no prefix)
+- Tablet/Desktop: `md:` (768px+)
+- Large Desktop: `lg:` (1024px+)
+
+### Common Patterns
+```jsx
+// Hide on mobile, show on desktop
+className="hidden md:flex"
+
+// Show on mobile, hide on desktop
+className="md:hidden"
+
+// Responsive grid
+className="grid grid-cols-1 md:grid-cols-2 gap-8"
+
+// Responsive text
+className="text-4xl md:text-5xl"
+
+// Responsive padding
+className="px-4 md:px-6"
+```
+
+---
+
+## Icons (Lucide React)
+
+### Sizes
+- Navigation: `size={20}`
+- Buttons: `size={16}`
+- Cards: `size={24}`
+- Hero: `size={32}`
+- Close: `size={24}`
+
+### Common Icons
+- `Menu`, `X` - Mobile nav
+- `Moon`, `Sun` - Theme toggle
+- `ExternalLink` - External links
+- `Network`, `Shield`, `Code`, `Sparkles`, `Zap`, `Cpu` - Role icons
+- `GitBranch`, `Database`, `MessageSquare`, `Layers`, `BarChart3` - Feature icons
+
+---
+
+## Internationalization (i18n)
+
+### Structure
+```
+lib/
+ translations.json # All translations
+ LanguageContext.tsx # React context provider
+```
+
+### Usage
+```jsx
+import { useLanguage } from '@/lib/LanguageContext'
+
+const { lang, setLang, t } = useLanguage()
+
+// Access translations
+
{t.hero.title}
+```
+
+### Language Toggle Button
+Shows opposite language:
+- When `en`: Shows `中文`
+- When `zh`: Shows `EN`
+
+---
+
+## File Structure
+
+```
+components/
+ Header.tsx # Navigation, dropdowns, language toggle
+ HeroBlobFlow.tsx # Hero with avatar, rotating roles
+ ExperienceTimeline.tsx # Work history timeline
+ ProjectShowcase.tsx # Project cards
+ InteractiveTerminal.tsx # Cmd+K terminal
+ TypewriterText.tsx # Typewriter effect
+
+lib/
+ TerminalContext.tsx # Terminal open/close state
+ LanguageContext.tsx # Language state + translations
+ translations.json # EN/ZH translations
+
+app/
+ page.tsx # Main page
+ layout.tsx # Root layout with providers
+ globals.css # Tailwind + global styles
+
+public/
+ avatar.jpeg # Profile photo
+ hire-me.png # Dark mode hero background
+ hire-me-light.png # Light mode hero background
+```
+
+---
+
+## Dark Mode
+
+Implemented via Tailwind's `dark:` prefix. Toggle class on `` element.
+
+```jsx
+// Toggle
+document.documentElement.classList.toggle('dark')
+
+// Check
+document.documentElement.classList.contains('dark')
+```
+
+Observe changes via MutationObserver for components that need to react to theme changes (e.g., background image swap).
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..26aadee
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,45 @@
+# Build stage
+FROM node:20-alpine AS builder
+
+WORKDIR /app
+
+# Copy package files
+COPY package.json pnpm-lock.yaml ./
+
+# Install pnpm
+RUN npm install -g pnpm
+
+# Install dependencies
+RUN pnpm install --frozen-lockfile
+
+# Copy source
+COPY . .
+
+# Build Next.js app
+RUN pnpm run build
+
+# Production stage
+FROM node:20-alpine
+
+WORKDIR /app
+
+# Install dumb-init for proper signal handling
+RUN apk add --no-cache dumb-init
+
+# Create non-root user
+RUN addgroup -g 1001 -S nodejs && \
+ adduser -S nextjs -u 1001
+
+# Copy built app from builder
+COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
+COPY --from=builder --chown=nextjs:nodejs /app/public ./public
+
+USER nextjs
+
+EXPOSE 3000
+
+ENV NODE_ENV=production
+
+ENTRYPOINT ["dumb-init", "--"]
+CMD ["node", "server.js"]
diff --git a/app/layout.tsx b/app/layout.tsx
index 6c51a91..625de0f 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,5 +1,7 @@
import './globals.css'
import Header from '@/components/Header'
+import { TerminalProvider } from '@/lib/TerminalContext'
+import { LanguageProvider } from '@/lib/LanguageContext'
export const metadata = {
title: 'Rock Liang — Portfolio & Live Infrastructure',
@@ -14,8 +16,12 @@ export default function RootLayout({
return (
-
- {children}
+
+
+
+ {children}
+
+
);
diff --git a/app/page.tsx b/app/page.tsx
index 4f7d55a..e3b71e6 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,71 +1,35 @@
'use client'
import { motion } from 'framer-motion'
-import { FeatureCard } from '@/components/FeatureCard'
+import { useState } from 'react'
+import { X } from 'lucide-react'
+import { ProjectShowcase } from '@/components/ProjectShowcase'
import { HeroBlobFlow } from '@/components/HeroBlobFlow'
import { InteractiveTerminal } from '@/components/InteractiveTerminal'
import { ExperienceTimeline } from '@/components/ExperienceTimeline'
-import {
- GitBranch,
- Database,
- BarChart3,
- MessageSquare,
- Cpu,
- Layers,
-} from 'lucide-react'
-
-const features = [
- {
- icon: GitBranch,
- title: 'Infrastructure Platform',
- description: 'Multi-region Terraform + Argo CD GitOps on Kubernetes',
- href: '/infrastructure',
- stat: '40% CPU reduction',
- status: 'live' as const,
- },
- {
- icon: Cpu,
- title: 'Distributed Systems',
- description: 'gRPC, Kafka, AWS Step Functions across 57+ regions',
- href: '/systems',
- stat: 'Mission-critical',
- status: 'live' as const,
- },
- {
- icon: MessageSquare,
- title: 'LLM Systems',
- description: 'CPU-bound inference optimization, INT4/INT8 quantization',
- href: '/llm',
- stat: '60% latency cut',
- status: 'live' as const,
- },
- {
- icon: Database,
- title: 'Kafka Cluster',
- description: 'Strimzi KRaft cluster with auto-scaling brokers',
- href: '/kafka',
- stat: '3 brokers',
- status: 'live' as const,
- },
- {
- icon: Layers,
- title: 'Open Source',
- description: 'go-flink: distributed DataLakeHouse in Go',
- href: '/opensource',
- stat: 'Public repo',
- status: 'live' as const,
- },
- {
- icon: BarChart3,
- title: 'Observability',
- description: 'Prometheus, Grafana, Dynatrace — live metrics',
- href: '/infrastructure',
- stat: '99.2% uptime',
- status: 'live' as const,
- },
-]
+import { useLanguage } from '@/lib/LanguageContext'
export default function Home() {
+ const [bioModalOpen, setBioModalOpen] = useState(false)
+ const { t } = useLanguage()
+
+ const projects = t.projects.items.map((item, index) => ({
+ ...item,
+ id: index === 1 ? 'project-rbc' : undefined,
+ status: index === 1
+ ? 'completed' as const
+ : index === 5
+ ? 'building' as const
+ : 'live' as const,
+ media: index === 0
+ ? { type: 'video' as const, url: 'https://www.youtube.com/watch?v=wdJ5DN15jus' }
+ : index === 1
+ ? { type: 'image' as const, url: '/rbc-images.jpeg' }
+ : undefined,
+ videoUrl: index === 5 ? 'https://github.com/rockliang/go-flink' : '#',
+ articleUrl: index === 0 ? 'https://lnkd.in/p/gm2PZkWw' : '#',
+ }))
+
return (
{/* Hero */}
@@ -82,30 +46,22 @@ export default function Home() {
className="text-center mt-16"
>
- Full-Stack Systems Engineer
+ {t.hero.title}
- Infrastructure × Backend × LLM Systems
+ {t.hero.subtitle}
- Building observable, fault-tolerant systems from bare metal to cloud. Optimizing cost (infrastructure) × performance (inference) × reliability (SRE).
+ {t.hero.description}
- 💡 Press Cmd+K to explore via terminal
+ 💡 {t.hero.terminalHint} Cmd+K {t.hero.terminalHintSuffix}
@@ -132,9 +88,9 @@ export default function Home() {
transition={{ duration: 0.6 }}
className="mb-16"
>
- What I Build
+ {t.projects.title}
- Production systems spanning infrastructure, distributed backends, and LLM optimization. Click any domain to explore.
+ {t.projects.subtitle}
@@ -147,14 +103,20 @@ export default function Home() {
visible: {
opacity: 1,
transition: {
- staggerChildren: 0.1,
+ staggerChildren: 0.15,
},
},
}}
- className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
+ className="grid grid-cols-1 gap-8 max-w-3xl mx-auto"
>
- {features.map((feature) => (
-
+ {projects.map((project) => (
+
))}
@@ -162,14 +124,14 @@ export default function Home() {
{/* Footer */}
)
}
diff --git a/app/terraform-drift/page.tsx b/app/terraform-drift/page.tsx
new file mode 100644
index 0000000..256f8b3
--- /dev/null
+++ b/app/terraform-drift/page.tsx
@@ -0,0 +1,51 @@
+'use client'
+
+import Link from 'next/link'
+import { ArrowLeft } from 'lucide-react'
+
+export default function TerraformDriftPage() {
+ return (
+
+
+
+
+ Back to Projects
+
+
+
+ Terraform State Drift Solution
+
+
+
+ Coming soon...
+
+
+ {/* Placeholder sections */}
+
+
+
The Problem
+
Content coming soon...
+
+
+
+
Immediate Fix
+
Content coming soon...
+
+
+
+
Short Term Fix
+
Content coming soon...
+
+
+
+
Long Term Fix
+
Content coming soon...
+
+
+
+
+ )
+}
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000..007750e
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+set -euo pipefail
+
+REGISTRY="${REGISTRY:-forgejo.riotpiao.com}"
+IMAGE="${REGISTRY}/rock/portfolio:latest"
+
+echo "Building image: ${IMAGE}"
+docker buildx build --platform linux/amd64 -t "${IMAGE}" .
+
+echo "Pushing image..."
+docker push "${IMAGE}"
+
+echo "✓ Successfully pushed ${IMAGE}"
diff --git a/components/ExperienceTimeline.tsx b/components/ExperienceTimeline.tsx
index 500806a..cf29081 100644
--- a/components/ExperienceTimeline.tsx
+++ b/components/ExperienceTimeline.tsx
@@ -1,54 +1,38 @@
'use client'
import { motion } from 'framer-motion'
+import { useLanguage } from '@/lib/LanguageContext'
-interface TimelineItem {
- company: string
- role: string
- period: string
- description: string
- impact: string
- skills: string[]
- color: string
-}
+const colors = [
+ 'from-green-500 to-green-600',
+ 'from-red-500 to-red-600',
+ 'from-orange-500 to-orange-600',
+ 'from-cyan-500 to-cyan-600',
+ 'from-blue-500 to-blue-600',
+]
-const timeline: TimelineItem[] = [
- {
- company: 'RBC',
- role: 'Infrastructure Platform Architect',
- period: '2020 — Present',
- description: 'Leading multi-region infrastructure platform on Kubernetes. Building GitOps workflows with Terraform and Argo CD across 4 global regions.',
- impact: '40% CPU reduction, 99.2% uptime',
- skills: ['Kubernetes', 'Terraform', 'Argo CD', 'Go', 'AWS', 'Cilium', 'Prometheus', 'Grafana'],
- color: 'from-blue-500 to-blue-600',
- },
- {
- company: 'AWS',
- role: 'Senior Software Engineer (Step Functions)',
- period: '2019 — 2020',
- description: 'Designed and optimized distributed orchestration platform. Built fault-tolerant task scheduling across 57+ regions serving mission-critical workloads.',
- impact: '8 core components, 57+ regions',
- skills: ['AWS', 'Java', 'gRPC', 'Distributed Systems', 'DynamoDB', 'CloudWatch'],
- color: 'from-orange-500 to-orange-600',
- },
- {
- company: 'Homelab',
- role: 'DevOps / SRE / SDE',
- period: '2021 — Present',
- description: 'Production-grade bare-metal K8s cluster (Talos + Cilium). Built Temporal-based LLM inference pipeline with CPU-bound optimization and quantization.',
- impact: '60% latency cut, 75% memory saved',
- skills: ['LangGraph', 'Temporal', 'LLM Ops', 'PyTorch', 'Kafka', 'Observability'],
- color: 'from-green-500 to-green-600',
- },
- {
- company: 'Open Source',
- role: 'Maintainer — go-flink',
- period: '2022 — Present',
- description: 'Building distributed DataLakeHouse framework in Go. Fault-tolerant task scheduling, streaming semantics, and efficient data processing.',
- impact: 'Public repository, active development',
- skills: ['Go', 'Distributed Systems', 'Data Pipeline', 'Testing', 'Architecture'],
- color: 'from-purple-500 to-purple-600',
- },
+const skills = [
+ ['LangGraph', 'Temporal', 'LLM Ops', 'PyTorch', 'Kafka', 'Observability', 'Go-Flink'],
+ ['Golang', 'Terraform', 'IaC', 'OpenShift', 'Docker', 'Distributed Systems', 'Grafana'],
+ ['AWS', 'Java', 'gRPC', 'Distributed Systems', 'DynamoDB', 'CloudWatch'],
+ ['Machine Learning', 'Golang', 'Anomaly Detection', 'C++', 'CMake', 'Data Security'],
+ ['Django', 'React', 'Agile', 'Web Development', 'Sonar', 'ATC Systems'],
+]
+
+const links = [
+ null,
+ '#project-rbc', // RBC - link to RBC project showcase
+ null,
+ null,
+ 'https://plan.navcanada.ca/account/login/?next=/',
+]
+
+const linkTexts = [
+ '',
+ 'Learn more →',
+ '',
+ '',
+ 'View Product →',
]
const containerVariants = {
@@ -71,6 +55,9 @@ const itemVariants = {
}
export function ExperienceTimeline() {
+ const { t } = useLanguage()
+ const timeline = t.experience.items
+
return (
- Explore Experience
+ {t.experience.title}
- From AWS to RBC. Building systems at scale. Skills acquired along the journey.
+ {t.experience.subtitle}
@@ -105,7 +92,7 @@ export function ExperienceTimeline() {
className="relative pl-8 md:pl-0"
>
{/* Timeline dot */}
-
+
{/* Content card */}
@@ -128,17 +115,24 @@ export function ExperienceTimeline() {
{item.description}
-
- 📊 {item.impact}
-
-
-
diff --git a/components/Header.tsx b/components/Header.tsx
index 6b1e0db..d172eff 100644
--- a/components/Header.tsx
+++ b/components/Header.tsx
@@ -1,12 +1,31 @@
'use client'
import Link from 'next/link'
+import Image from 'next/image'
import { Menu, X, Moon, Sun } from 'lucide-react'
import { useState, useEffect } from 'react'
+import { useTerminal } from '@/lib/TerminalContext'
+import { useLanguage } from '@/lib/LanguageContext'
export default function Header() {
const [open, setOpen] = useState(false)
const [dark, setDark] = useState(false)
+ const [skillsOpen, setSkillsOpen] = useState(false)
+ const [contactOpen, setContactOpen] = useState(false)
+ const [educationOpen, setEducationOpen] = useState(false)
+ const { isOpen: terminalOpen, setIsOpen: setTerminalOpen } = useTerminal()
+ const { lang, setLang, t } = useLanguage()
+ const [copied, setCopied] = useState(null)
+
+ const handleCopy = (text: string, label: string) => {
+ navigator.clipboard.writeText(text)
+ setCopied(label)
+ setTimeout(() => setCopied(null), 2000)
+ }
+
+ const toggleLang = () => {
+ setLang(lang === 'en' ? 'zh' : 'en')
+ }
useEffect(() => {
const isDark = document.documentElement.classList.contains('dark')
@@ -22,42 +41,315 @@ export default function Header() {
-
- RL
-
+
-
Rock Liang
-
Software Engineer Lead
+
{t.header.title}
+
{t.header.subtitle}
- Home
- Cluster
- Infrastructure
- Kafka
- CI/CD
- Agent
-
- {dark ? : }
+ {t.header.nav.home}
+ setTerminalOpen(!terminalOpen)}
+ className="text-sm hover:text-blue-600 dark:hover:text-blue-400"
+ >
+ {t.header.nav.askPoimen}
+
+
setSkillsOpen(!skillsOpen)}
+ className="text-sm hover:text-blue-600 dark:hover:text-blue-400"
+ >
+ {t.header.nav.skills}
+
+ {skillsOpen && (
+
+
{t.header.skillsPlaceholder}
+
+ )}
+
+
+
setContactOpen(!contactOpen)}
+ className="text-sm hover:text-blue-600 dark:hover:text-blue-400"
+ >
+ {t.header.nav.contact}
+
+ {contactOpen && (
+
+
+
+ {t.header.contact.portfolio}
+
+ handleCopy('https://portfolio.riotpiao.com', 'Portfolio')}
+ className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
+ >
+ {copied === 'Portfolio' ? '✓' : t.header.contact.copy}
+
+
+
+
+ {t.header.contact.github}
+
+
handleCopy('https://github.com/rockliang', 'GitHub')}
+ className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
+ >
+ {copied === 'GitHub' ? '✓' : t.header.contact.copy}
+
+
+
+
+ {t.header.contact.privateGithub}
+
+
handleCopy('https://github.com/rockliang', 'PrivateGithub')}
+ className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
+ >
+ {copied === 'PrivateGithub' ? '✓' : t.header.contact.copy}
+
+
+
+
+ {t.header.contact.linkedin}
+
+
handleCopy('https://www.linkedin.com/in/rockliang', 'LinkedIn')}
+ className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
+ >
+ {copied === 'LinkedIn' ? '✓' : t.header.contact.copy}
+
+
+
+
+ {t.header.contact.email}
+
+
handleCopy('locartrock@gmail.com', 'Email')}
+ className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
+ >
+ {copied === 'Email' ? '✓' : t.header.contact.copy}
+
+
+
+ )}
+
+
+
+
{t.header.experience}
+
{t.header.experienceValue}
+
+
+
setEducationOpen(!educationOpen)}
+ className="text-xs cursor-pointer hover:text-blue-600 dark:hover:text-blue-400"
+ >
+ {t.header.education}
+ {t.header.educationValue}
+
+ {educationOpen && (
+
+
+
{t.header.educationDetails.bachelor.school}
+
{t.header.educationDetails.bachelor.degree}
+
{t.header.educationDetails.bachelor.period}
+
+
+
{t.header.educationDetails.master.school}
+
{t.header.educationDetails.master.degree}
+
{t.header.educationDetails.master.period}
+
+
+ )}
+
+
+ {dark ? : }
+
+ {/* Language Toggle */}
+
+ {lang === 'en' ? '中文' : 'EN'}
+
+
-
setOpen(!open)}
- className="md:hidden p-2"
- >
- {open ? : }
-
+
+ {/* Mobile Language Toggle */}
+
+ {lang === 'en' ? '中文' : 'EN'}
+
+ setOpen(!open)}
+ className="p-2"
+ >
+ {open ? : }
+
+
{open && (
-
-
Home
-
Infrastructure
-
Kafka
-
CI/CD
-
Agent
+
+
{t.header.nav.home}
+
setTerminalOpen(!terminalOpen)}
+ className="block py-2 text-sm hover:text-blue-600"
+ >
+ {t.header.nav.askPoimen}
+
+
setSkillsOpen(!skillsOpen)}
+ className="block py-2 text-sm hover:text-blue-600"
+ >
+ {t.header.nav.skills}
+
+ {skillsOpen && (
+
+
{t.header.skillsPlaceholder}
+
+ )}
+
setContactOpen(!contactOpen)}
+ className="block py-2 text-sm hover:text-blue-600"
+ >
+ {t.header.nav.contact}
+
+ {contactOpen && (
+
+
+
+ {t.header.contact.portfolio}
+
+ handleCopy('https://portfolio.riotpiao.com', 'Portfolio')}
+ className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
+ >
+ {copied === 'Portfolio' ? '✓' : t.header.contact.copy}
+
+
+
+
+ {t.header.contact.github}
+
+
handleCopy('https://github.com/rockliang', 'GitHub')}
+ className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
+ >
+ {copied === 'GitHub' ? '✓' : t.header.contact.copy}
+
+
+
+
+ {t.header.contact.privateGithub}
+
+
handleCopy('https://github.com/rockliang', 'PrivateGithub')}
+ className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
+ >
+ {copied === 'PrivateGithub' ? '✓' : t.header.contact.copy}
+
+
+
+
+ {t.header.contact.linkedin}
+
+
handleCopy('https://www.linkedin.com/in/rockliang', 'LinkedIn')}
+ className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
+ >
+ {copied === 'LinkedIn' ? '✓' : t.header.contact.copy}
+
+
+
+
+ {t.header.contact.email}
+
+
handleCopy('locartrock@gmail.com', 'Email')}
+ className="text-xs px-2 py-1 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-blue-100 dark:hover:bg-blue-900"
+ >
+ {copied === 'Email' ? '✓' : t.header.contact.copy}
+
+
+
+ )}
+
+
+
{t.header.experience}
+
{t.header.experienceValue}
+
+
+
setEducationOpen(!educationOpen)}
+ className="text-xs cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 w-full text-left"
+ >
+ {t.header.education}
+ {t.header.educationValue}
+
+ {educationOpen && (
+
+
+
{t.header.educationDetails.bachelor.school}
+
{t.header.educationDetails.bachelor.degree}
+
{t.header.educationDetails.bachelor.period}
+
+
+
{t.header.educationDetails.master.school}
+
{t.header.educationDetails.master.degree}
+
{t.header.educationDetails.master.period}
+
+
+ )}
+
+
)}
diff --git a/components/HeroBlobFlow.tsx b/components/HeroBlobFlow.tsx
index 0a5dfdb..35341d8 100644
--- a/components/HeroBlobFlow.tsx
+++ b/components/HeroBlobFlow.tsx
@@ -2,78 +2,92 @@
import { motion, AnimatePresence } from 'framer-motion'
import { useState, useEffect } from 'react'
-import { Code, Network, Zap, Sparkles, Github } from 'lucide-react'
-import { TypewriterText } from './TypewriterText'
+import Image from 'next/image'
+import { Code, Network, Zap, Sparkles, Shield, Cpu } from 'lucide-react'
+import { useLanguage } from '@/lib/LanguageContext'
-const roles = [
- { text: 'Software Engineer Lead', icon: Code },
- { text: 'Infrastructure Architect', icon: Network },
- { text: 'Distributed Systems Builder', icon: Zap },
- { text: 'LLM Systems Optimizer', icon: Sparkles },
- { text: 'Open Source Developer', icon: Github },
-]
+const roleIcons = [Network, Shield, Code, Sparkles, Zap, Cpu]
export function HeroBlobFlow() {
const [index, setIndex] = useState(0)
+ const [isDark, setIsDark] = useState(false)
+ const { t } = useLanguage()
+ const roles = t.hero.roles
+
+ useEffect(() => {
+ const isDarkMode = document.documentElement.classList.contains('dark')
+ setIsDark(isDarkMode)
+
+ const observer = new MutationObserver(() => {
+ const darkMode = document.documentElement.classList.contains('dark')
+ setIsDark(darkMode)
+ })
+
+ observer.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ['class'],
+ })
+
+ return () => observer.disconnect()
+ }, [])
useEffect(() => {
const interval = setInterval(() => {
setIndex((prev) => (prev + 1) % roles.length)
}, 2000)
return () => clearInterval(interval)
- }, [])
+ }, [roles.length])
return (
-
- {/* Background glow */}
-
-
+ {/* Full hire-me image */}
+
+
-
-
- {/* Avatar center */}
-
-
- RL
-
-
-
- {/* Typewriter text below avatar */}
-
-
- {/* Rotating role text */}
-
-
+
+ {/* Avatar + Rotating text - centered on image */}
+
+ {/* Avatar */}
- {(() => {
- const Icon = roles[index].icon
- return
- })()}
- {roles[index].text}
+
-
+
+ {/* Rotating role text */}
+
+
+
+ {(() => {
+ const Icon = roleIcons[index]
+ return
+ })()}
+ {roles[index]}
+
+
+
+
)
diff --git a/components/InteractiveTerminal.tsx b/components/InteractiveTerminal.tsx
index b6d8c8e..eb0bbb8 100644
--- a/components/InteractiveTerminal.tsx
+++ b/components/InteractiveTerminal.tsx
@@ -2,6 +2,7 @@
import { motion } from 'framer-motion'
import { useEffect, useRef, useState } from 'react'
+import { useTerminal } from '@/lib/TerminalContext'
const commands: Record
= {
help: `Available commands:
@@ -46,10 +47,15 @@ Open Source: go-flink (distributed DataLakeHouse)`,
export function InteractiveTerminal() {
const [input, setInput] = useState('')
const [history, setHistory] = useState<{ cmd: string; output: string }[]>([])
- const [isOpen, setIsOpen] = useState(false)
+ const { isOpen, setIsOpen } = useTerminal()
+ const [isMac, setIsMac] = useState(true)
const terminalRef = useRef(null)
useEffect(() => {
+ // Detect if user is on Mac
+ const isMacOS = /Mac|iPhone|iPad|iPod/.test(navigator.platform)
+ setIsMac(isMacOS)
+
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault()
@@ -59,7 +65,7 @@ export function InteractiveTerminal() {
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
- }, [isOpen])
+ }, [isOpen, setIsOpen])
const handleCommand = (cmd: string) => {
const trimmed = cmd.trim().toLowerCase()
@@ -89,7 +95,7 @@ export function InteractiveTerminal() {
onClick={() => setIsOpen(true)}
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-mono text-sm shadow-lg"
>
- Terminal (Cmd+K)
+ Ask Poimen ({isMac ? 'Cmd' : 'Ctrl'}+K)
) : (
-
ZSH_THEME="robbyrussell"
+
Poimen (Agent Terminal)
setIsOpen(false)}
className="text-gray-400 hover:text-white"
diff --git a/components/ProjectShowcase.tsx b/components/ProjectShowcase.tsx
new file mode 100644
index 0000000..4d55321
--- /dev/null
+++ b/components/ProjectShowcase.tsx
@@ -0,0 +1,200 @@
+'use client'
+
+import { motion } from 'framer-motion'
+import { ExternalLink, MessageSquare } from 'lucide-react'
+import { useTerminal } from '@/lib/TerminalContext'
+
+interface ProjectShowcaseProps {
+ id?: string
+ title: string
+ description: string
+ longDescription?: string
+ stat: string
+ highlight?: string
+ status: 'live' | 'building' | 'planning' | 'completed'
+ media?: {
+ type: 'image' | 'video'
+ url: string
+ }
+ videoUrl?: string
+ articleUrl?: string
+ watchVideoText?: string
+ readArticleText?: string
+ askPoimenText?: string
+ bullets?: string[]
+ deepDive?: {
+ label: string
+ url: string
+ }
+}
+
+export function ProjectShowcase({
+ id,
+ title,
+ description,
+ longDescription,
+ stat,
+ highlight,
+ status,
+ media,
+ videoUrl,
+ articleUrl,
+ watchVideoText = 'Watch Video',
+ readArticleText = 'Read Article',
+ askPoimenText = 'Ask Poimen for technical details',
+ bullets,
+ deepDive,
+}: ProjectShowcaseProps) {
+ const { setIsOpen } = useTerminal()
+
+ const statusColors = {
+ live: 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
+ completed: 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
+ building: 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
+ planning: 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200',
+ }
+
+ return (
+
+ {/* Optional Media (Image or Video) */}
+ {media && (
+
+ {media.type === 'video' ? (
+ (() => {
+ const youtubeMatch = media.url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&]+)/)
+ if (youtubeMatch) {
+ return (
+
VIDEO
+ )
+ }
+ return null
+ })()
+ ) : (
+
+ )}
+
+ )}
+
+ {/* Content */}
+
+
+
+
+ {title}
+
+
+ {description}
+
+ {longDescription && (
+
+ {longDescription}
+
+ )}
+ {bullets && bullets.length > 0 && (
+
+ {bullets.map((bullet, i) => {
+ // Highlight skills: parentheses + keywords
+ const skillKeywords = ['Terraform', 'Golang', 'K8s', 'Slack', 'Grafana', 'IaC', 'Docker', 'OpenShift', 'Temporal', 'Kafka', 'gRPC', 'AWS', 'Java', 'Python', 'Go', 'C++']
+ const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+ const pattern = new RegExp(`(\\([^)]+\\)|${skillKeywords.map(escapeRegex).join('|')})`, 'g')
+ const parts = bullet.split(pattern).filter(Boolean)
+ return (
+
+ •
+
+ {parts.map((part, j) =>
+ (part.startsWith('(') && part.endsWith(')')) || skillKeywords.includes(part) ? (
+
+ {part}
+
+ ) : (
+ {part}
+ )
+ )}
+
+
+ )
+ })}
+
+ )}
+ {deepDive && (
+
+ {deepDive.label}
+
+ )}
+
+
+ {status.charAt(0).toUpperCase() + status.slice(1)}
+
+
+
+ {/* Stat */}
+
+
+ 📊 {stat}
+
+ {highlight && (
+
+ 📊 {highlight}
+
+ )}
+
+
+ {/* CTA Buttons */}
+
+
+ {/* Ask Poimen */}
+
setIsOpen(true)}
+ className="mt-4 w-full text-sm text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 flex items-center justify-center gap-2 transition"
+ >
+
+ {askPoimenText}
+
+
+
+ )
+}
diff --git a/infra/argocd-apps.yaml b/infra/argocd-apps.yaml
index 84cc553..0bacdf1 100644
--- a/infra/argocd-apps.yaml
+++ b/infra/argocd-apps.yaml
@@ -6,7 +6,7 @@ metadata:
spec:
project: default
source:
- repoURL: https://forgejo.riotpiao.homelab.com/rock/riotpiao.git
+ repoURL: https://forgejo.riotpiao.com/rock/riotpiao.git
targetRevision: main
path: infra/portfolio/base
destination:
@@ -27,7 +27,7 @@ metadata:
spec:
project: default
source:
- repoURL: https://forgejo.riotpiao.homelab.com/rock/riotpiao.git
+ repoURL: https://forgejo.riotpiao.com/rock/riotpiao.git
targetRevision: main
path: infra/homarr/base
destination:
@@ -48,7 +48,7 @@ metadata:
spec:
project: default
source:
- repoURL: https://forgejo.riotpiao.homelab.com/rock/riotpiao.git
+ repoURL: https://forgejo.riotpiao.com/rock/riotpiao.git
targetRevision: main
path: infra/auth-infra/base
destination:
diff --git a/infra/auth-infra/base/ingress-protected.yaml b/infra/auth-infra/base/ingress-protected.yaml
index 6db9605..6068b40 100644
--- a/infra/auth-infra/base/ingress-protected.yaml
+++ b/infra/auth-infra/base/ingress-protected.yaml
@@ -5,17 +5,17 @@ metadata:
namespace: longhorn-system
annotations:
nginx.ingress.kubernetes.io/auth-url: http://authentik-server.iam.svc.cluster.local/outpost.goauthentik.io/auth/nginx
- nginx.ingress.kubernetes.io/auth-signin: https://authentik.riotpiao.homelab.com/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri
+ nginx.ingress.kubernetes.io/auth-signin: https://authentik.riotpiao.com/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri
nginx.ingress.kubernetes.io/auth-response-headers: Set-Cookie,X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- - longhorn.riotpiao.homelab.com
+ - longhorn.riotpiao.com
secretName: longhorn-tls
rules:
- - host: longhorn.riotpiao.homelab.com
+ - host: longhorn.riotpiao.com
http:
paths:
- path: /
@@ -33,17 +33,17 @@ metadata:
namespace: dashboard
annotations:
nginx.ingress.kubernetes.io/auth-url: http://authentik-server.iam.svc.cluster.local/outpost.goauthentik.io/auth/nginx
- nginx.ingress.kubernetes.io/auth-signin: https://authentik.riotpiao.homelab.com/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri
+ nginx.ingress.kubernetes.io/auth-signin: https://authentik.riotpiao.com/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri
nginx.ingress.kubernetes.io/auth-response-headers: Set-Cookie,X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- - portainer.riotpiao.homelab.com
+ - portainer.riotpiao.com
secretName: portainer-tls
rules:
- - host: portainer.riotpiao.homelab.com
+ - host: portainer.riotpiao.com
http:
paths:
- path: /
@@ -61,17 +61,17 @@ metadata:
namespace: monitoring
annotations:
nginx.ingress.kubernetes.io/auth-url: http://authentik-server.iam.svc.cluster.local/outpost.goauthentik.io/auth/nginx
- nginx.ingress.kubernetes.io/auth-signin: https://authentik.riotpiao.homelab.com/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri
+ nginx.ingress.kubernetes.io/auth-signin: https://authentik.riotpiao.com/outpost.goauthentik.io/start?rd=$scheme://$http_host$escaped_request_uri
nginx.ingress.kubernetes.io/auth-response-headers: Set-Cookie,X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- - prometheus.riotpiao.homelab.com
+ - prometheus.riotpiao.com
secretName: prometheus-tls
rules:
- - host: prometheus.riotpiao.homelab.com
+ - host: prometheus.riotpiao.com
http:
paths:
- path: /
diff --git a/infra/homarr/base/helmrelease.yaml b/infra/homarr/base/helmrelease.yaml
index 1fc6aa1..d3d4c04 100644
--- a/infra/homarr/base/helmrelease.yaml
+++ b/infra/homarr/base/helmrelease.yaml
@@ -12,8 +12,8 @@ spec:
env:
AUTH_PROVIDERS: "oidc"
AUTH_OIDC_CLIENT_ID: "homarr"
- AUTH_OIDC_ISSUER: "https://authentik.riotpiao.homelab.com/application/o/homarr/"
- AUTH_OIDC_URI: "https://authentik.riotpiao.homelab.com/application/o/homarr/.well-known/openid-configuration"
+ AUTH_OIDC_ISSUER: "https://authentik.riotpiao.com/application/o/homarr/"
+ AUTH_OIDC_URI: "https://authentik.riotpiao.com/application/o/homarr/.well-known/openid-configuration"
AUTH_OIDC_GROUPS_ATTRIBUTE: "groups"
envFrom:
- secretRef:
diff --git a/infra/homarr/base/ingress.yaml b/infra/homarr/base/ingress.yaml
index 810f339..8acfd70 100644
--- a/infra/homarr/base/ingress.yaml
+++ b/infra/homarr/base/ingress.yaml
@@ -9,10 +9,10 @@ spec:
ingressClassName: nginx
tls:
- hosts:
- - homarr.riotpiao.homelab.com
+ - homarr.riotpiao.com
secretName: homarr-tls
rules:
- - host: homarr.riotpiao.homelab.com
+ - host: homarr.riotpiao.com
http:
paths:
- path: /
diff --git a/infra/portfolio/base/deployment.yaml b/infra/portfolio/base/deployment.yaml
index 96e1cd1..1c3e64d 100644
--- a/infra/portfolio/base/deployment.yaml
+++ b/infra/portfolio/base/deployment.yaml
@@ -18,7 +18,7 @@ spec:
spec:
containers:
- name: portfolio
- image: forgejo.riotpiao.homelab.com/rock/portfolio:latest
+ image: forgejo.riotpiao.com/rock/portfolio:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
diff --git a/infra/portfolio/base/ingress.yaml b/infra/portfolio/base/ingress.yaml
index 1420c1b..8d0c35f 100644
--- a/infra/portfolio/base/ingress.yaml
+++ b/infra/portfolio/base/ingress.yaml
@@ -3,16 +3,13 @@ kind: Ingress
metadata:
name: portfolio
namespace: portfolio
- annotations:
- cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
- ingressClassName: nginx
tls:
- hosts:
- - portfolio.riotpiao.homelab.com
+ - riotpiao.com
secretName: portfolio-tls
rules:
- - host: portfolio.riotpiao.homelab.com
+ - host: riotpiao.com
http:
paths:
- path: /
diff --git a/lib/LanguageContext.tsx b/lib/LanguageContext.tsx
new file mode 100644
index 0000000..ff3199e
--- /dev/null
+++ b/lib/LanguageContext.tsx
@@ -0,0 +1,34 @@
+'use client'
+
+import { createContext, useContext, useState, ReactNode } from 'react'
+import translations from './translations.json'
+
+type Language = 'en' | 'zh'
+
+interface LanguageContextType {
+ lang: Language
+ setLang: (lang: Language) => void
+ t: typeof translations.en
+}
+
+const LanguageContext = createContext(undefined)
+
+export function LanguageProvider({ children }: { children: ReactNode }) {
+ const [lang, setLang] = useState('en')
+
+ const t = lang === 'zh' ? translations.zh : translations.en
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useLanguage() {
+ const context = useContext(LanguageContext)
+ if (!context) {
+ throw new Error('useLanguage must be used within LanguageProvider')
+ }
+ return context
+}
diff --git a/lib/TerminalContext.tsx b/lib/TerminalContext.tsx
new file mode 100644
index 0000000..7e056de
--- /dev/null
+++ b/lib/TerminalContext.tsx
@@ -0,0 +1,28 @@
+'use client'
+
+import { createContext, useContext, useState, ReactNode } from 'react'
+
+interface TerminalContextType {
+ isOpen: boolean
+ setIsOpen: (open: boolean) => void
+}
+
+const TerminalContext = createContext(undefined)
+
+export function TerminalProvider({ children }: { children: ReactNode }) {
+ const [isOpen, setIsOpen] = useState(false)
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useTerminal() {
+ const context = useContext(TerminalContext)
+ if (!context) {
+ throw new Error('useTerminal must be used within TerminalProvider')
+ }
+ return context
+}
diff --git a/lib/translations.json b/lib/translations.json
new file mode 100644
index 0000000..bc19a09
--- /dev/null
+++ b/lib/translations.json
@@ -0,0 +1,354 @@
+{
+ "en": {
+ "header": {
+ "title": "Rock Liang",
+ "subtitle": "Senior Software Engineer",
+ "nav": {
+ "home": "Home",
+ "askPoimen": "AskPoimen",
+ "skills": "Skills",
+ "contact": "Contact"
+ },
+ "experience": "Experience",
+ "experienceValue": "6+ YoE",
+ "education": "Education",
+ "educationValue": "M.Sc. UOttawa",
+ "educationDetails": {
+ "bachelor": {
+ "school": "University of Ottawa",
+ "degree": "Bachelor's Degree, Computer Science",
+ "period": "2016 – 2019"
+ },
+ "master": {
+ "school": "University of Ottawa",
+ "degree": "Master's degree, Computer Science",
+ "period": "Sep 2019 – Nov 2021"
+ }
+ },
+ "contact": {
+ "portfolio": "portfolio.riotpiao.com",
+ "github": "GitHub",
+ "privateGithub": "Private Github",
+ "linkedin": "LinkedIn",
+ "email": "Email",
+ "copy": "Copy"
+ },
+ "skillsPlaceholder": "Skills will appear here"
+ },
+ "hero": {
+ "title": "Senior Software Engineer",
+ "subtitle": "Infrastructure × Backend × LLM Systems",
+ "description": "Building observable, scalable, fault-tolerant systems for mass audience.",
+ "exploreMore": "Explore more",
+ "terminalHint": "Press",
+ "terminalHintSuffix": "to explore via terminal",
+ "roles": [
+ "Infrastructure",
+ "SRE",
+ "SDE",
+ "Agentic Engineer",
+ "Distributed Systems",
+ "System/Platform Engineer"
+ ]
+ },
+ "bio": {
+ "title": "About Me",
+ "intro": "Senior Software Engineer with 6+ years of experience building observable, scalable, and fault-tolerant systems for mass audiences.",
+ "expertise": {
+ "title": "Technical Expertise",
+ "content": "Infrastructure automation (Terraform, Kubernetes, ArgoCD) • Distributed systems (gRPC, Kafka, AWS Step Functions) • LLM inference optimization (INT4/INT8 quantization) • Backend systems (Go, Java, Python, C++)"
+ },
+ "highlights": {
+ "title": "Career Highlights",
+ "items": [
+ "Launched AWS Distributed-Map service across 57+ regions",
+ "Built multi-region infrastructure at RBC (40% cost reduction)",
+ "Architected production homelab: Kubernetes, Kafka, LLM inference",
+ "Open source: go-flink (distributed DataLakeHouse)"
+ ]
+ },
+ "educationTitle": "Education",
+ "educationContent": "M.Sc. Computer Science, University of Ottawa (2019–2021)\nB.Sc. Computer Science, University of Ottawa (2016–2019)",
+ "currentFocus": "Currently focused on infrastructure automation, LLM systems optimization, and building the next generation of distributed systems."
+ },
+ "projects": {
+ "title": "What have i Built and Building",
+ "subtitle": "Demonstrate my work",
+ "watchVideo": "Watch Video",
+ "readArticle": "Read Article",
+ "askPoimen": "Ask Poimen for technical details",
+ "items": [
+ {
+ "title": "AWS Distributed-Map",
+ "description": "Launched distributed task orchestration service for mission-critical workloads.",
+ "longDescription": "Optimized execution across 57+ regions with fault-tolerant scheduling and auto-scaling.",
+ "stat": "Production launch, 57+ regions"
+ },
+ {
+ "title": "RBC: Multi-Cloud Platform",
+ "description": "Unified infrastructure platform consolidating public cloud and on-prem.",
+ "longDescription": "Terraform automation with Temporal orchestration. 99.2% automated provisioning, notification-driven operator fallback.",
+ "stat": "200+ microservices, 4 regions, 2hr→20min deploy",
+ "highlight": "Standardized IaC patterns across 12 teams—cut provisioning toil, 3x integration velocity.",
+ "bullets": [
+ "Integrated Terraform Cloud to centralize IaC workflows—reduced onboarding time for 12 teams",
+ "Built K8s CronJob to detect and reconcile Terraform state drift automatically (Golang)",
+ "Designed Slack notification service with Golang workers—operators resolve apply failures in <5min",
+ "Led requirement gathering across 4 platform teams—disambiguated specs, unblocked 3 stalled projects",
+ "Translated technical decisions for non-technical stakeholders—secured buy-in for platform migration"
+ ],
+ "deepDive": {
+ "label": "Deep Dive: Terraform State Drift Solution →",
+ "url": "/terraform-drift"
+ }
+ },
+ {
+ "title": "Homelab: Kubernetes Cluster",
+ "description": "Bare-metal Kubernetes cluster running production workloads.",
+ "longDescription": "Talos OS with 3-node control plane. Cilium eBPF CNI, Longhorn storage, ArgoCD GitOps.",
+ "stat": "3-node cluster, 99.2% uptime"
+ },
+ {
+ "title": "Homelab: LLM Inference",
+ "description": "Production LLM inference pipeline with CPU optimization.",
+ "longDescription": "Temporal-based orchestration with INT4/INT8 quantization. Achieved 60% latency reduction.",
+ "stat": "60% latency cut, real-time inference"
+ },
+ {
+ "title": "Homelab: Kafka Cluster",
+ "description": "High-throughput event streaming with Strimzi KRaft.",
+ "longDescription": "3-broker KRaft cluster with auto-scaling, persistent storage, and monitoring.",
+ "stat": "3 brokers, 1K+ msgs/sec"
+ },
+ {
+ "title": "Open Source: go-flink",
+ "description": "Distributed DataLakeHouse framework written in Go.",
+ "longDescription": "Fault-tolerant data pipelines with streaming semantics and efficient processing.",
+ "stat": "Public repository, active"
+ }
+ ]
+ },
+ "experience": {
+ "title": "Explore Experience",
+ "subtitle": "Feel free to Ask Poimen more abt it",
+ "items": [
+ {
+ "company": "riotpiao.com",
+ "role": "DevOps / SRE / SDE",
+ "period": "May 2026 — Present",
+ "description": "Built and maintain production-grade homelab on bare-metal Kubernetes (3-node Talos cluster). Architected Temporal-based LLM inference pipelines with 60% latency reduction. Full GitOps automation with ArgoCD, Terraform, and custom operators."
+ },
+ {
+ "company": "RBC",
+ "role": "Lead Software Engineer",
+ "period": "Nov 2024 — May 2026",
+ "description": "Deployments were manual and slow—teams blocked 2+ hours waiting. Chose Terraform for idempotent drift handling, Temporal for multi-cloud orchestration with built-in retry. Cut deploy time to 20min, consolidated public cloud and on-prem into single platform with 99.2% automation and notification-driven operator fallback."
+ },
+ {
+ "company": "AWS",
+ "role": "Senior Software Engineer (Step Functions)",
+ "period": "2022 — 2024",
+ "description": "Core contributor to AWS Step Functions, scaling to 57+ regions. Designed and implemented 8 critical components for distributed state machine execution. Achieved sub-100ms P99 latency for high-throughput workflow orchestration."
+ },
+ {
+ "company": "Titus",
+ "role": "ML Software Engineer Intern",
+ "period": "May — Aug 2019",
+ "description": "Built Personal Data Detection pipeline with 97.8% accuracy using transformer models. Developed Golang connector reducing p99 latency by 28%. Created SmartRegex system accelerating model deployment by 5x."
+ },
+ {
+ "company": "NAV Canada",
+ "role": "Summer Student",
+ "period": "May — Aug 2018",
+ "description": "Developed CFPS Flight Planning application using Django and React. Implemented NOTAMJ polling system and integrated Sonar code coverage. Built FWGS weather briefing interface for pilots."
+ }
+ ]
+ },
+ "terminal": {
+ "title": "Poimen (Agent Terminal)",
+ "button": "Ask Poimen",
+ "helpText": "type 'help' for commands"
+ },
+ "footer": {
+ "copyright": "© 2025 Rock Liang. Deployed on homelab Kubernetes cluster (3-node Talos).",
+ "github": "GitHub",
+ "email": "Email"
+ }
+ },
+ "zh": {
+ "header": {
+ "title": "梁伟哲",
+ "subtitle": "高级软件工程师",
+ "nav": {
+ "home": "首页",
+ "askPoimen": "问Poimen",
+ "skills": "技能",
+ "contact": "联系"
+ },
+ "experience": "经验",
+ "experienceValue": "6年+",
+ "education": "学历",
+ "educationValue": "渥太华大学硕士",
+ "educationDetails": {
+ "bachelor": {
+ "school": "渥太华大学",
+ "degree": "计算机科学学士",
+ "period": "2016 – 2019"
+ },
+ "master": {
+ "school": "渥太华大学",
+ "degree": "计算机科学硕士",
+ "period": "2019年9月 – 2021年11月"
+ }
+ },
+ "contact": {
+ "portfolio": "portfolio.riotpiao.com",
+ "github": "GitHub",
+ "privateGithub": "私人GitHub",
+ "linkedin": "领英",
+ "email": "邮箱",
+ "copy": "复制"
+ },
+ "skillsPlaceholder": "技能将显示在这里"
+ },
+ "hero": {
+ "title": "高级软件工程师",
+ "subtitle": "基础设施 × 后端 × LLM系统",
+ "description": "为大规模用户构建可观测、可扩展、容错的系统。",
+ "exploreMore": "了解更多",
+ "terminalHint": "按",
+ "terminalHintSuffix": "通过终端探索",
+ "roles": [
+ "基础设施",
+ "SRE",
+ "软件开发",
+ "AI代理工程师",
+ "分布式系统",
+ "系统/平台工程师"
+ ]
+ },
+ "bio": {
+ "title": "关于我",
+ "intro": "拥有6年以上经验的高级软件工程师,专注于为大规模用户构建可观测、可扩展、容错的系统。",
+ "expertise": {
+ "title": "技术专长",
+ "content": "基础设施自动化 (Terraform, Kubernetes, ArgoCD) • 分布式系统 (gRPC, Kafka, AWS Step Functions) • LLM推理优化 (INT4/INT8量化) • 后端系统 (Go, Java, Python, C++)"
+ },
+ "highlights": {
+ "title": "职业亮点",
+ "items": [
+ "在57+区域发布AWS Distributed-Map服务",
+ "在RBC构建多区域基础设施(成本降低40%)",
+ "架构生产级家庭实验室:Kubernetes、Kafka、LLM推理",
+ "开源项目:go-flink(分布式数据湖仓库)"
+ ]
+ },
+ "educationTitle": "教育背景",
+ "educationContent": "渥太华大学计算机科学硕士 (2019–2021)\n渥太华大学计算机科学学士 (2016–2019)",
+ "currentFocus": "目前专注于基础设施自动化、LLM系统优化,以及构建下一代分布式系统。"
+ },
+ "projects": {
+ "title": "我构建的项目",
+ "subtitle": "展示我的作品",
+ "watchVideo": "观看视频",
+ "readArticle": "阅读文章",
+ "askPoimen": "问Poimen了解技术细节",
+ "items": [
+ {
+ "title": "AWS Distributed-Map",
+ "description": "发布面向关键任务的分布式任务编排服务。",
+ "longDescription": "在57+区域优化执行,具有容错调度和自动扩展功能。",
+ "stat": "生产发布,57+区域"
+ },
+ {
+ "title": "RBC: 多云平台",
+ "description": "统一基础设施平台,整合公有云和本地部署。",
+ "longDescription": "Terraform自动化配合Temporal编排。99.2%自动化配置,通知驱动的运维人员兜底。",
+ "stat": "200+微服务,4个区域,部署2小时→20分钟",
+ "highlight": "标准化12个团队的IaC模式——减少配置负担,集成速度提升3倍。",
+ "bullets": [
+ "集成Terraform Cloud实现IaC工作流集中化——12个团队onboarding时间缩短",
+ "构建K8s CronJob自动检测和修复Terraform状态漂移(Golang)",
+ "设计Slack通知服务 + Golang worker——运维人员<5分钟解决apply失败",
+ "主导4个平台团队需求收集——澄清规格,解锁3个停滞项目",
+ "向非技术stakeholder翻译技术决策——获得平台迁移支持"
+ ],
+ "deepDive": {
+ "label": "深入了解:Terraform状态漂移解决方案 →",
+ "url": "/terraform-drift"
+ }
+ },
+ {
+ "title": "家庭实验室:Kubernetes集群",
+ "description": "运行生产工作负载的裸机Kubernetes集群。",
+ "longDescription": "Talos OS 3节点控制平面,Cilium eBPF CNI,Longhorn存储,ArgoCD GitOps。",
+ "stat": "3节点集群,99.2%可用性"
+ },
+ {
+ "title": "家庭实验室:LLM推理",
+ "description": "具有CPU优化的生产LLM推理管道。",
+ "longDescription": "基于Temporal的编排,INT4/INT8量化,延迟降低60%。",
+ "stat": "延迟降低60%,实时推理"
+ },
+ {
+ "title": "家庭实验室:Kafka集群",
+ "description": "使用Strimzi KRaft的高吞吐量事件流。",
+ "longDescription": "3代理KRaft集群,具有自动扩展、持久存储和监控。",
+ "stat": "3代理,1K+消息/秒"
+ },
+ {
+ "title": "开源:go-flink",
+ "description": "用Go编写的分布式数据湖仓库框架。",
+ "longDescription": "具有流语义和高效处理的容错数据管道。",
+ "stat": "公开仓库,活跃中"
+ }
+ ]
+ },
+ "experience": {
+ "title": "工作经历",
+ "subtitle": "欢迎向Poimen了解更多",
+ "items": [
+ {
+ "company": "riotpiao.com",
+ "role": "DevOps / SRE / 软件开发",
+ "period": "2026年5月 — 至今",
+ "description": "在裸机Kubernetes(3节点Talos集群)上构建和维护生产级家庭实验室。架构基于Temporal的LLM推理管道,延迟降低60%。使用ArgoCD、Terraform和自定义操作器实现完整GitOps自动化。"
+ },
+ {
+ "company": "RBC",
+ "role": "技术主管",
+ "period": "2024年11月 — 2026年5月",
+ "description": "部署流程手动且缓慢——团队等待2+小时。选择Terraform处理幂等漂移,Temporal用于多云编排和内置重试。将部署时间缩短至20分钟,整合公有云和本地部署为统一平台,99.2%自动化,通知驱动的运维人员兜底机制。"
+ },
+ {
+ "company": "AWS",
+ "role": "高级软件工程师 (Step Functions)",
+ "period": "2022 — 2024",
+ "description": "AWS Step Functions核心贡献者,扩展至57+区域。设计实现8个关键组件用于分布式状态机执行。高吞吐量工作流编排实现P99延迟低于100ms。"
+ },
+ {
+ "company": "Titus",
+ "role": "机器学习软件工程师实习",
+ "period": "2019年5月 — 8月",
+ "description": "使用transformer模型构建准确率97.8%的个人数据检测管道。开发Golang连接器,p99延迟降低28%。创建SmartRegex系统,模型部署速度提升5倍。"
+ },
+ {
+ "company": "NAV Canada",
+ "role": "暑期实习生",
+ "period": "2018年5月 — 8月",
+ "description": "使用Django和React开发CFPS航班计划应用。实现NOTAMJ轮询系统并集成Sonar代码覆盖。为飞行员构建FWGS天气简报界面。"
+ }
+ ]
+ },
+ "terminal": {
+ "title": "Poimen(代理终端)",
+ "button": "问Poimen",
+ "helpText": "输入 'help' 查看命令"
+ },
+ "footer": {
+ "copyright": "© 2025 梁伟哲。部署于家庭实验室Kubernetes集群(3节点Talos)。",
+ "github": "GitHub",
+ "email": "邮箱"
+ }
+ }
+}
diff --git a/public/avatar.jpeg b/public/avatar.jpeg
new file mode 100644
index 0000000..9bdf078
Binary files /dev/null and b/public/avatar.jpeg differ
diff --git a/public/hire-me-light.png b/public/hire-me-light.png
new file mode 100644
index 0000000..adb81a3
Binary files /dev/null and b/public/hire-me-light.png differ
diff --git a/public/hire-me.png b/public/hire-me.png
new file mode 100644
index 0000000..c72aede
Binary files /dev/null and b/public/hire-me.png differ
diff --git a/public/rbc-images.jpeg b/public/rbc-images.jpeg
new file mode 100644
index 0000000..0ec105d
Binary files /dev/null and b/public/rbc-images.jpeg differ