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:
parent
6a2780f9b0
commit
50f9a2df68
11
CLAUDE.md
11
CLAUDE.md
@ -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`
|
||||
|
||||
@ -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
@ -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
|
||||
@ -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": [
|
||||
|
||||
603
dev/index.html
603
dev/index.html
@ -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>
|
||||
@ -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-Carr’s 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.
|
||||
125
dev/plan.md
125
dev/plan.md
@ -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`
|
||||
Loading…
x
Reference in New Issue
Block a user