Remove dev documentation and add design spec to CLAUDE.md

- Delete dev/CLAUDE.md, dev/blog-example.html, dev/contact-form-plan.md, dev/index.html, dev/optimization-guidelines.md, dev/plan.md
- Update CLAUDE.md to include Design Specification section referencing dev/design.json
- Modify dev/design.json with V7 Industrial Dark Mode system details
- Update src/utils/git-commit.js to reflect new commit message format
This commit is contained in:
Nicholai 2025-12-18 15:22:34 -07:00
parent 6a2780f9b0
commit 50f9a2df68
8 changed files with 8 additions and 1301 deletions

View File

@ -88,10 +88,13 @@ MDX file → src/content.config.ts schema → getCollection() → Component prop
- Static files in `public/media/` are served as-is (use absolute paths like `/media/file.mp4`)
- AVIF conversion utility available for optimization
### Styling
- Tailwind CSS v4 via Vite plugin
- Custom animation classes: `.animate-on-scroll`, `.slide-up`, `.stagger-*`, `.fade-in`
- Monospace font used for technical labels and metadata
## Design Specification
`dev/design.json` contains V7 Industrial Dark Mode system specification (not yet implemented):
- Dark mode native with `#0B0D11` primary background
- Orange/yellow accent `#FFB84C` for CTAs
- Brutalist/industrial aesthetic with visible grid structure
- Heavy typography emphasis
### Deployment
- Cloudflare Pages adapter configured in `astro.config.mjs`

View File

@ -1,61 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Astro 5 blog with Cloudflare Pages deployment. Uses PNPM as package manager.
## Commands
```bash
pnpm dev # Start dev server at localhost:4321
pnpm build # Build production site to ./dist/
pnpm preview # Build and preview locally via Wrangler
pnpm deploy # Build and deploy to Cloudflare Pages
pnpm cf-typegen # Generate Wrangler types for Cloudflare bindings
```
## Architecture
### Content System
- Blog posts in `src/content/blog/` as Markdown/MDX files
- Content schema defined in `src/content.config.ts` with Zod validation
- Required frontmatter: `title`, `description`, `pubDate`
- Optional frontmatter: `updatedDate`, `heroImage`
### Routing
- `src/pages/` - File-based routing
- `src/pages/blog/[...slug].astro` - Dynamic blog post routes
- `src/pages/rss.xml.js` - RSS feed endpoint
### Components
- `src/components/BaseHead.astro` - SEO metadata, og:image, Twitter cards
- `src/layouts/BlogPost.astro` - Blog post layout template
- `src/consts.ts` - Site title and description constants
### Styling
- Tailwind CSS via Vite plugin
- Design tokens in `src/styles/global.css` as CSS custom properties
- Current accent color: `#2337ff` (blue)
- Max content width: 720px
### Cloudflare Integration
- Adapter: `@astrojs/cloudflare` with platform proxy enabled
- Wrangler config: `wrangler.jsonc`
- Environment types: `src/env.d.ts`
- Node.js compatibility enabled via `nodejs_compat` flag
## Design Specification
`design.json` contains V7 Industrial Dark Mode system specification (not yet implemented):
- Dark mode native with `#0B0D11` primary background
- Orange/yellow accent `#FFB84C` for CTAs
- Brutalist/industrial aesthetic with visible grid structure
- Heavy typography emphasis
## Key Configuration
- **Site URL**: Currently `https://example.com` in `astro.config.mjs` - update for production
- **Project name**: `nicholai-work-2026` in `wrangler.jsonc`
- **TypeScript**: Strict mode with Astro and Cloudflare Worker types

File diff suppressed because one or more lines are too long

View File

