vivekmahendra.com

Personal website with markdown-based content and interactive visualizations

Date:

Technologies:

typescriptreactreact-routertailwindcssmdxvisxd3Cloud Run

Personal Portfolio Website

Note: This article was AI-generated based on the project directory structure and codebase to provide comprehensive technical documentation.

This is my personal portfolio website built with modern web technologies, focusing on performance, maintainability, and clean design. The site serves as both a showcase of my work and a platform for sharing ideas through a built-in blog.

Architecture Overview

The application is built on React Router v7 (the evolution of Remix), providing full-stack capabilities with file-based routing and server-side rendering. The content management system uses MDX files for blog posts and project descriptions, allowing for rich content with embedded React components.

The design philosophy emphasizes minimalism and performance:

  • Clean, professional typography using system fonts
  • Tailwind CSS for consistent, utility-first styling
  • Subtle animations that enhance UX without being distracting
  • Mobile-first responsive design
  • Fast build times and optimal loading performance

Key Technical Features

Content-First Architecture

All blog posts and project descriptions are written in MDX, stored in the /content directory. This approach provides the best of both worlds: the simplicity of Markdown with the power of React components for interactive elements like charts and code snippets.

Component Organization

The codebase follows a clear component hierarchy:

  • /layouts - Reusable layout components
  • /home - Home page specific sections
  • /ui - General UI components
  • /mdx - Components designed for use within MDX content
  • /react-bits - Third-party animated components

Performance Optimizations

  • Vite for fast development and optimized production builds
  • Dynamic imports for code splitting
  • Responsive images and lazy loading
  • Minimal JavaScript bundle size

Core Implementation Examples:

Layout.tsx
1// Main app layout with mobile-responsive navigation
2export function Layout({ children }: { children: React.ReactNode }) {
3const location = useLocation();
4const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
5 
6return (
7 
8<div className="min-h-screen bg-white text-black flex flex-col">
9<header className="border-b border-gray-200">
10{/* Desktop Navigation */}
11<nav className="hidden md:flex space-x-8">
12{navigation.map((item) => (
13<Link
14to={item.href}
15className={`transition-colors ${
16location.pathname === item.href
17? "text-black"
18: "text-gray-500 hover:text-black"
19}`} >
20{item.name}
21</Link>
22))}
23</nav>
24 
25 {/* Animated mobile menu with staggered transitions */}
26 <div className={`md:hidden overflow-hidden transition-all duration-300 ${
27 mobileMenuOpen ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
28 }`}>
29 {/* Menu items with cascading animation delays */}
30 </div>
31 </header>
32 
33 <main className="flex-grow">{children}</main>
34 <footer className="border-t border-gray-200 mt-auto">
35 Made in Phoenix 🌵
36 </footer>
37 </div>
38 
39);
40}

The layout component demonstrates several key patterns: a sticky header with conditional styling based on the current route, an animated mobile menu with staggered transitions, and a flexbox layout that ensures the footer stays at the bottom. The mobile menu uses CSS transforms and opacity for smooth animations.

content.ts
1// Dynamic content loading with TypeScript support
2export function getIdeas() {
3const ideas = import.meta.glob('../content/ideas/*.mdx', { eager: true });
4 
5return Object.entries(ideas)
6 .map(([path, module]) => ({
7 slug: path.replace('../content/ideas/', '').replace('.mdx', ''),
8 title: module.frontmatter.title,
9 date: module.frontmatter.date,
10 excerpt: module.frontmatter.excerpt,
11 }))
12 .sort((a, b) => new Date(b.date) - new Date(a.date));
13}
14 
15// Route loader for individual posts
16export async function loader({ params }) {
17const { slug } = params;
18const content = await import(`../content/ideas/${slug}.mdx`);
19 
20return {
21content: content.default,
22frontmatter: content.frontmatter
23};
24}

