tangled
alpha
login
or
join now
nekomimi.pet
/
bunsite
0
fork
atom
personal website
0
fork
atom
overview
issues
pulls
pipelines
generate static vibes/index.html instead of SPA rendering
nekomimi.pet
3 weeks ago
bda4a968
c18ed544
1/1
deploy.yml
success
1m 14s
+54
-54
4 changed files
expand all
collapse all
unified
split
build.ts
src
App.tsx
components
sections
Vibes.tsx
index.ts
+46
-7
build.ts
reviewed
···
4
4
import { rm, cp, readdir, writeFile, unlink } from "fs/promises";
5
5
import path from "path";
6
6
7
7
+
export function buildVibesHtml(files: string[]) {
8
8
+
const items = files.map((f) => {
9
9
+
const src = `/vibes/${f}`
10
10
+
if (/\.(mp4|mov|webm)$/i.test(f))
11
11
+
return `<video src="${src}" controls loop muted preload="none"></video>`
12
12
+
return `<img src="${src}" alt="" loading="lazy">`
13
13
+
}).join("\n ")
14
14
+
15
15
+
return `<!DOCTYPE html>
16
16
+
<html lang="en">
17
17
+
<head>
18
18
+
<meta charset="UTF-8">
19
19
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
20
20
+
<title>vibes</title>
21
21
+
<style>
22
22
+
* { margin: 0; padding: 0; box-sizing: border-box; }
23
23
+
body { background: #1a1a1a; }
24
24
+
a.back {
25
25
+
position: fixed; top: 1.5rem; left: 1.5rem; z-index: 10;
26
26
+
padding: 0.4rem 1rem; border-radius: 999px;
27
27
+
background: #2a2a2a; color: #ccc; text-decoration: none;
28
28
+
font: 0.85rem monospace; border: 1px solid #444;
29
29
+
}
30
30
+
a.back:hover { background: #333; color: #fff; }
31
31
+
.gallery {
32
32
+
columns: 4 180px; column-gap: 6px;
33
33
+
padding: 5rem 1rem 2rem;
34
34
+
}
35
35
+
.gallery img, .gallery video {
36
36
+
width: 100%; height: auto; display: block; margin-bottom: 6px;
37
37
+
}
38
38
+
</style>
39
39
+
</head>
40
40
+
<body>
41
41
+
<a class="back" href="/">← back</a>
42
42
+
<div class="gallery">
43
43
+
${items}
44
44
+
</div>
45
45
+
</body>
46
46
+
</html>`
47
47
+
}
48
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
176
-
// Generate manifest from final dist/vibes contents
177
177
-
const finalFiles = await readdir(distVibesDir);
178
178
-
const manifest = finalFiles
179
179
-
.filter((f) => f !== "manifest.json")
180
180
-
.map((f) => `/vibes/${f}`);
181
181
-
await writeFile(path.join(distVibesDir, "manifest.json"), JSON.stringify(manifest));
182
182
-
console.log(`📦 Generated vibes/manifest.json with ${manifest.length} files`);
218
218
+
// Generate vibes/index.html
219
219
+
const finalFiles = (await readdir(distVibesDir)).filter((f) => f !== "index.html");
220
220
+
await writeFile(path.join(distVibesDir, "index.html"), buildVibesHtml(finalFiles));
221
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
reviewed
···
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
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
24
-
if (currentPath === '/guestbook' || currentPath === '/vibes') return // Skip observer on sub-pages
23
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
44
-
45
45
-
// Vibes page
46
46
-
if (currentPath === '/vibes') {
47
47
-
return (
48
48
-
<div className="min-h-screen dark:bg-background text-foreground relative">
49
49
-
<div className="fixed top-6 left-6 z-50">
50
50
-
<button
51
51
-
onClick={() => {
52
52
-
window.history.pushState({}, '', '/')
53
53
-
setCurrentPath('/')
54
54
-
}}
55
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
56
-
>
57
57
-
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
58
58
-
<path strokeLinecap="round" strokeLinejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
59
59
-
</svg>
60
60
-
Back
61
61
-
</button>
62
62
-
</div>
63
63
-
<Vibes />
64
64
-
</div>
65
65
-
)
66
66
-
}
67
43
68
44
// Guestbook page
69
45
if (currentPath === '/guestbook') {
-22
src/components/sections/Vibes.tsx
reviewed
···
1
1
-
import { useEffect, useState } from "react"
2
2
-
3
3
-
export function Vibes() {
4
4
-
const [vibes, setVibes] = useState<string[]>([])
5
5
-
6
6
-
useEffect(() => {
7
7
-
fetch("/vibes/manifest.json")
8
8
-
.then((r) => r.json())
9
9
-
.then(setVibes)
10
10
-
.catch(() => {})
11
11
-
}, [])
12
12
-
13
13
-
return (
14
14
-
<div className="vibes-container">
15
15
-
{vibes.map((src) =>
16
16
-
/\.(mp4|mov|webm)$/i.test(src)
17
17
-
? <video key={src} src={src} controls loop muted preload="none" />
18
18
-
: <img key={src} src={src} alt="" loading="lazy" />
19
19
-
)}
20
20
-
</div>
21
21
-
)
22
22
-
}
+7
src/index.ts
reviewed
···
1
1
import { serve } from "bun";
2
2
+
import { readdir } from "fs/promises";
3
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
36
+
},
37
37
+
38
38
+
"/vibes": async () => {
39
39
+
const files = (await readdir("public/vibes")).filter((f) => f !== "index.html");
40
40
+
return new Response(buildVibesHtml(files), { headers: { "Content-Type": "text/html" } });
34
41
},
35
42
36
43
"/vibes/*": async (req) => {