@ -1,262 +0,0 @@
# Contact Form → n8n Webhook with Personalized MDX Response Plan
## Overview
Wire the existing Astro contact form to an n8n webhook using `PUBLIC_N8N_WEBHOOK_URL`, enable n8n to return a personalized MDX/Markdown message, render it on the client, and implement automatic fallback to a standard toast notification when n8n is down or fails.
---
## Implementation Steps
### 1. n8n Webhook + Environment Setup
**Verify n8n Webhook Configuration:**
- In your n8n instance, create or verify a Webhook node configured for `POST` requests
- Use a path like `contact-form`
- Note the complete webhook URL (e.g., `https://your-n8n-instance.com/webhook/contact-form`)
**Define Response Contract:**
The n8n workflow should return JSON in one of these formats:
- **Success:** `{ success: true, format: 'mdx', message: '...markdown/mdx string...' }`
- **Handled Error:** `{ success: false, error: 'Human-friendly error message' }`
**Environment Variable:**
- Confirm `PUBLIC_N8N_WEBHOOK_URL` is set in `.env` with the webhook URL
- Ensure the same variable is configured in your Cloudflare Pages environment settings
- Optional: Update `env.d.ts` to type `import.meta.env.PUBLIC_N8N_WEBHOOK_URL` for TypeScript safety
---
### 2. Wire Astro Contact Form to n8n (with Robust Error Detection)
**File to modify:** `src/pages/contact.astro`
**Form Markup Updates:**
- Add `id="contact-form"` to the form element
- Remove `action="#"` and `method="POST"` attributes (JavaScript will handle submission)
- Preserve all existing classes, labels, and the custom subject dropdown
**Client-Side Submit Handler:**
Add a new script block (or extend the existing one) with:
1. **Form submission interception:**
- Attach a `submit` event listener that calls `preventDefault()`
- Extract form data using `FormData` API
- Build JSON payload including:
- `name`, `email`, `subject`, `message`
- Metadata: `timestamp` (ISO string), `source: 'portfolio-website'`
2. **Fetch with timeout wrapper:**
- Use `fetch(import.meta.env.PUBLIC_N8N_WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) })`
- Wrap with `AbortController` or `Promise.race` for 8-10 second timeout
3. **Failure detection conditions** (any of these triggers fallback):
- Network error or thrown exception
- Timeout reached
- Non-2xx HTTP response
- 2xx response with `success: false` in JSON
4. **Success path:**
- Extract the `message` field from response
- Pass to MDX/Markdown rendering logic (see Step 3)
- Show brief success state on submit button
5. **Failure path:**
- Display standard toast notification with error message
- Keep form data intact (don't reset)
- Re-enable submit button
**Button UX States:**
- **Waiting:** Disable button, change text to "Transmitting..."
- **Success:** Briefly show "Message Sent!" then re-enable
- **Failure:** Show "Transmission Failed" then revert to original text
---
### 3. Render Personalized MDX/Markdown Response
**Add Markdown Renderer:**
- Install a lightweight markdown library via `pnpm add marked` (or `markdown-it`)
- Import it in the client-side script section
**Response Panel UI:**
- Create a dedicated container near the form submit area (e.g., bordered card)
- Initially hidden (`hidden` class or `display: none`)
- Becomes visible only when successful response is received
- Style with existing design system classes for consistency
**Rendering Logic:**
When response has `success: true` and `format: 'mdx'`:
1. Convert the `message` string to HTML using the markdown library
2. Inject into response panel using `innerHTML`
3. Apply typography classes (`prose` or custom) for proper formatting
4. If markdown conversion throws, treat as failure and show fallback toast
**Accessibility:**
- Add `role="status"` to the response panel
- Ensure proper color contrast
- Test with keyboard navigation and screen readers
**Security:**
- Since content comes from your own n8n instance, it's trusted
- Still avoid allowing script tags in the markdown content
- Keep response panel visually constrained
---
### 4. n8n Workflow Processing & Templating
**In your n8n workflow (after the Webhook node):**
**Template the Personalized Message:**
- Use Set or Function nodes to build a Markdown/MDX string
- Use incoming fields like `{{ $json.name }}`, `{{ $json.subject }}`, `{{ $json.message }}`
- Example structure:
```markdown
# Thanks, {{ name }}!
I received your message about **{{ subject }}**.
I'll review it and get back to you within 24-48 hours at {{ email }}.
In the meantime, feel free to check out [my recent work](/work) or [blog posts](/blog).
— Nicholai
```
**Workflow Branches:**
- **Validation node:** Check for required fields (name, email, message)
- If missing: Return `{ success: false, error: 'Please fill in all required fields' }`
- **Email notification node:** Send yourself a formatted email with the submission details
- **Optional logging node:** Save to Google Sheets, database, or CRM
**Webhook Response Node:**
- At the end of the workflow, add a "Respond to Webhook" node
- Return JSON matching the contract:
- Success: `{ success: true, format: 'mdx', message: '...' }`
- Error: `{ success: false, error: '...' }`
- For unexpected internal errors, either:
- Let workflow fail (frontend timeout will catch it), or
- Wrap in try/catch and still return `{ success: false }`
---
### 5. Fallback Toast & Automatic Failure Detection UX
**Toast Notification Implementation:**
- Create a reusable toast function (if not already present)
- Should support both success and error styles
- Position in top-right or bottom-right of viewport
- Auto-dismiss after 5-7 seconds with smooth fade-out
**Error Toast Content:**
```
"We couldn't reach the messaging system. Please try again or email me directly at nicholai@nicholai.work"
```
**Automatic Detection:**
- Trigger error toast for any failure condition from Step 2
- Works even if n8n is completely unreachable (DNS/SSL issues, 500 errors, timeouts)
**User Experience:**
- On failure: **Do not clear the form** (preserves user's work)
- Optional: Add inline text under submit button: "Auto-response unavailable; message will still be delivered via email"
- Ensure toast has `role="alert"` for accessibility
---
### 6. Testing & Validation
**Happy Path Tests:**
- With n8n workflow active and webhook listening:
1. Submit form with various subject/message combinations
2. Verify n8n receives correct payload with all fields
3. Confirm n8n builds expected personalized MDX string
4. Check that frontend displays rendered response panel with proper formatting
5. Verify email notification is sent
6. Test that form resets appropriately
**Failure Path Tests:**
1. **n8n completely down:**
- Stop n8n instance or point env var to invalid URL
- Submit form
- Confirm: Timeout triggers, error toast appears, form data preserved, no response panel shown
2. **n8n returns error:**
- Modify workflow to return `{ success: false, error: 'Test error' }`
- Submit form
- Confirm: Error toast shows n8n's error message, no response panel
3. **Network timeout:**
- Add artificial delay in n8n workflow (>10 seconds)
- Confirm: Frontend timeout triggers fallback
4. **Invalid markdown:**
- Have n8n return malformed markdown that breaks the parser
- Confirm: Rendering error is caught and fallback toast appears
**Browser & Responsiveness:**
- Test on desktop (Chrome, Firefox, Safari)
- Test on mobile viewport (iOS Safari, Chrome Android)
- Verify response panel and toasts don't break layout
- Check animations and transitions are smooth
- Test with keyboard-only navigation
- Test with screen reader (VoiceOver or NVDA)
**Production Verification:**
- After deploying with env var configured:
1. Submit real test message from live site
2. Confirm end-to-end flow works
3. Check browser console for CORS errors (adjust n8n/proxy if needed)
4. Verify SSL/HTTPS works correctly
5. Test from different networks (WiFi, mobile data)
---
### 7. Future-Proofing Options
**Server-Side Proxy (Optional):**
If you want to hide the webhook URL and do MDX→HTML conversion server-side:
1. Create an Astro API route (e.g., `/api/contact.ts`) or Cloudflare Worker
2. Have it:
- Accept form JSON from browser
- Add server-side validation/rate limiting
- Call n8n webhook
- Convert returned MDX to HTML server-side
- Return normalized `{ success, html }` to client
3. Frontend code changes minimally (just POST URL changes)
**Benefits:**
- Webhook URL never exposed to client
- Additional security layer
- Server-side rate limiting
- Can add spam protection (honeypot, CAPTCHA)
**Richer MDX Components:**
If you later want actual MDX components (not just markdown):
- Add runtime MDX renderer like `@mdx-js/mdx` on client
- Or render MDX to React components server-side in the proxy route
- Would allow n8n to return interactive components, not just static markdown
---
## Critical Files
- **`src/pages/contact.astro`** - Main file to modify (form markup + client script)
- **`.env`** - Contains `PUBLIC_N8N_WEBHOOK_URL`
- **`env.d.ts`** - Optional TypeScript environment variable typing
- **n8n workflow** - Webhook node + processing nodes + response node
---
## Success Criteria
✅ Form submits to n8n webhook successfully
✅ n8n returns personalized MDX message
✅ Frontend renders markdown as HTML in response panel
✅ Timeout/error conditions trigger fallback toast
✅ Form data preserved on failure
✅ Works on desktop and mobile
✅ Accessible to keyboard and screen reader users
✅ No CORS issues in production
✅ Email notifications sent from n8n

View File

@ -40,8 +40,7 @@
"usage": "Secondary backgrounds, panels, cards"
},
"brand_accent": {
"hex": "#ff4d00",
"rgb": "255, 77, 0",
"hex": "#dd4132",
"name": "Vibrant Orange",
"usage": "Primary accent, CTAs, highlights, interactive elements, status indicators",
"opacity_variants": [

View File

@ -1,603 +0,0 @@
<!DOCTYPE html>
<html lang="en" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nicholai Vogel | VFX Artist & Technical Generalist</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&family=Space+Mono:wght@400;700&display=swap"
rel="stylesheet">
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<!-- Design System Configuration -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
brand: {
dark: '#0B0D11', // Primary BG
panel: '#151921', // Secondary BG
accent: '#FFB84C', // Orange/Yellow
cyan: '#22D3EE',
border: 'rgba(255, 255, 255, 0.1)',
borderStrong: 'rgba(255, 255, 255, 0.2)'
}
},
fontFamily: {
sans: ['Inter', 'sans-serif'],
mono: ['Space Mono', 'monospace'],
},
spacing: {
'128': '32rem',
},
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
}
}
}
}
</script>
<style>
/* Custom Utilities based on Design JSON */
body {
background-color: #0B0D11;
color: #ffffff;
overflow-x: hidden;
cursor: none;
/* Custom cursor implementation */
}
/* The Grid Overlay */
.grid-overlay {
background-size: 100px 100px;
background-image:
linear-gradient(to right, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
pointer-events: none;
z-index: 0;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #0B0D11;
}
::-webkit-scrollbar-thumb {
background: #334155;
}
::-webkit-scrollbar-thumb:hover {
background: #FFB84C;
}
/* Typography Utilities */
.text-massive {
line-height: 0.9;
letter-spacing: -0.04em;
}
.text-stroke {
-webkit-text-stroke: 1px rgba(255, 255, 255, 0.1);
color: transparent;
}
/* Animations */
.reveal-text {
opacity: 0;
transform: translateY(20px);
transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1);
}
.reveal-text.active {
opacity: 1;
transform: translateY(0);
}
/* Custom Cursor */
.cursor-dot,
.cursor-outline {
position: fixed;
top: 0;
left: 0;
transform: translate(-50%, -50%);
border-radius: 50%;
z-index: 9999;
pointer-events: none;
}
.cursor-dot {
width: 8px;
height: 8px;
background-color: #FFB84C;
}
.cursor-outline {
width: 40px;
height: 40px;
border: 1px solid rgba(255, 184, 76, 0.5);
transition: width 0.2s, height 0.2s, background-color 0.2s;
}
/* Interactive Elements */
.hover-trigger:hover~.cursor-outline,
a:hover~.cursor-outline,
button:hover~.cursor-outline {
width: 60px;
height: 60px;
background-color: rgba(255, 184, 76, 0.05);
border-color: #FFB84C;
}
</style>
</head>
<body class="antialiased selection:bg-brand-accent selection:text-brand-dark">
<!-- Custom Cursor -->
<div class="cursor-dot hidden md:block"></div>
<div class="cursor-outline hidden md:block"></div>
<!-- Fixed Grid Overlay -->
<div class="fixed inset-0 grid-overlay h-screen w-screen"></div>
<!-- 12 Column Guide (Visual Only - Low Opacity) -->
<div
class="fixed inset-0 container mx-auto px-6 lg:px-12 grid grid-cols-4 md:grid-cols-12 gap-4 pointer-events-none z-0 opacity-10 border-x border-white/5">
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
<div class="border-r border-white/5 h-full hidden md:block"></div>
</div>
<!-- Navigation -->
<nav
class="fixed top-0 left-0 w-full z-50 px-6 lg:px-12 py-8 flex justify-between items-center backdrop-blur-sm border-b border-white/5">
<div class="flex items-center gap-4 group cursor-none-target">
<div
class="w-10 h-10 border border-brand-accent/50 flex items-center justify-center bg-brand-accent/5 group-hover:bg-brand-accent transition-colors duration-300">
<span class="font-bold text-brand-accent group-hover:text-brand-dark">NV</span>
</div>
<div class="flex flex-col">
<span class="text-xs font-mono uppercase tracking-widest text-slate-400">Status</span>
<span class="text-xs font-bold text-brand-accent animate-pulse">AVAILABLE FOR WORK</span>
</div>
</div>
<div class="hidden md:flex gap-12">
<a href="#about"
class="text-xs font-bold uppercase tracking-widest text-slate-500 hover:text-white transition-colors">01.
Profile</a>
<a href="#work"
class="text-xs font-bold uppercase tracking-widest text-slate-500 hover:text-white transition-colors">02.
Selected Works</a>
<a href="#experience"
class="text-xs font-bold uppercase tracking-widest text-slate-500 hover:text-white transition-colors">03.
History</a>
<a href="#skills"
class="text-xs font-bold uppercase tracking-widest text-slate-500 hover:text-white transition-colors">04.
Stack</a>
</div>
<a href="mailto:nicholai@nicholai.work"
class="border border-slate-600 px-6 py-3 text-xs font-bold uppercase tracking-widest hover:border-brand-accent hover:bg-brand-accent/10 transition-all cursor-none-target">
Contact
</a>
</nav>
<!-- Main Content -->
<main class="relative z-10 pt-32 lg:pt-48 pb-24">
<!-- Hero Section -->
<section class="container mx-auto px-6 lg:px-12 min-h-[70vh] flex flex-col justify-center relative">
<div class="grid grid-cols-1 md:grid-cols-12 gap-6">
<div class="col-span-12">
<p class="font-mono text-brand-accent mb-4 ml-1 reveal-text">/// TECHNICAL GENERALIST & VFX
SUPERVISOR</p>
<h1
class="text-6xl md:text-8xl lg:text-[10rem] font-bold text-massive uppercase leading-none tracking-tighter mb-8 text-white">
<span class="reveal-text block delay-100">Visual</span>
<span
class="reveal-text block delay-200 text-transparent bg-clip-text bg-gradient-to-tr from-brand-accent to-white">Alchemist</span>
</h1>
</div>
<div
class="col-span-12 md:col-span-6 lg:col-span-5 lg:col-start-8 mt-12 border-l border-brand-accent/30 pl-8 reveal-text delay-300">
<p class="text-slate-400 text-lg leading-relaxed mb-8">
I am a problem solver who loves visual effects. With 10 years of experience creating end-to-end
visual content for clients like <span class="text-white font-semibold">Post Malone</span>, <span
class="text-white font-semibold">Stinkfilms</span>, and <span
class="text-white font-semibold">Adidas</span>. Comfortable managing teams while staying
knee-deep in hands-on shot work.
</p>
<div class="flex gap-4">
<a href="#work"
class="group flex items-center gap-3 text-xs font-bold uppercase tracking-widest text-white hover:text-brand-accent transition-colors">
<span class="w-8 h-[1px] bg-brand-accent group-hover:w-12 transition-all"></span>
View Selected Works
</a>
</div>
</div>
</div>
<!-- Hero Footer -->
<div
class="absolute bottom-0 w-full left-0 px-6 lg:px-12 pb-12 hidden md:flex justify-between items-end opacity-50">
<div class="font-mono text-xs text-slate-500">
LOC: COLORADO SPRINGS<br>
TIME: <span id="clock">00:00:00</span>
</div>
<div class="font-mono text-xs text-slate-500 text-right">
SCROLL<br>
</div>
</div>
</section>
<!-- Divider -->
<div class="w-full h-[1px] bg-white/10 my-24"></div>
<!-- Experience / Timeline -->
<section id="experience" class="container mx-auto px-6 lg:px-12 py-24">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12">
<div class="lg:col-span-4">
<h2 class="text-4xl font-bold uppercase tracking-tight mb-2 text-stroke">Experience</h2>
<h2 class="text-4xl font-bold uppercase tracking-tight mb-8">History</h2>
<p class="text-slate-400 mb-8 max-w-sm">
From founding my own VFX house to supervising global campaigns. I bridge the gap between
creative vision and technical execution.
</p>
<a href="https://biohazardvfx.com" target="_blank"
class="inline-flex items-center gap-2 text-xs font-mono uppercase text-brand-accent hover:underline">
Visit Biohazard VFX <i data-lucide="arrow-up-right" class="w-4 h-4"></i>
</a>
</div>
<div class="lg:col-span-8 relative">
<!-- Vertical line -->
<div
class="absolute left-0 top-0 bottom-0 w-[1px] bg-gradient-to-b from-brand-accent via-slate-800 to-transparent">
</div>
<!-- Item 1 -->
<div class="pl-12 mb-20 relative reveal-text">
<div
class="absolute left-[-5px] top-2 w-2.5 h-2.5 bg-brand-dark border border-brand-accent rounded-full">
</div>
<div class="flex flex-col md:flex-row md:items-baseline gap-4 mb-4">
<h3 class="text-2xl font-bold text-white uppercase">Biohazard VFX</h3>
<span class="font-mono text-xs text-brand-accent bg-brand-accent/10 px-2 py-1">FOUNDER / VFX
SUPERVISOR</span>
<span class="font-mono text-xs text-slate-500 ml-auto">MAR 2022 - OCT 2025</span>
</div>
<p class="text-slate-400 mb-6 leading-relaxed">
Founded and led a VFX studio specializing in high-end commercial and music video work for
Post Malone, ENHYPEN, and Nike. Architected a custom pipeline combining cloud and
self-hosted infrastructure.
</p>
<ul class="space-y-2 mb-6">
<li class="flex items-start gap-3 text-sm text-slate-300">
<span class="text-brand-accent mt-1"></span>
Designed 7-plate reconciliation workflows for ENHYPEN (projection mapping live action
onto CAD).
</li>
<li class="flex items-start gap-3 text-sm text-slate-300">
<span class="text-brand-accent mt-1"></span>
Developed QA systems for AI-generated assets, transforming mid-tier output into
production-ready deliverables.
</li>
</ul>
</div>
<!-- Item 2 -->
<div class="pl-12 mb-20 relative reveal-text">
<div class="absolute left-[-5px] top-2 w-2.5 h-2.5 bg-slate-700 rounded-full"></div>
<div class="flex flex-col md:flex-row md:items-baseline gap-4 mb-4">
<h3 class="text-2xl font-bold text-white uppercase">Stinkfilms</h3>
<span class="font-mono text-xs text-slate-400 border border-slate-700 px-2 py-1">GLOBAL
PRODUCTION STUDIO</span>
<span class="font-mono text-xs text-slate-500 ml-auto">SUMMER 2024</span>
</div>
<p class="text-slate-400 mb-6 leading-relaxed">
Led Biohazard VFX team (60+ artists) alongside director Felix Brady to create a brand film
for G-Star Raw.
</p>
<div
class="border border-white/5 bg-white/5 p-6 backdrop-blur-sm hover:border-brand-accent/50 transition-colors cursor-pointer group">
<h4 class="text-sm font-bold uppercase text-white mb-2 flex justify-between">
Project: G-Star Raw Olympics Campaign
<i data-lucide="arrow-right"
class="w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity text-brand-accent"></i>
</h4>
<p class="text-xs text-slate-400 mb-4">Managed full CG environments in Blender/Houdini and
integrated AI/ML workflows (Stable Diffusion reference gen, Copycat cleanup).</p>
<a href="https://stinkfilms.com"
class="text-[10px] font-bold text-brand-accent uppercase tracking-widest">View Case
Study</a>
</div>
</div>
<!-- Item 3 -->
<div class="pl-12 relative reveal-text">
<div class="absolute left-[-5px] top-2 w-2.5 h-2.5 bg-slate-700 rounded-full"></div>
<div class="flex flex-col md:flex-row md:items-baseline gap-4 mb-4">
<h3 class="text-2xl font-bold text-white uppercase">Freelance</h3>
<span class="font-mono text-xs text-slate-400 border border-slate-700 px-2 py-1">2D/3D
ARTIST</span>
<span class="font-mono text-xs text-slate-500 ml-auto">2015 - PRESENT</span>
</div>
<p class="text-slate-400 mb-4 leading-relaxed">
Compositor for Abyss Digital and major labels (Atlantic, Interscope). Clients: David
Kushner, Opium, Lil Durk, Don Toliver.
</p>
</div>
</div>
</div>
</section>
<!-- Divider -->
<div class="container mx-auto px-6 lg:px-12">
<div class="w-full h-[1px] bg-white/10"></div>
</div>
<!-- Featured Project Section (G-Star Raw) -->
<section id="work" class="py-24">
<div class="container mx-auto px-6 lg:px-12 mb-12">
<span class="text-xs font-mono text-brand-accent mb-2 block">/// HIGHLIGHT</span>
<h2 class="text-5xl md:text-7xl font-bold uppercase text-white mb-4">G-Star Raw <span
class="text-stroke">Olympics</span></h2>
</div>
<!-- Full Width Project Card -->
<div class="w-full h-[80vh] relative group overflow-hidden border-y border-white/10">
<!-- Abstract Background representing the project -->
<div
class="absolute inset-0 bg-[url('https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=2564&auto=format&fit=crop')] bg-cover bg-center transition-transform duration-1000 group-hover:scale-105">
</div>
<div class="absolute inset-0 bg-brand-dark/80 mix-blend-multiply"></div>
<div class="absolute inset-0 bg-gradient-to-t from-brand-dark via-brand-dark/20 to-transparent"></div>
<!-- Grid Overlay on image -->
<div
class="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdHRlcm4gaWQ9ImIiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PGNpcmNsZSBjeD0iMiIgY3k9IjIiIHI9IjEiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4xKSIvPjwvcGF0dGVybj48cmVjdCB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9InVybCgjYikiLz48L3BhdHRlcm4+PC9kZWZzPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjYSkiLz48L3N2Zz4=')] opacity-30">
</div>
<div
class="absolute bottom-0 left-0 w-full p-6 lg:p-12 flex flex-col md:flex-row items-end justify-between">
<div
class="max-w-2xl transform translate-y-8 group-hover:translate-y-0 transition-transform duration-500">
<div class="flex gap-2 mb-4">
<span class="bg-brand-accent text-brand-dark text-[10px] font-bold uppercase px-2 py-1">VFX
Supervision</span>
<span
class="border border-white/30 text-white text-[10px] font-bold uppercase px-2 py-1">AI/ML</span>
<span
class="border border-white/30 text-white text-[10px] font-bold uppercase px-2 py-1">Houdini</span>
</div>
<p class="text-xl md:text-2xl text-white font-medium mb-6">
Managed full CG environment builds, procedural city generation, and integrated AI-generated
normal maps for relighting in Nuke.
</p>
<a href="https://f.io/7ijf23Wm"
class="inline-flex items-center gap-3 text-sm font-bold uppercase tracking-widest text-white hover:text-brand-accent transition-colors">
Watch Making Of <i data-lucide="play-circle" class="w-5 h-5"></i>
</a>
</div>
<div class="hidden md:block text-right">
<span class="block text-[10px] uppercase text-slate-500 tracking-widest mb-1">Year</span>
<span class="block text-2xl font-bold text-white mb-4">2024</span>
<span class="block text-[10px] uppercase text-slate-500 tracking-widest mb-1">Client</span>
<span class="block text-xl font-bold text-white">Stinkfilms</span>
</div>
</div>
</div>
</section>
<!-- Skills Matrix -->
<section id="skills" class="container mx-auto px-6 lg:px-12 py-24 bg-brand-panel border-y border-white/5">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12">
<div class="col-span-1 md:col-span-2 lg:col-span-4 mb-8">
<h2 class="text-4xl font-bold uppercase mb-2">Technical Arsenal</h2>
<p class="text-slate-400 font-mono text-sm">/// SOFTWARE & LANGUAGES</p>
</div>
<!-- Compositing -->
<div>
<h3 class="text-lg font-bold text-white uppercase mb-6 flex items-center gap-2">
<i data-lucide="layers" class="w-4 h-4 text-brand-accent"></i> Compositing
</h3>
<div class="flex flex-wrap gap-2">
<span class="skill-tag">Nuke/NukeX</span>
<span class="skill-tag">ComfyUI</span>
<span class="skill-tag">After Effects</span>
<span class="skill-tag">Photoshop</span>
<span class="skill-tag">Deep Compositing</span>
<span class="skill-tag">Live Action VFX</span>
</div>
</div>
<!-- 3D -->
<div>
<h3 class="text-lg font-bold text-white uppercase mb-6 flex items-center gap-2">
<i data-lucide="box" class="w-4 h-4 text-brand-accent"></i> 3D Generalist
</h3>
<div class="flex flex-wrap gap-2">
<span class="skill-tag">Houdini</span>
<span class="skill-tag">Blender</span>
<span class="skill-tag">Maya</span>
<span class="skill-tag">USD</span>
<span class="skill-tag">Solaris/Karma</span>
<span class="skill-tag">Unreal Engine</span>
<span class="skill-tag">Substance</span>
<span class="skill-tag">Procedural Gen</span>
</div>
</div>
<!-- AI/ML -->
<div>
<h3 class="text-lg font-bold text-white uppercase mb-6 flex items-center gap-2">
<i data-lucide="cpu" class="w-4 h-4 text-brand-accent"></i> AI/ML Integration
</h3>
<div class="flex flex-wrap gap-2">
<span class="skill-tag bg-brand-accent/10 border-brand-accent/50 text-brand-accent">Stable
Diffusion</span>
<span class="skill-tag">LoRA Training</span>
<span class="skill-tag">Dataset Prep</span>
<span class="skill-tag">Synthetic Data</span>
<span class="skill-tag">Prompt Engineering</span>
</div>
</div>
<!-- Dev -->
<div>
<h3 class="text-lg font-bold text-white uppercase mb-6 flex items-center gap-2">
<i data-lucide="code" class="w-4 h-4 text-brand-accent"></i> Development
</h3>
<div class="flex flex-wrap gap-2">
<span class="skill-tag">Python</span>
<span class="skill-tag">JavaScript</span>
<span class="skill-tag">React</span>
<span class="skill-tag">Docker</span>
<span class="skill-tag">Linux</span>
<span class="skill-tag">Pipeline Dev</span>
</div>
</div>
</div>
</section>
<!-- Footer / Contact -->
<footer class="container mx-auto px-6 lg:px-12 py-32 relative overflow-hidden">
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 items-end">
<div>
<h2
class="text-6xl md:text-8xl font-bold uppercase leading-none tracking-tighter mb-8 text-white group cursor-pointer">
Let's<br>
<span
class="text-stroke group-hover:text-brand-accent transition-colors duration-300">Build</span><br>
Reality.
</h2>
<div class="flex flex-wrap gap-6 mt-12">
<a href="mailto:nicholai@nicholai.work" class="btn-primary">nicholai@nicholai.work</a>
<a href="tel:7196604281" class="btn-ghost">719 660 4281</a>
</div>
</div>
<div class="md:text-right">
<div class="mb-12">
<p class="text-xs font-bold uppercase text-slate-500 mb-4 tracking-widest">Social Uplink</p>
<ul class="space-y-2">
<li><a href="https://nicholai.work"
class="text-white hover:text-brand-accent text-lg font-mono">nicholai.work</a></li>
<li><a href="#"
class="text-white hover:text-brand-accent text-lg font-mono">@nicholai.exe</a></li>
<li><a href="#" class="text-white hover:text-brand-accent text-lg font-mono">LinkedIn</a>
</li>
</ul>
</div>
<div class="flex justify-end items-end gap-2 text-[10px] text-slate-600 font-mono uppercase">
<span>© 2025 Nicholai Vogel</span>
<span>/</span>
<span>V7 SYSTEM</span>
</div>
</div>
</div>
<!-- Decorative huge text bg -->
<div class="absolute -bottom-10 left-0 w-full text-center pointer-events-none opacity-5">
<span class="text-[15rem] font-bold text-white uppercase leading-none whitespace-nowrap">VOGEL</span>
</div>
</footer>
</main>
<style>
/* Component Classes */
.skill-tag {
@apply text-[10px] font-mono font-bold uppercase tracking-wider px-2 py-2 border border-slate-700 text-slate-400 hover:border-brand-accent hover:text-white transition-colors cursor-default select-none;
}
.btn-primary {
@apply bg-brand-accent text-brand-dark px-8 py-4 text-xs font-bold uppercase tracking-widest hover:bg-white transition-colors inline-block;
}
.btn-ghost {
@apply border border-slate-600 text-white px-8 py-4 text-xs font-bold uppercase tracking-widest hover:border-brand-accent hover:bg-brand-accent/5 transition-colors inline-block;
}
</style>
<script>
// Icons
lucide.createIcons();
// Clock
function updateClock() {
const now = new Date();
const timeString = now.toLocaleTimeString('en-US', {hour12: false, timeZone: 'America/Denver'});
document.getElementById('clock').textContent = timeString + " MST";
}
setInterval(updateClock, 1000);
updateClock();
// Custom Cursor Logic
const cursorDot = document.querySelector('.cursor-dot');
const cursorOutline = document.querySelector('.cursor-outline');
window.addEventListener('mousemove', (e) => {
const posX = e.clientX;
const posY = e.clientY;
// Dot follows instantly
cursorDot.style.left = `${posX}px`;
cursorDot.style.top = `${posY}px`;
// Outline follows with slight delay (handled by CSS transition, we just set position)
// But for smoother JS animation:
cursorOutline.animate({
left: `${posX}px`,
top: `${posY}px`
}, {duration: 500, fill: "forwards"});
});
// Intersection Observer for Reveal Animations
const observerOptions = {
threshold: 0.1,
rootMargin: "0px"
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('active');
}
});
}, observerOptions);
document.querySelectorAll('.reveal-text').forEach(el => {
observer.observe(el);
});
</script>
</body>
</html>