This content management system uses Vite's import.meta.glob to automatically discover and import MDX files at build time. The frontmatter is extracted for metadata like titles and dates, while the content is dynamically loaded in route loaders. This approach eliminates the need for a traditional CMS while maintaining type safety.

vite.config.ts
1// Modern build setup with MDX and ngrok support
2export default defineConfig({
3plugins: [
4 tailwindcss(),
5 mdx({
6 jsxImportSource: "react",
7 providerImportSource: "@mdx-js/react",
8 remarkPlugins: [
9 remarkFrontmatter,
10 [remarkMdxFrontmatter, { name: "frontmatter" }]
11 ],
12 }),
13 reactRouter(),
14 tsconfigPaths()
15],
16server: {
17 allowedHosts: [
18 "localhost",
19 ".ngrok-free.app", // Support for ngrok tunneling
20 ".ngrok.io"
21 ]
22}
23});

The Vite configuration showcases the modern build pipeline. The MDX plugin handles frontmatter extraction and JSX compilation, while the ngrok allowedHosts configuration enables easy local development with external access for testing on mobile devices.

HomePage.tsx
1// LetterGlitch background with DecryptedText header
2import { LetterGlitch, DecryptedText } from "../components/react-bits";
3 
4export default function Home() {
5return (
6 <div className="bg-white text-black overflow-x-hidden">
7 {/* Full-width animated background */}
8 <div className="relative overflow-hidden">
9 <div className="absolute inset-0 z-0">
10 <LetterGlitch
11 glitchColors={["#f0f0f0", "#f4f4f4", "#eeeeee"]}
12 glitchSpeed={500}
13 smooth={true}
14 outerVignette={false}
15 centerVignette={false}
16 />
17 </div>
18 
19 <div className="container mx-auto px-6 relative z-10">
20 <h1 className="text-5xl sm:text-6xl lg:text-7xl font-extralight">
21 <DecryptedText
22 text="Hello, I'm Vivek"
23 animateOn="view"
24 revealDirection="start"
25 speed={100}
26 />
27 </h1>
28 </div>
29 </div>
30 </div>
31 
32);
33}

The home page demonstrates layered animations using React Bits components. The LetterGlitch provides a subtle animated background that doesn't interfere with readability, while DecryptedText creates an engaging reveal effect for the main heading. The overflow-x-hidden class prevents horizontal scrolling issues on mobile.

PageHeader.tsx
1// Simple, consistent page headers across all routes
2interface PageHeaderProps {
3title: string;
4}
5 
6export function PageHeader({ title }: PageHeaderProps) {
7return (
8 <div className="mb-16">
9 <h1 className="text-4xl font-light">{title}</h1>
10 </div>
11);
12}
13 
14// Usage in route components
15import { PageHeader } from "../components/layouts";
16 
17export default function About() {
18return (
19 <div className="bg-white text-black">
20 <div className="container mx-auto px-6 py-20">
21 <PageHeader title="About" />
22 {/* Page content */}
23 </div>
24 </div>
25);
26}

This demonstrates the DRY principle applied to UI components. Rather than repeating header markup across pages, a single reusable component ensures consistency. The interface provides type safety while keeping the API minimal. This pattern makes global design changes simple to implement.

LinkAnimations.tsx
1// Consistent hover animations across all arrow links
2<Link
3to="/projects"
4className="group px-4 py-1 text-sm border border-gray-300 rounded-full hover:bg-gray-50 transition-colors"
5>
6View all <span className="inline-block transition-transform group-hover:translate-x-1">→</span>
7</Link>
8 
9<Link
10to={`/ideas/${post.slug}`}
11className="group text-sm text-gray-600 hover:text-black transition-colors inline-block"
12>
13View Article <span className="inline-block transition-transform group-hover:translate-x-1">→</span>
14</Link>
15 
16// Applied consistently across home sections, project cards, and article links

These micro-interactions add polish without overwhelming the user. The arrow translate animation provides immediate feedback on hover, while the group/group-hover pattern ensures the animation only affects the arrow, not the entire link. This creates a cohesive experience across all navigation elements.

