personal website

generate static vibes/index.html instead of SPA rendering

+54 -54
+46 -7
build.ts
··· 4 4 import { rm, cp, readdir, writeFile, unlink } from "fs/promises"; 5 5 import path from "path"; 6 6 7 + export function buildVibesHtml(files: string[]) { 8 + const items = files.map((f) => { 9 + const src = `/vibes/${f}` 10 + if (/\.(mp4|mov|webm)$/i.test(f)) 11 + return `<video src="${src}" controls loop muted preload="none"></video>` 12 + return `<img src="${src}" alt="" loading="lazy">` 13 + }).join("\n ") 14 + 15 + return `<!DOCTYPE html> 16 + <html lang="en"> 17 + <head> 18 + <meta charset="UTF-8"> 19 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 20 + <title>vibes</title> 21 + <style> 22 + * { margin: 0; padding: 0; box-sizing: border-box; } 23 + body { background: #1a1a1a; } 24 + a.back { 25 + position: fixed; top: 1.5rem; left: 1.5rem; z-index: 10; 26 + padding: 0.4rem 1rem; border-radius: 999px; 27 + background: #2a2a2a; color: #ccc; text-decoration: none; 28 + font: 0.85rem monospace; border: 1px solid #444; 29 + } 30 + a.back:hover { background: #333; color: #fff; } 31 + .gallery { 32 + columns: 4 180px; column-gap: 6px; 33 + padding: 5rem 1rem 2rem; 34 + } 35 + .gallery img, .gallery video { 36 + width: 100%; height: auto; display: block; margin-bottom: 6px; 37 + } 38 + </style> 39 + </head> 40 + <body> 41 + <a class="back" href="/">← back</a> 42 + <div class="gallery"> 43 + ${items} 44 + </div> 45 + </body> 46 + </html>` 47 + } 48 + 7 49 if (process.argv.includes("--help") || process.argv.includes("-h")) { 8 50 console.log(` 9 51 🏗️ Bun Build Script ··· 173 215 console.log("✅ WebP conversion done"); 174 216 } 175 217 176 - // Generate manifest from final dist/vibes contents 177 - const finalFiles = await readdir(distVibesDir); 178 - const manifest = finalFiles 179 - .filter((f) => f !== "manifest.json") 180 - .map((f) => `/vibes/${f}`); 181 - await writeFile(path.join(distVibesDir, "manifest.json"), JSON.stringify(manifest)); 182 - console.log(`📦 Generated vibes/manifest.json with ${manifest.length} files`); 218 + // Generate vibes/index.html 219 + const finalFiles = (await readdir(distVibesDir)).filter((f) => f !== "index.html"); 220 + await writeFile(path.join(distVibesDir, "index.html"), buildVibesHtml(finalFiles)); 221 + console.log(`📦 Generated vibes/index.html with ${finalFiles.length} files`); 183 222 } 184 223 185 224 console.log(`\n✅ Build completed in ${buildTime}ms\n`);
+1 -25
src/App.tsx
··· 5 5 import { Work } from "./components/sections/Work" 6 6 import { Connect } from "./components/sections/Connect" 7 7 import { GuestbookPage } from "./components/sections/GuestbookPage" 8 - import { Vibes } from "./components/sections/Vibes" 9 8 import { sections } from "./data/portfolio" 10 9 11 10 export function App() { ··· 21 20 }, []) 22 21 23 22 useEffect(() => { 24 - if (currentPath === '/guestbook' || currentPath === '/vibes') return // Skip observer on sub-pages 23 + if (currentPath === '/guestbook') return // Skip observer on sub-pages 25 24 26 25 const observer = new IntersectionObserver( 27 26 (entries) => { ··· 41 40 42 41 return () => observer.disconnect() 43 42 }, [currentPath]) 44 - 45 - // Vibes page 46 - if (currentPath === '/vibes') { 47 - return ( 48 - <div className="min-h-screen dark:bg-background text-foreground relative"> 49 - <div className="fixed top-6 left-6 z-50"> 50 - <button 51 - onClick={() => { 52 - window.history.pushState({}, '', '/') 53 - setCurrentPath('/') 54 - }} 55 - className="px-4 py-2 rounded-full text-sm font-medium bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 shadow-md hover:shadow-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition-all flex items-center gap-2" 56 - > 57 - <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}> 58 - <path strokeLinecap="round" strokeLinejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" /> 59 - </svg> 60 - Back 61 - </button> 62 - </div> 63 - <Vibes /> 64 - </div> 65 - ) 66 - } 67 43 68 44 // Guestbook page 69 45 if (currentPath === '/guestbook') {
-22
src/components/sections/Vibes.tsx
··· 1 - import { useEffect, useState } from "react" 2 - 3 - export function Vibes() { 4 - const [vibes, setVibes] = useState<string[]>([]) 5 - 6 - useEffect(() => { 7 - fetch("/vibes/manifest.json") 8 - .then((r) => r.json()) 9 - .then(setVibes) 10 - .catch(() => {}) 11 - }, []) 12 - 13 - return ( 14 - <div className="vibes-container"> 15 - {vibes.map((src) => 16 - /\.(mp4|mov|webm)$/i.test(src) 17 - ? <video key={src} src={src} controls loop muted preload="none" /> 18 - : <img key={src} src={src} alt="" loading="lazy" /> 19 - )} 20 - </div> 21 - ) 22 - }
+7
src/index.ts
··· 1 1 import { serve } from "bun"; 2 + import { readdir } from "fs/promises"; 3 + import { buildVibesHtml } from "../build"; 2 4 import index from "./index.html"; 3 5 4 6 const server = serve({ ··· 31 33 } catch { 32 34 return new Response("File not found", { status: 404 }); 33 35 } 36 + }, 37 + 38 + "/vibes": async () => { 39 + const files = (await readdir("public/vibes")).filter((f) => f !== "index.html"); 40 + return new Response(buildVibesHtml(files), { headers: { "Content-Type": "text/html" } }); 34 41 }, 35 42 36 43 "/vibes/*": async (req) => {