View File

@ -1,243 +0,0 @@
# High-Performance Web Code Guidelines
## Lessons from a 100-Year-Old Company with One of the Fastest Websites on the Internet
This guide distills practical engineering principles observed from a deep inspection of McMaster-Carrs website - a site that looks old, feels instant, and consistently outperforms modern, framework-heavy builds. None of these techniques are new. All of them are deliberate.
The takeaway is simple: **perceived speed is a product of ruthless prioritization, not trendy technology**.
---
## Core Philosophy
- HTML is the product. Everything else is optional.
- Load what is needed, when it is needed, and never sooner.
- Prevent layout shifts at all costs.
- Measure everything. Assume nothing.
- Optimize for real users on real devices, not benchmarks.
---
## 1. Server-Rendered HTML as the Primary Artifact
### Guideline
Always server-render full HTML responses.
### Why
Browsers are extremely good at parsing and rendering HTML. Shipping complete markup eliminates client-side bootstrapping delays and avoids blocking the initial render.
### Practices
- Avoid client-side frameworks for initial rendering.
- Return fully formed HTML from the server.
- Treat JavaScript as an enhancement layer, not a prerequisite.
---
## 2. Aggressive HTML Prefetching on User Intent
### Guideline
Prefetch HTML on hover or early interaction signals.
### Why
By the time the user clicks, the page is already downloaded.
### Practices
- Trigger HTML requests on `mouseover` or focus.
- Cache prefetched responses for immediate swap-in.
- Replace only the page-specific content shell, not global UI.
### Result
- Page content renders before the URL bar updates.
- Navigation feels instantaneous.
---
## 3. Partial Page Swaps with History API
### Guideline
Update only what changes between pages.
### Why
Navigation, carts, headers, and persistent UI should not be reloaded.
### Practices
- Use `pushState` to manage navigation.
- Replace only the dynamic content region.
- Preserve application state across page transitions.
---
## 4. Multi-Layer Caching Strategy
### Guideline
Cache HTML everywhere.
### Layers
- CDN edge caching for pre-rendered HTML.
- Proxy caches (e.g. Squid).
- Browser cache via service workers.
### Practices
- Inspect response headers to confirm cache hits.
- Use `HIT/MISS` headers to validate effectiveness.
- Serve cached HTML instantly when possible.
---
## 5. Service Workers for HTML, Not Just Assets
### Guideline
Intercept and serve cached HTML using service workers.
### Why
HTML caching enables near-zero-latency reloads and offline support.
### Practices
- Cache primary routes via service worker.
- Serve cached HTML on repeat visits.
- Use this approach to power mobile and iOS applications.
---
## 6. Strategic Resource Preloading
### Guideline
Tell the browser what it will need before it discovers it.
### Practices
- Use `<link rel="preload">` for:
- Logos
- Web fonts
- Critical images
- Use `<link rel="dns-prefetch">` for:
- Image domains
- Asset CDNs
### Why
This collapses waterfall request chains and removes DNS lookup latency during render.
---
## 7. Critical CSS Inlined in HTML
### Guideline
Inline all above-the-fold CSS directly in the document `<head>`.
### Why
External CSS blocks rendering and causes layout jank.
### Practices
- Embed essential layout and typography CSS inline.
- Load non-critical CSS asynchronously after initial render.
- Ensure the browser has all layout rules before parsing body HTML.
### Result
- No flashes
- No reflows
- No layout instability
---
## 8. Zero Layout Shift Image Strategy
### Guideline
Always reserve image space before images load.
### Practices
- Explicitly define width and height for all images.
- Use fixed-size containers for background images.
- Never allow images to resize content after load.
### Result
- No cumulative layout shift
- Stable rendering pipeline
---
## 9. Sprite-Based Image Bundling
### Guideline
Minimize image requests by bundling assets into sprites.
### Why
One request beats many, especially on constrained devices.
### Practices
- Combine page images into a single sprite.
- Use CSS background positioning to display regions.
- Prefer fewer medium-sized assets over many small ones.
---
## 10. Page-Specific JavaScript Loading
### Guideline
Only load JavaScript that is required for the current page.
### Why
Unused JavaScript still blocks parsing, execution, and memory.
### Practices
- Generate page-level dependency manifests server-side.
- Include only required scripts per route.
- Avoid global JavaScript bundles.
### Concept
Dependency injection at the page level, not the application level.
---
## 11. JavaScript Is Secondary, Not Sacred
### Observations
- Legacy libraries like YUI and jQuery are still in use.
- Total JavaScript payload can be large and still feel fast.
### Why It Works
- JavaScript does not block HTML rendering.
- Execution is deferred until after meaningful paint.
- Performance is measured and monitored constantly.
### Guideline
Framework choice does not determine performance discipline does.
---
## 12. Instrument Everything
### Guideline
Measure real user performance continuously.
### Practices
- Use `window.performance`
- Add custom performance marks.
- Track Largest Contentful Paint and render milestones.
### Why
You cannot optimize what you do not measure.
---
## 13. Optimize for Real Users, Not Ideal Conditions
### User Reality
- Old phones
- Dirty screens
- Fat fingers
- Poor connectivity
- Zero patience
### Design Implication
- Speed is usability.
- Complexity is abandonment.
- Friction leads to phone calls and paper workflows.
---
## Final Takeaways
- Fast websites are engineered, not themed.
- Old technology can outperform modern stacks when used intentionally.
- HTML-first, cache-everywhere, and measure-constantly beats any framework war.
- Perceived performance matters more than architectural purity.
This approach is compatible with modern server-rendered frameworks and decades-old stacks alike. The difference is not tooling. The difference is discipline.