Newsletter.tsx
1// Adding celebration effects with canvas-confetti
2import confetti from "canvas-confetti";
3 
4export function Newsletter() {
5const [showSuccessModal, setShowSuccessModal] = useState(false);
6 
7// Trigger confetti when success modal opens
8useEffect(() => {
9if (showSuccessModal) {
10confetti({
11particleCount: 50,
12spread: 60,
13origin: { y: 0.6 },
14disableForReducedMotion: true,
15colors: ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FECA57']
16});
17}
18}, [showSuccessModal]);
19 
20return (
21 
22<Modal
23isOpen={showSuccessModal}
24onClose={() => setShowSuccessModal(false)}
25title="Successfully Subscribed! 🎉"
26>
27{/* Modal content */}
28</Modal>
29); }

The newsletter subscription includes a delightful confetti effect using canvas-confetti. When users successfully subscribe, the celebration animation enhances the positive feedback while respecting accessibility preferences through the disableForReducedMotion option. The confetti colors are carefully chosen to match the festive party emoji aesthetic, creating a cohesive celebratory experience.

Development Experience

The development setup prioritizes fast feedback loops and maintainability:

  • Hot module replacement for instant updates during development
  • TypeScript throughout for type safety and better IDE support
  • Consistent code organization making features easy to locate and modify
  • Component-driven development enabling rapid UI iteration

Performance Considerations

The site achieves excellent Core Web Vitals through several optimizations:

  • Server-side rendering for fast initial page loads
  • Minimal JavaScript bundle sizes through code splitting
  • Optimized images and lazy loading
  • Efficient CSS delivery via Tailwind's purging

This architecture strikes a balance between developer experience and user performance, making it both enjoyable to work with and fast for end users.

Deployment with Google Cloud Run

The application is deployed on Google Cloud Run, Google's fully managed serverless container platform. Cloud Run provides the perfect balance of simplicity, scalability, and cost-effectiveness for modern web applications.

Why Google Cloud Run?

Cloud Run offers several compelling advantages for this portfolio site:

  • Serverless Architecture: No servers to manage or maintain - Cloud Run handles all infrastructure automatically
  • Scale to Zero: When there's no traffic, the service scales down to zero instances, meaning you only pay for actual usage
  • Automatic Scaling: Seamlessly handles traffic spikes by automatically scaling up to handle thousands of concurrent requests
  • Container-Based: Deploy any containerized application without vendor lock-in
  • Built-in HTTPS: Automatic SSL certificates and secure connections out of the box
  • Global Load Balancing: Requests are automatically routed to the nearest region for optimal performance

The Deployment Architecture

The deployment pipeline uses a multi-stage approach for optimal efficiency:

Dockerfile
1# Build stage - compile and build the application
2FROM node:20-alpine AS builder
3WORKDIR /app
4 
5# Install dependencies with legacy peer deps for React 19 compatibility
6 
7COPY package*.json ./
8RUN npm ci --legacy-peer-deps
9 
10# Build the application
11 
12COPY . .
13RUN npm run build
14 
15# Production stage - lightweight runtime image
16 
17FROM node:20-alpine
18 
19# Add signal handling for graceful shutdowns
20 
21RUN apk add --no-cache dumb-init
22 
23# Create non-root user for security
24 
25RUN addgroup -g 1001 -S nodejs && \
26adduser -S nodejs -u 1001
27 
28# Copy only production dependencies and built files
29 
30WORKDIR /app
31COPY package*.json ./
32RUN npm ci --only=production --legacy-peer-deps
33 
34# Copy built application from builder stage
35 
36COPY --from=builder --chown=nodejs:nodejs /app/build ./build
37 
38# Run as non-root user
39 
40USER nodejs
41 
42# Cloud Run automatically sets the PORT environment variable
43 
44ENV PORT=3000
45 
46# Start with proper signal handling
47 
48ENTRYPOINT ["dumb-init", "--"]
49CMD ["sh", "scripts/start.sh"]

