diff --git a/.gitignore b/.gitignore
index 89df803..10e6c59 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@ pnpm-debug.log*
# environment variables
.env
.env.production
+src/utils/.env
# macOS-specific files
.DS_Store
diff --git a/package.json b/package.json
index ed96874..825b20e 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,8 @@
"convert:avif": "node src/utils/convert-to-avif.js",
"convert:avif:all": "node src/utils/convert-to-avif.js --all",
"convert:avif:jpeg": "node src/utils/convert-to-avif.js --jpeg",
- "convert:avif:png": "node src/utils/convert-to-avif.js --png"
+ "convert:avif:png": "node src/utils/convert-to-avif.js --png",
+ "commit": "node src/utils/git-commit.js"
},
"dependencies": {
"@astrojs/cloudflare": "^12.6.12",
diff --git a/src/assets/blog-placeholder-1.avif b/src/assets/blog-placeholder-1.avif
deleted file mode 100644
index 9480c0e..0000000
Binary files a/src/assets/blog-placeholder-1.avif and /dev/null differ
diff --git a/src/assets/blog-placeholder-1.jpg b/src/assets/blog-placeholder-1.jpg
deleted file mode 100644
index 74d4009..0000000
Binary files a/src/assets/blog-placeholder-1.jpg and /dev/null differ
diff --git a/src/assets/blog-placeholder-2.avif b/src/assets/blog-placeholder-2.avif
deleted file mode 100644
index 73f3fc9..0000000
Binary files a/src/assets/blog-placeholder-2.avif and /dev/null differ
diff --git a/src/assets/blog-placeholder-2.jpg b/src/assets/blog-placeholder-2.jpg
deleted file mode 100644
index c4214b0..0000000
Binary files a/src/assets/blog-placeholder-2.jpg and /dev/null differ
diff --git a/src/assets/blog-placeholder-3.avif b/src/assets/blog-placeholder-3.avif
deleted file mode 100644
index 5ff93a6..0000000
Binary files a/src/assets/blog-placeholder-3.avif and /dev/null differ
diff --git a/src/assets/blog-placeholder-3.jpg b/src/assets/blog-placeholder-3.jpg
deleted file mode 100644
index fbe2ac0..0000000
Binary files a/src/assets/blog-placeholder-3.jpg and /dev/null differ
diff --git a/src/assets/blog-placeholder-4.avif b/src/assets/blog-placeholder-4.avif
deleted file mode 100644
index 8059858..0000000
Binary files a/src/assets/blog-placeholder-4.avif and /dev/null differ
diff --git a/src/assets/blog-placeholder-4.jpg b/src/assets/blog-placeholder-4.jpg
deleted file mode 100644
index f4fc88e..0000000
Binary files a/src/assets/blog-placeholder-4.jpg and /dev/null differ
diff --git a/src/assets/blog-placeholder-5.avif b/src/assets/blog-placeholder-5.avif
deleted file mode 100644
index a1ed108..0000000
Binary files a/src/assets/blog-placeholder-5.avif and /dev/null differ
diff --git a/src/assets/blog-placeholder-5.jpg b/src/assets/blog-placeholder-5.jpg
deleted file mode 100644
index c564674..0000000
Binary files a/src/assets/blog-placeholder-5.jpg and /dev/null differ
diff --git a/src/assets/blog-placeholder-about.avif b/src/assets/blog-placeholder-about.avif
deleted file mode 100644
index a8d119b..0000000
Binary files a/src/assets/blog-placeholder-about.avif and /dev/null differ
diff --git a/src/assets/blog-placeholder-about.jpg b/src/assets/blog-placeholder-about.jpg
deleted file mode 100644
index cf5f685..0000000
Binary files a/src/assets/blog-placeholder-about.jpg and /dev/null differ
diff --git a/src/assets/claude-nuke.png b/src/assets/claude-nuke.png
deleted file mode 100644
index 4d8913e..0000000
Binary files a/src/assets/claude-nuke.png and /dev/null differ
diff --git a/src/assets/foxrenderfarm-arch-linux.png b/src/assets/foxrenderfarm-arch-linux.png
deleted file mode 100644
index f3e9280..0000000
Binary files a/src/assets/foxrenderfarm-arch-linux.png and /dev/null differ
diff --git a/src/assets/g-star-image.jpg b/src/assets/g-star-image.jpg
deleted file mode 100644
index de9ed3b..0000000
Binary files a/src/assets/g-star-image.jpg and /dev/null differ
diff --git a/src/assets/nicholai-closeup-portrait.JPEG b/src/assets/nicholai-closeup-portrait.JPEG
deleted file mode 100644
index c9b0a61..0000000
Binary files a/src/assets/nicholai-closeup-portrait.JPEG and /dev/null differ
diff --git a/src/assets/nicholai-medium-portrait.jpg b/src/assets/nicholai-medium-portrait.jpg
deleted file mode 100644
index 60c4d17..0000000
Binary files a/src/assets/nicholai-medium-portrait.jpg and /dev/null differ
diff --git a/src/components/BaseHead.astro b/src/components/BaseHead.astro
index d143b4e..f39db61 100644
--- a/src/components/BaseHead.astro
+++ b/src/components/BaseHead.astro
@@ -3,7 +3,7 @@
// all pages through the use of the component.
import '../styles/global.css';
import type { ImageMetadata } from 'astro';
-import DefaultOGImage from '../assets/nicholai-medium-portrait.jpg';
+import DefaultOGImage from '../assets/nicholai-medium-portrait.avif';
import { SITE_TITLE, SITE_DESCRIPTION, SOCIAL_LINKS } from '../consts';
interface Props {
diff --git a/src/components/sections/Hero.astro b/src/components/sections/Hero.astro
index 0bf2b27..63997f1 100644
--- a/src/components/sections/Hero.astro
+++ b/src/components/sections/Hero.astro
@@ -1,6 +1,6 @@
---
import { Picture } from 'astro:assets';
-import heroPortrait from '../../assets/nicholai-closeup-portrait.JPEG';
+import heroPortrait from '../../assets/nicholai-closeup-portrait.avif';
interface Props {
headlineLine1: string;
diff --git a/src/utils/.env.example b/src/utils/.env.example
new file mode 100644
index 0000000..2774e89
--- /dev/null
+++ b/src/utils/.env.example
@@ -0,0 +1,4 @@
+# OpenRouter API Configuration
+# Get your API key from: https://openrouter.ai/keys
+
+OPENROUTER_API_KEY=your_api_key_here
diff --git a/src/utils/README.md b/src/utils/README.md
index a73bf3d..bd1b551 100644
--- a/src/utils/README.md
+++ b/src/utils/README.md
@@ -2,6 +2,93 @@
This directory contains utility scripts for the project.
+## Git Commit Automation Script
+
+### git-commit.js
+
+Automatically generates commit messages using OpenRouter AI (inception/mercury-coder) based on your staged changes. The script analyzes both the git diff and status to create meaningful commit messages, then allows you to review, edit, and approve before committing.
+
+**Prerequisites:**
+- OpenRouter API key (free to get started)
+ - Sign up: [openrouter.ai](https://openrouter.ai)
+ - Get your API key: [openrouter.ai/keys](https://openrouter.ai/keys)
+- Create a `.env` file in `src/utils/` directory:
+ ```bash
+ # Copy the example file
+ cp src/utils/.env.example src/utils/.env
+
+ # Edit the file and add your API key
+ OPENROUTER_API_KEY=your_actual_api_key_here
+ ```
+
+**Usage:**
+
+```bash
+# 1. Stage your changes
+git add
+
+# 2. Run the commit script
+pnpm commit
+
+# 3. Review the AI-generated message
+# 4. Choose to [A]ccept, [E]dit, or [C]ancel
+# 5. Optionally push to remote
+```
+
+**Workflow:**
+1. **Staged Files Check** - Verifies you have changes staged
+2. **Context Gathering** - Collects git diff and status
+3. **AI Generation** - Calls OpenRouter API (inception/mercury-coder model) to generate commit message
+4. **Review & Edit** - Shows message and prompts for approval
+5. **Commit** - Creates the commit with approved message
+6. **Optional Push** - Asks if you want to push to remote
+
+**Features:**
+- AI-powered commit message generation
+- Interactive approval process
+- Message editing capability
+- Optional automatic push
+- Follows project commit message conventions
+- Detailed error messages and help text
+
+**Example session:**
+```
+🚀 Git Commit Automation
+
+🔍 Gathering git context...
+🤖 Generating commit message with OpenRouter...
+
+📝 Generated commit message:
+────────────────────────────────────────────────────────────
+Add git commit automation script with OpenRouter integration
+
+- Create interactive commit message generator using inception/mercury-coder
+- Support message editing and approval workflow
+- Add optional push to remote after commit
+────────────────────────────────────────────────────────────
+
+[A]ccept / [E]dit / [C]ancel? a
+
+📦 Creating commit...
+✅ Commit created successfully!
+
+Push to remote? [y/N] y
+🚀 Pushing to remote...
+✅ Pushed successfully!
+
+✨ Done!
+```
+
+**Options:**
+- `--help`, `-h` - Show help message
+
+**Troubleshooting:**
+- If you get ".env file not found" error, create `src/utils/.env` with your OpenRouter API key
+- Get your API key from: https://openrouter.ai/keys
+- If you get "401 Unauthorized", check that your API key is correct in the `.env` file
+- The script requires staged changes - use `git add` first
+- Make sure `.env` is git-ignored (already configured in `.gitignore`)
+
## Image Conversion Script
### convert-to-avif.js
diff --git a/src/utils/git-commit.js b/src/utils/git-commit.js
new file mode 100644
index 0000000..577ad3e
--- /dev/null
+++ b/src/utils/git-commit.js
@@ -0,0 +1,464 @@
+#!/usr/bin/env node
+
+/**
+ * Git Commit Automation Script
+ *
+ * Automatically generates commit messages using OpenRouter AI (inception/mercury-coder)
+ * based on staged changes. Supports message editing and optional pushing.
+ *
+ * Usage:
+ * 1. Stage your changes: git add
+ * 2. Run: pnpm commit
+ * 3. Review/edit the generated message
+ * 4. Approve and optionally push
+ */
+
+import { execSync, spawnSync } from 'child_process';
+import { createInterface } from 'readline';
+import { readFileSync, writeFileSync, unlinkSync } from 'fs';
+import { join, dirname } from 'path';
+import { fileURLToPath } from 'url';
+import { tmpdir } from 'os';
+
+// Get current directory for ES modules
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+// Load environment variables from .env file
+function loadEnv() {
+ try {
+ const envPath = join(__dirname, '.env');
+ const envContent = readFileSync(envPath, 'utf-8');
+ const lines = envContent.split('\n');
+
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (!trimmed || trimmed.startsWith('#')) continue;
+
+ const [key, ...valueParts] = trimmed.split('=');
+ const value = valueParts.join('=').trim();
+
+ if (key && value) {
+ process.env[key.trim()] = value.replace(/^["']|["']$/g, '');
+ }
+ }
+ } catch (error) {
+ console.error(`${colors.red}❌ Failed to load .env file${colors.reset}`);
+ console.error(`${colors.yellow}💡 Create a .env file in src/utils/ with:${colors.reset}`);
+ console.error(` ${colors.dim}OPENROUTER_API_KEY=your_api_key_here${colors.reset}\n`);
+ process.exit(1);
+ }
+}
+
+// Configuration
+const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1';
+const MODEL_NAME = 'inception/mercury-coder';
+
+// Color codes for terminal output
+const colors = {
+ reset: '\x1b[0m',
+ bright: '\x1b[1m',
+ dim: '\x1b[2m',
+ red: '\x1b[31m',
+ green: '\x1b[32m',
+ yellow: '\x1b[33m',
+ blue: '\x1b[34m',
+ cyan: '\x1b[36m',
+};
+
+/**
+ * Execute a git command and return the output
+ */
+function git(command, silent = false) {
+ try {
+ return execSync(`git ${command}`, {
+ encoding: 'utf-8',
+ stdio: silent ? 'pipe' : ['pipe', 'pipe', 'pipe']
+ }).trim();
+ } catch (error) {
+ if (!silent) {
+ console.error(`${colors.red}❌ Git command failed: ${command}${colors.reset}`);
+ console.error(error.message);
+ }
+ return null;
+ }
+}
+
+/**
+ * Check if there are staged changes
+ */
+function checkStagedChanges() {
+ const stagedFiles = git('diff --staged --name-only', true);
+ return stagedFiles && stagedFiles.length > 0;
+}
+
+/**
+ * Get git context for AI commit message generation
+ */
+function getGitContext() {
+ console.log(`${colors.cyan}🔍 Gathering git context...${colors.reset}`);
+
+ const status = git('status --short');
+ const diff = git('diff --staged');
+ const stagedFiles = git('diff --staged --name-only');
+
+ return {
+ status,
+ diff,
+ stagedFiles
+ };
+}
+
+/**
+ * Call OpenRouter API to generate commit message
+ */
+async function generateCommitMessage(context) {
+ console.log(`${colors.cyan}🤖 Generating commit message with OpenRouter...${colors.reset}`);
+
+ const systemPrompt = `You are a helpful assistant that generates concise, clear git commit messages.
+
+Generate commit messages following these guidelines:
+- Use imperative mood (e.g., "Add", "Fix", "Update", "Refactor")
+- Keep it concise but descriptive
+- First line should be a short summary (50-72 characters)
+- If needed, add a blank line and then bullet points for details
+- Focus on WHAT changed and WHY, not HOW
+
+Generate ONLY the commit message, nothing else. Do not include any explanations or meta-commentary.`;
+
+ const userPrompt = `Based on the following git changes, generate a commit message:
+
+Staged files:
+${context.stagedFiles}
+
+Git status:
+${context.status}
+
+Git diff:
+${context.diff.slice(0, 8000)}${context.diff.length > 8000 ? '\n... (diff truncated)' : ''}`;
+
+ try {
+ const apiKey = process.env.OPENROUTER_API_KEY;
+
+ if (!apiKey) {
+ throw new Error('OPENROUTER_API_KEY not found in environment variables');
+ }
+
+ const response = await fetch(`${OPENROUTER_API_URL}/chat/completions`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${apiKey}`,
+ 'HTTP-Referer': 'https://github.com/yourusername/git-commit-automation',
+ 'X-Title': 'Git Commit Automation',
+ },
+ body: JSON.stringify({
+ model: MODEL_NAME,
+ messages: [
+ { role: 'system', content: systemPrompt },
+ { role: 'user', content: userPrompt }
+ ],
+ }),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ throw new Error(`OpenRouter API error: ${response.status} ${response.statusText}\n${JSON.stringify(errorData, null, 2)}`);
+ }
+
+ const data = await response.json();
+
+ if (!data.choices || !data.choices[0] || !data.choices[0].message) {
+ throw new Error('Unexpected API response format');
+ }
+
+ return data.choices[0].message.content.trim();
+ } catch (error) {
+ console.error(`${colors.red}❌ Failed to generate commit message${colors.reset}`);
+ console.error(error.message);
+
+ // Check for common errors
+ if (error.message.includes('OPENROUTER_API_KEY not found')) {
+ console.log(`\n${colors.yellow}💡 Make sure you have a .env file in src/utils/ with:${colors.reset}`);
+ console.log(` ${colors.dim}OPENROUTER_API_KEY=your_api_key_here${colors.reset}`);
+ console.log(`\n${colors.yellow}💡 Get your API key from:${colors.reset}`);
+ console.log(` ${colors.dim}https://openrouter.ai/keys${colors.reset}`);
+ } else if (error.message.includes('ECONNREFUSED') || error.message.includes('fetch failed')) {
+ console.log(`\n${colors.yellow}💡 Check your internet connection${colors.reset}`);
+ } else if (error.message.includes('401')) {
+ console.log(`\n${colors.yellow}💡 Invalid API key. Check your OPENROUTER_API_KEY in .env${colors.reset}`);
+ }
+
+ process.exit(1);
+ }
+}
+
+/**
+ * Create readline interface for user input
+ */
+function createReadlineInterface() {
+ return createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ });
+}
+
+/**
+ * Ask user a question and get input
+ */
+function question(rl, query) {
+ return new Promise((resolve) => {
+ rl.question(query, resolve);
+ });
+}
+
+/**
+ * Open neovim to edit the commit message
+ */
+function editInNeovim(message) {
+ // Create a temporary file for editing
+ const tempFile = join(tmpdir(), `git-commit-${Date.now()}.txt`);
+
+ try {
+ // Write the current message to the temp file
+ writeFileSync(tempFile, message, 'utf-8');
+
+ console.log(`\n${colors.cyan}✏️ Opening neovim to edit commit message...${colors.reset}`);
+
+ // Open neovim with the temp file
+ const result = spawnSync('nvim', [tempFile], {
+ stdio: 'inherit',
+ shell: false
+ });
+
+ if (result.error) {
+ throw new Error(`Failed to open neovim: ${result.error.message}`);
+ }
+
+ // Read the edited content
+ const editedMessage = readFileSync(tempFile, 'utf-8').trim();
+
+ // Clean up temp file
+ unlinkSync(tempFile);
+
+ return editedMessage;
+ } catch (error) {
+ // Clean up temp file if it exists
+ try {
+ unlinkSync(tempFile);
+ } catch {}
+
+ console.error(`${colors.red}❌ Failed to edit in neovim${colors.reset}`);
+ console.error(error.message);
+
+ if (error.message.includes('Failed to open neovim')) {
+ console.log(`\n${colors.yellow}💡 Make sure neovim is installed:${colors.reset}`);
+ console.log(` ${colors.dim}# Arch Linux${colors.reset}`);
+ console.log(` ${colors.dim}sudo pacman -S neovim${colors.reset}`);
+ }
+
+ // Return the original message if editing fails
+ return message;
+ }
+}
+
+/**
+ * Display the commit message and get user approval
+ */
+async function getUserApproval(message, rl) {
+ console.log(`\n${colors.bright}${colors.green}📝 Generated commit message:${colors.reset}`);
+ console.log(`${colors.dim}${'─'.repeat(60)}${colors.reset}`);
+ console.log(message);
+ console.log(`${colors.dim}${'─'.repeat(60)}${colors.reset}\n`);
+
+ while (true) {
+ const answer = await question(
+ rl,
+ `${colors.yellow}[A]ccept / [E]dit / [C]ancel?${colors.reset} `
+ );
+
+ const choice = answer.trim().toLowerCase();
+
+ if (choice === 'a' || choice === 'accept') {
+ return { approved: true, message };
+ } else if (choice === 'e' || choice === 'edit') {
+ // Close readline to give full control to neovim
+ rl.pause();
+
+ // Open neovim for editing
+ const editedMessage = editInNeovim(message);
+
+ // Resume readline
+ rl.resume();
+
+ // Show the edited message and ask for approval again
+ return getUserApproval(editedMessage, rl);
+ } else if (choice === 'c' || choice === 'cancel') {
+ return { approved: false, message: null };
+ } else {
+ console.log(`${colors.red}Invalid option. Please enter A, E, or C.${colors.reset}`);
+ }
+ }
+}
+
+/**
+ * Create the commit with the approved message
+ */
+function createCommit(message) {
+ console.log(`\n${colors.cyan}📦 Creating commit...${colors.reset}`);
+
+ try {
+ // Use a temporary file for the commit message to handle multi-line messages
+ execSync(`git commit -F -`, {
+ input: message,
+ encoding: 'utf-8',
+ stdio: ['pipe', 'inherit', 'inherit']
+ });
+
+ console.log(`${colors.green}✅ Commit created successfully!${colors.reset}`);
+ return true;
+ } catch (error) {
+ console.error(`${colors.red}❌ Failed to create commit${colors.reset}`);
+ console.error(error.message);
+ return false;
+ }
+}
+
+/**
+ * Ask if user wants to push to remote
+ */
+async function askToPush(rl) {
+ const answer = await question(
+ rl,
+ `\n${colors.yellow}Push to remote? [y/N]${colors.reset} `
+ );
+
+ return answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes';
+}
+
+/**
+ * Push to remote repository
+ */
+function pushToRemote() {
+ console.log(`${colors.cyan}🚀 Pushing to remote...${colors.reset}`);
+
+ try {
+ // Get current branch
+ const branch = git('rev-parse --abbrev-ref HEAD');
+
+ execSync(`git push origin ${branch}`, {
+ encoding: 'utf-8',
+ stdio: 'inherit'
+ });
+
+ console.log(`${colors.green}✅ Pushed successfully!${colors.reset}`);
+ return true;
+ } catch (error) {
+ console.error(`${colors.red}❌ Failed to push${colors.reset}`);
+ console.error(error.message);
+ return false;
+ }
+}
+
+/**
+ * Show help message
+ */
+function showHelp() {
+ console.log(`
+${colors.bright}Git Commit Automation Script${colors.reset}
+${colors.dim}Generates commit messages using OpenRouter AI${colors.reset}
+
+${colors.bright}Usage:${colors.reset}
+ 1. Stage your changes:
+ ${colors.cyan}git add ${colors.reset}
+
+ 2. Run this script:
+ ${colors.cyan}pnpm commit${colors.reset}
+
+ 3. Review the AI-generated commit message
+
+ 4. Choose to accept, edit, or cancel
+
+ 5. Optionally push to remote
+
+${colors.bright}Requirements:${colors.reset}
+ - OpenRouter API key in .env file
+ - Create ${colors.dim}src/utils/.env${colors.reset} with:
+ ${colors.dim}OPENROUTER_API_KEY=your_api_key_here${colors.reset}
+ - Get your key from: ${colors.dim}https://openrouter.ai/keys${colors.reset}
+
+${colors.bright}Options:${colors.reset}
+ --help, -h Show this help message
+`);
+}
+
+/**
+ * Main function
+ */
+async function main() {
+ // Check for help flag
+ const args = process.argv.slice(2);
+ if (args.includes('--help') || args.includes('-h')) {
+ showHelp();
+ process.exit(0);
+ }
+
+ // Load environment variables
+ loadEnv();
+
+ console.log(`${colors.bright}${colors.blue}🚀 Git Commit Automation${colors.reset}\n`);
+
+ // Check if we're in a git repository
+ if (!git('rev-parse --git-dir', true)) {
+ console.error(`${colors.red}❌ Not a git repository${colors.reset}`);
+ process.exit(1);
+ }
+
+ // Check for staged changes
+ if (!checkStagedChanges()) {
+ console.error(`${colors.red}❌ No staged changes found${colors.reset}`);
+ console.log(`\n${colors.yellow}💡 Stage your changes first:${colors.reset}`);
+ console.log(` ${colors.dim}git add ${colors.reset}\n`);
+ process.exit(1);
+ }
+
+ // Get git context
+ const context = getGitContext();
+
+ // Generate commit message using OpenRouter
+ const generatedMessage = await generateCommitMessage(context);
+
+ // Get user approval
+ const rl = createReadlineInterface();
+ const { approved, message } = await getUserApproval(generatedMessage, rl);
+
+ if (!approved) {
+ console.log(`\n${colors.yellow}⏭️ Commit cancelled${colors.reset}`);
+ rl.close();
+ process.exit(0);
+ }
+
+ // Create the commit
+ const commitSuccess = createCommit(message);
+
+ if (!commitSuccess) {
+ rl.close();
+ process.exit(1);
+ }
+
+ // Ask to push
+ const shouldPush = await askToPush(rl);
+ rl.close();
+
+ if (shouldPush) {
+ pushToRemote();
+ }
+
+ console.log(`\n${colors.green}✨ Done!${colors.reset}\n`);
+}
+
+// Run the script
+main().catch((error) => {
+ console.error(`${colors.red}❌ Unexpected error:${colors.reset}`, error);
+ process.exit(1);
+});