View File

@ -1,125 +0,0 @@
# Portfolio Site Integration Plan
## Phase 1: Foundation Setup
### Tailwind Configuration
Update `src/styles/global.css` to configure Tailwind v4 with the V7 design system tokens from `design.json`:
- Brand colors (dark: #0B0D11, panel: #151921, accent: #FFB84C, cyan: #22D3EE)
- Font families (Inter + Space Mono from Google Fonts)
- Custom utilities (grid-overlay, text-massive, text-stroke, skill-tag, btn-primary, btn-ghost)
- Scrollbar styling, reveal animations, cursor styles
### Constants Update
Update `src/consts.ts` with Nicholai's site metadata:
- SITE_TITLE, SITE_DESCRIPTION, contact info, social links
## Phase 2: Core Components
### BaseHead.astro
Modify to include:
- Google Fonts preconnect/stylesheet (Inter, Space Mono)
- Lucide icons CDN script
- Remove old Atkinson font preloads
### Layout Components (new files in `src/components/`)
Extract from index.html:
| Component | Purpose |
|-----------|---------|
| `GridOverlay.astro` | Fixed background grid + 12-column guide |
| `Navigation.astro` | Fixed nav with logo, status badge, links, contact CTA |
| `Footer.astro` | Replace existing - contact CTA, social links, copyright |
| `CustomCursor.tsx` | React island for cursor dot + outline with mousemove tracking |
### Section Components (new files in `src/components/sections/`)
| Component | Purpose |
|-----------|---------|
| `Hero.astro` | Hero with title, subtitle, description, clock |
| `Experience.astro` | Timeline with Biohazard VFX, Stinkfilms, Freelance |
| `FeaturedProject.astro` | G-Star Raw Olympics full-width showcase |
| `Skills.astro` | Technical arsenal 4-column grid |
## Phase 3: Layouts
### BaseLayout.astro (new)
Create shared layout wrapping:
- BaseHead, GridOverlay, Navigation, main slot, Footer, CustomCursor island
- Body classes: `antialiased selection:bg-brand-accent selection:text-brand-dark`
### BlogPost.astro
Restyle to match V7 dark theme while keeping content structure.
## Phase 4: Pages
### index.astro
Compose from section components:
```astro
<BaseLayout>
<Hero />
<Divider />
<Experience />
<Divider />
<FeaturedProject />
<Skills />
</BaseLayout>
```
### blog/index.astro
Restyle post grid with dark theme, card hover effects, accent colors.
### blog/[...slug].astro
Uses updated BlogPost layout (no changes needed to routing logic).
### Delete about.astro
Remove the about page entirely.
## Phase 5: Assets
- Add favicon matching brand identity to `public/`
- Keep existing blog placeholder images
- Fonts served via Google Fonts CDN (no local files needed)
## Key Files to Modify
- `src/styles/global.css` - Tailwind config + custom CSS
- `src/consts.ts` - Site metadata
- `src/components/BaseHead.astro` - Font/icon loading
- `src/components/Footer.astro` - Complete rewrite
- `src/layouts/BlogPost.astro` - Restyle for dark theme
## Key Files to Create
- `src/components/GridOverlay.astro`
- `src/components/Navigation.astro`
- `src/components/CustomCursor.tsx`
- `src/components/sections/Hero.astro`
- `src/components/sections/Experience.astro`
- `src/components/sections/FeaturedProject.astro`
- `src/components/sections/Skills.astro`
- `src/layouts/BaseLayout.astro`