This multi-stage build approach reduces the final image size by over 60%, keeping only the production dependencies and compiled assets. The use of Alpine Linux further minimizes the footprint, while dumb-init ensures proper signal handling for graceful shutdowns in the containerized environment.

Automated CI/CD with Cloud Build

The deployment process is fully automated using Google Cloud Build, triggered on every push to the main branch:

cloudbuild.yaml
1steps:
2# Build Docker image with versioning
3- name: 'gcr.io/cloud-builders/docker'
4 args: ['build', '-t', 'gcr.io/$PROJECT_ID/website:$BUILD_ID',
5 '-t', 'gcr.io/$PROJECT_ID/website:latest', '.']
6 
7# Push to Google Container Registry
8- name: 'gcr.io/cloud-builders/docker'
9 args: ['push', 'gcr.io/$PROJECT_ID/website:$BUILD_ID']
10 
11# Deploy to Cloud Run with optimized settings
12- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
13 entrypoint: gcloud
14 args:
15 - 'run'
16 - 'deploy'
17 - 'website'
18 - '--image=gcr.io/$PROJECT_ID/website:$BUILD_ID'
19 - '--region=us-central1'
20 - '--platform=managed'
21 - '--allow-unauthenticated' # Public website access
22 - '--memory=512Mi' # Optimized memory allocation
23 - '--cpu=1' # Single CPU for cost efficiency
24 - '--max-instances=10' # Auto-scale up to 10 instances
25 - '--min-instances=0' # Scale to zero when idle
26 - '--set-env-vars=NODE_ENV=production'
27 - '--update-secrets=SUPABASE_URL=supabase-url:latest'

This configuration provides:

  • Automatic builds on code changes
  • Versioned deployments with rollback capability
  • Zero-downtime updates through Cloud Run's traffic management
  • Secret management for sensitive environment variables
  • Cost optimization through scale-to-zero and resource limits

Setting Up Cloud Run Deployment

Here's the complete process I used to deploy this application:

  1. Project Setup: Created a new Google Cloud project and enabled the required APIs:

    gcloud services enable cloudbuild.googleapis.com
    gcloud services enable run.googleapis.com
    gcloud services enable containerregistry.googleapis.com
  2. Container Registry Configuration: Configured Docker authentication for pushing images:

    gcloud auth configure-docker
  3. Initial Deployment: The first deployment was done manually to verify everything worked:

    # Build the container locally
    docker build -t gcr.io/PROJECT_ID/website:latest .
    
    # Push to Container Registry
    docker push gcr.io/PROJECT_ID/website:latest
    
    # Deploy to Cloud Run
    gcloud run deploy website \
      --image gcr.io/PROJECT_ID/website:latest \
      --region us-central1 \
      --allow-unauthenticated
  4. Automated Pipeline: Set up Cloud Build triggers to automatically deploy on pushes to main branch

  5. Custom Domain: Mapped the custom domain through Cloud Run's domain mapping feature:

    gcloud run domain-mappings create \
      --service website \
      --domain vivekmahendra.com \
      --region us-central1

Performance and Cost Benefits

The Cloud Run deployment delivers impressive results:

  • Cold starts under 1 second thanks to the optimized container
  • Response times averaging 50-100ms for cached content
  • Monthly costs under $5 for typical portfolio traffic
  • 99.95% uptime SLA with Google's infrastructure
  • Automatic scaling handling traffic spikes seamlessly

Monitoring and Observability

Cloud Run provides built-in monitoring through Google Cloud Console:

  • Real-time request metrics and latency tracking
  • Automatic error reporting and alerting
  • Container CPU and memory utilization graphs
  • Detailed request logs for debugging

The combination of React Router v7's server-side rendering and Cloud Run's edge locations ensures fast load times globally, while the scale-to-zero capability keeps hosting costs minimal for a portfolio site with variable traffic patterns.