Add Drake vibe page and project scaffold #4

Merged
ldraney merged 2 commits from 2-drake-vibe-page into main 2026-06-19 02:04:53 +00:00
Owner

Summary

  • First vibe page: "music is self expression. music is hypnotizing. That is why I like Drake."
  • Full project scaffold: Vite build, Tone.js, Spotify embed, Docker/nginx, Woodpecker CI
  • Dark moody aesthetic with Playfair Display serif, staggered line reveals, ambient drone on tap

Changes

  • package.json / vite.config.js: Vite 6 + Tone.js, multi-page auto-discovery of vibe directories
  • src/drake/index.html: Page with phrase text, Spotify embed (God's Plan), OG meta tags
  • src/drake/style.css: Dark background with purple radial gradients, Playfair Display, CSS reveal animations
  • src/drake/main.js: Tone.js ambient — low sine oscillator with reverb + LFO filter sweep, triggered on tap
  • Dockerfile: Two-stage build (node for Vite, nginx:alpine for serving)
  • .woodpecker.yml: Kaniko build to Harbor on push to main
  • nginx.conf: Static serving with try_files
  • .gitignore: node_modules, dist

Test Plan

  • npm run build succeeds — produces dist/drake/index.html
  • npm run dev serves page at /drake/ with correct HTML
  • Visual check: text reveals, Spotify embed loads, Tone.js plays on tap
  • Docker build succeeds locally

Review Checklist

  • Passed automated review-fix loop
  • No secrets committed
  • No unnecessary file changes
  • Commit messages are descriptive
  • Feature flag needed? No — public creative page
  • Closes #3
  • my-vibes-world — project this work belongs to
## Summary - First vibe page: "music is self expression. music is hypnotizing. That is why I like Drake." - Full project scaffold: Vite build, Tone.js, Spotify embed, Docker/nginx, Woodpecker CI - Dark moody aesthetic with Playfair Display serif, staggered line reveals, ambient drone on tap ## Changes - `package.json` / `vite.config.js`: Vite 6 + Tone.js, multi-page auto-discovery of vibe directories - `src/drake/index.html`: Page with phrase text, Spotify embed (God's Plan), OG meta tags - `src/drake/style.css`: Dark background with purple radial gradients, Playfair Display, CSS reveal animations - `src/drake/main.js`: Tone.js ambient — low sine oscillator with reverb + LFO filter sweep, triggered on tap - `Dockerfile`: Two-stage build (node for Vite, nginx:alpine for serving) - `.woodpecker.yml`: Kaniko build to Harbor on push to main - `nginx.conf`: Static serving with try_files - `.gitignore`: node_modules, dist ## Test Plan - [x] `npm run build` succeeds — produces dist/drake/index.html - [x] `npm run dev` serves page at /drake/ with correct HTML - [ ] Visual check: text reveals, Spotify embed loads, Tone.js plays on tap - [ ] Docker build succeeds locally ## Review Checklist - [ ] Passed automated review-fix loop - [ ] No secrets committed - [ ] No unnecessary file changes - [ ] Commit messages are descriptive - [ ] Feature flag needed? No — public creative page ## Related Notes - Closes #3 - `my-vibes-world` — project this work belongs to
First vibe page: "music is self expression. music is hypnotizing.
That is why I like Drake."

- Vite multi-page build with auto-discovery of vibe directories
- Tone.js ambient drone (low sine + LFO filter sweep) on tap
- Spotify embed for God's Plan
- Dark moody aesthetic with Playfair Display, staggered line reveals
- nginx + Docker for production serving
- Woodpecker CI pipeline (Kaniko build to Harbor)
- OpenGraph meta for Instagram link previews

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Owner

PR #4 Review

DOMAIN REVIEW

Tech stack identified: Vanilla JS + Vite 6 + Tone.js, HTML/CSS, Docker (multi-stage), nginx, Woodpecker CI (Kaniko). 10 new files, 1449 additions, 0 deletions.


vite.config.js -- Auto-discovery logic has redundant dead code

getVibeEntries() contains a double-check pattern that is confusing:

if (statSync(dir).isDirectory() && statSync(html, { throwIfNoEntry: false })) {
  try {
    statSync(html)       // <-- redundant: we already confirmed html exists above
    entries[name] = html
  } catch {}             // <-- this catch can never fire
}

The statSync(html, { throwIfNoEntry: false }) on line 11 returns undefined (falsy) if the file doesn't exist, so the if body is only entered when the file exists. The inner try { statSync(html) } catch {} is therefore dead code -- it re-checks existence of a file we already know exists. Either remove the inner try/catch (relying on the condition) or remove the throwIfNoEntry from the condition and rely solely on the try/catch. Pick one pattern.

Additionally, statSync with { throwIfNoEntry: false } requires Node >= 15.3. The Dockerfile uses node:22-alpine so this is fine today, but if someone runs this on an older Node, it will throw instead of returning undefined. Consider fs.existsSync() for clarity.

src/drake/main.js -- Tone.js audio nodes are never disposed (memory leak)

The startAmbient() function creates a Reverb, Filter, Synth, and LFO but stores none of them in module-scope variables and provides no cleanup path. For a single-page creative site that never navigates away, this is tolerable -- the nodes live for the page lifetime. However:

  1. If this page is ever embedded in an SPA or if vibe pages get client-side routing in the future, these nodes will leak.
  2. synth.triggerAttack('C2') starts a note that plays indefinitely (sustain: 1, no release triggered). There is no triggerRelease() call and no way for the user to stop the audio once started.

Recommendation: Store nodes in module scope and add a stopAmbient() function that calls .dispose() on each node. Even if not wired up now, it prepares for future use. Not a blocker for an MVP creative page.

src/drake/main.js -- No error handling on Tone.start()

Tone.start() can throw if the AudioContext fails to resume (e.g., browser policy blocks it). The await Tone.start() is unguarded -- if it rejects, started is already true so the user cannot retry by tapping again. The flag should be set AFTER Tone.start() succeeds, or the function should catch and reset:

async function startAmbient() {
  if (started) return
  document.getElementById('tapHint')?.classList.add('hidden')
  try {
    await Tone.start()
    started = true   // <-- set only after success
    // ... create nodes
  } catch (e) {
    console.error('Audio failed to start:', e)
    // user can tap again
  }
}

src/drake/index.html -- OG meta tags

OG tags look correct for Instagram link previews: og:title, og:description, og:type, og:url are all present. Missing og:image -- Instagram and other social platforms will show a blank preview without a share image. This is a creative site so you may want to add one later, but it is not a blocker.

Also worth noting: <title> is generic "myvibes.world" rather than page-specific. Consider "Drake | myvibes.world" for better tab identification when multiple vibe pages exist.

src/drake/style.css -- Good responsive design

The CSS is clean. clamp() for font sizing, 100dvh for dynamic viewport height, mobile breakpoint at 480px. The radial gradient approach for the moody background is tasteful. overflow: hidden on body prevents scroll but also prevents the user from seeing any content that overflows on very small screens -- worth testing on 320px width devices.

The .tap-hint.hidden uses opacity: 0 !important which works but the element remains in the DOM and technically interactive. Since it is just a text hint with no click handler, this is fine.

Dockerfile -- Clean two-stage build

The Dockerfile is correctly structured: build stage with node:22-alpine, production stage with nginx:alpine. No unnecessary layers. npm ci is correct for reproducible builds. COPY . . in the build stage copies everything (including .git, node_modules if present locally) -- consider adding a .dockerignore file to exclude .git/, node_modules/, dist/, .woodpecker.yml, etc. This reduces build context size and prevents cache busting.

nginx.conf -- Minimal and correct

The config is correct for static serving. try_files $uri $uri/ =404 handles both files and directory index lookups. One consideration: no gzip compression is configured. For a static site with CSS/JS assets, adding gzip on; gzip_types text/css application/javascript; would improve load times. Not a blocker.

Also missing: cache headers for static assets. Vite generates hashed filenames so you could safely set aggressive caching.

.woodpecker.yml -- Follows platform pattern

The CI config triggers on push to main and manual events, uses Kaniko for building, pushes to Harbor. Secrets are referenced via from_secret (not hardcoded). The insecure: true and insecure-registry settings are for in-cluster Harbor access over HTTP, which is standard for internal registries.

One note: there is no step to lint, test, or validate the build before pushing. Currently the pipeline just builds and pushes. If npm run build fails inside the Dockerfile, Kaniko will fail the step, so this is implicitly covered, but explicit CI steps would give better error messages.

package.json -- Clean

Minimal dependencies. "private": true is correct. "type": "module" matches the ESM imports in main.js and vite.config.js.

Spotify embed -- Correct format

The iframe src https://open.spotify.com/embed/track/6DCZcSspjsKoFjzjrWoCdn?utm_source=generator&theme=0 is the correct Spotify embed format. Track ID 6DCZcSspjsKoFjzjrWoCdn corresponds to "God's Plan" by Drake. theme=0 is dark theme. The allow attribute includes the necessary permissions. loading="lazy" is correct.

BLOCKERS

None. This is an MVP creative page with no user input, no auth, no server-side logic, no secrets in code. The code quality issues identified are nits for this context.

NITS

  1. vite.config.js: Remove redundant inner try/catch in getVibeEntries() -- pick either the throwIfNoEntry pattern or the try/catch pattern, not both. (Code clarity)
  2. main.js: Set started = true AFTER Tone.start() succeeds, not before, so users can retry on failure. (Resilience)
  3. main.js: Consider storing Tone.js nodes in module scope and adding a cleanup function for future-proofing. (Maintainability)
  4. Dockerfile: Add a .dockerignore file (.git, node_modules, dist, .woodpecker.yml). (Build performance)
  5. nginx.conf: Add gzip compression for text/css and application/javascript. (Performance)
  6. nginx.conf: Add cache-control headers for hashed static assets. (Performance)
  7. index.html: Add og:image meta tag for richer social previews. (UX)
  8. index.html: Make <title> page-specific, e.g. "Drake | myvibes.world". (UX)
  9. style.css: Test overflow: hidden on very narrow viewports (320px) to ensure no content is clipped. (Accessibility)

SOP COMPLIANCE

  • PR body has: Summary, Changes, Test Plan, Related -- all present and detailed
  • No secrets committed -- Harbor creds are via from_secret, no hardcoded values
  • No unnecessary file changes -- all 10 files are relevant to the feature
  • Commit messages -- single PR commit, title is descriptive
  • package-lock.json is committed (correct for reproducible builds)

PROCESS OBSERVATIONS

  • First PR for the repo -- establishes the project scaffold. Good that CI, Docker, and serving are all in place from day one.
  • Test plan gaps -- two items are unchecked (visual check, Docker build). These are manual checks that should be completed before merge.
  • No automated tests -- acceptable for a static creative page with no logic beyond Tone.js audio. If the project grows to include dynamic features, a test harness should be added.
  • Deployment path -- Woodpecker builds and pushes to Harbor, but there is no k8s manifest, Helm chart, or ArgoCD config in this PR. Presumably handled separately, but worth confirming the deployment story is complete.

VERDICT: APPROVED

## PR #4 Review ### DOMAIN REVIEW **Tech stack identified:** Vanilla JS + Vite 6 + Tone.js, HTML/CSS, Docker (multi-stage), nginx, Woodpecker CI (Kaniko). 10 new files, 1449 additions, 0 deletions. --- **vite.config.js -- Auto-discovery logic has redundant dead code** `getVibeEntries()` contains a double-check pattern that is confusing: ```js if (statSync(dir).isDirectory() && statSync(html, { throwIfNoEntry: false })) { try { statSync(html) // <-- redundant: we already confirmed html exists above entries[name] = html } catch {} // <-- this catch can never fire } ``` The `statSync(html, { throwIfNoEntry: false })` on line 11 returns `undefined` (falsy) if the file doesn't exist, so the `if` body is only entered when the file exists. The inner `try { statSync(html) } catch {}` is therefore dead code -- it re-checks existence of a file we already know exists. Either remove the inner try/catch (relying on the condition) or remove the `throwIfNoEntry` from the condition and rely solely on the try/catch. Pick one pattern. Additionally, `statSync` with `{ throwIfNoEntry: false }` requires Node >= 15.3. The Dockerfile uses `node:22-alpine` so this is fine today, but if someone runs this on an older Node, it will throw instead of returning undefined. Consider `fs.existsSync()` for clarity. **src/drake/main.js -- Tone.js audio nodes are never disposed (memory leak)** The `startAmbient()` function creates a `Reverb`, `Filter`, `Synth`, and `LFO` but stores none of them in module-scope variables and provides no cleanup path. For a single-page creative site that never navigates away, this is tolerable -- the nodes live for the page lifetime. However: 1. If this page is ever embedded in an SPA or if vibe pages get client-side routing in the future, these nodes will leak. 2. `synth.triggerAttack('C2')` starts a note that plays indefinitely (sustain: 1, no release triggered). There is no `triggerRelease()` call and no way for the user to stop the audio once started. Recommendation: Store nodes in module scope and add a `stopAmbient()` function that calls `.dispose()` on each node. Even if not wired up now, it prepares for future use. Not a blocker for an MVP creative page. **src/drake/main.js -- No error handling on Tone.start()** `Tone.start()` can throw if the AudioContext fails to resume (e.g., browser policy blocks it). The `await Tone.start()` is unguarded -- if it rejects, `started` is already `true` so the user cannot retry by tapping again. The flag should be set AFTER `Tone.start()` succeeds, or the function should catch and reset: ```js async function startAmbient() { if (started) return document.getElementById('tapHint')?.classList.add('hidden') try { await Tone.start() started = true // <-- set only after success // ... create nodes } catch (e) { console.error('Audio failed to start:', e) // user can tap again } } ``` **src/drake/index.html -- OG meta tags** OG tags look correct for Instagram link previews: `og:title`, `og:description`, `og:type`, `og:url` are all present. Missing `og:image` -- Instagram and other social platforms will show a blank preview without a share image. This is a creative site so you may want to add one later, but it is not a blocker. Also worth noting: `<title>` is generic "myvibes.world" rather than page-specific. Consider "Drake | myvibes.world" for better tab identification when multiple vibe pages exist. **src/drake/style.css -- Good responsive design** The CSS is clean. `clamp()` for font sizing, `100dvh` for dynamic viewport height, mobile breakpoint at 480px. The radial gradient approach for the moody background is tasteful. `overflow: hidden` on body prevents scroll but also prevents the user from seeing any content that overflows on very small screens -- worth testing on 320px width devices. The `.tap-hint.hidden` uses `opacity: 0 !important` which works but the element remains in the DOM and technically interactive. Since it is just a text hint with no click handler, this is fine. **Dockerfile -- Clean two-stage build** The Dockerfile is correctly structured: build stage with `node:22-alpine`, production stage with `nginx:alpine`. No unnecessary layers. `npm ci` is correct for reproducible builds. `COPY . .` in the build stage copies everything (including `.git`, `node_modules` if present locally) -- consider adding a `.dockerignore` file to exclude `.git/`, `node_modules/`, `dist/`, `.woodpecker.yml`, etc. This reduces build context size and prevents cache busting. **nginx.conf -- Minimal and correct** The config is correct for static serving. `try_files $uri $uri/ =404` handles both files and directory index lookups. One consideration: no gzip compression is configured. For a static site with CSS/JS assets, adding `gzip on; gzip_types text/css application/javascript;` would improve load times. Not a blocker. Also missing: cache headers for static assets. Vite generates hashed filenames so you could safely set aggressive caching. **.woodpecker.yml -- Follows platform pattern** The CI config triggers on push to main and manual events, uses Kaniko for building, pushes to Harbor. Secrets are referenced via `from_secret` (not hardcoded). The `insecure: true` and `insecure-registry` settings are for in-cluster Harbor access over HTTP, which is standard for internal registries. One note: there is no step to lint, test, or validate the build before pushing. Currently the pipeline just builds and pushes. If `npm run build` fails inside the Dockerfile, Kaniko will fail the step, so this is implicitly covered, but explicit CI steps would give better error messages. **package.json -- Clean** Minimal dependencies. `"private": true` is correct. `"type": "module"` matches the ESM imports in `main.js` and `vite.config.js`. **Spotify embed -- Correct format** The iframe src `https://open.spotify.com/embed/track/6DCZcSspjsKoFjzjrWoCdn?utm_source=generator&theme=0` is the correct Spotify embed format. Track ID 6DCZcSspjsKoFjzjrWoCdn corresponds to "God's Plan" by Drake. `theme=0` is dark theme. The `allow` attribute includes the necessary permissions. `loading="lazy"` is correct. ### BLOCKERS None. This is an MVP creative page with no user input, no auth, no server-side logic, no secrets in code. The code quality issues identified are nits for this context. ### NITS 1. **vite.config.js**: Remove redundant inner `try/catch` in `getVibeEntries()` -- pick either the `throwIfNoEntry` pattern or the try/catch pattern, not both. (Code clarity) 2. **main.js**: Set `started = true` AFTER `Tone.start()` succeeds, not before, so users can retry on failure. (Resilience) 3. **main.js**: Consider storing Tone.js nodes in module scope and adding a cleanup function for future-proofing. (Maintainability) 4. **Dockerfile**: Add a `.dockerignore` file (`.git`, `node_modules`, `dist`, `.woodpecker.yml`). (Build performance) 5. **nginx.conf**: Add gzip compression for text/css and application/javascript. (Performance) 6. **nginx.conf**: Add cache-control headers for hashed static assets. (Performance) 7. **index.html**: Add `og:image` meta tag for richer social previews. (UX) 8. **index.html**: Make `<title>` page-specific, e.g. "Drake | myvibes.world". (UX) 9. **style.css**: Test `overflow: hidden` on very narrow viewports (320px) to ensure no content is clipped. (Accessibility) ### SOP COMPLIANCE - [x] PR body has: Summary, Changes, Test Plan, Related -- all present and detailed - [x] No secrets committed -- Harbor creds are via `from_secret`, no hardcoded values - [x] No unnecessary file changes -- all 10 files are relevant to the feature - [x] Commit messages -- single PR commit, title is descriptive - [x] `package-lock.json` is committed (correct for reproducible builds) ### PROCESS OBSERVATIONS - **First PR for the repo** -- establishes the project scaffold. Good that CI, Docker, and serving are all in place from day one. - **Test plan gaps** -- two items are unchecked (visual check, Docker build). These are manual checks that should be completed before merge. - **No automated tests** -- acceptable for a static creative page with no logic beyond Tone.js audio. If the project grows to include dynamic features, a test harness should be added. - **Deployment path** -- Woodpecker builds and pushes to Harbor, but there is no k8s manifest, Helm chart, or ArgoCD config in this PR. Presumably handled separately, but worth confirming the deployment story is complete. ### VERDICT: APPROVED
- vite.config.js: remove redundant statSync pattern
- main.js: set started flag after Tone.start() succeeds so tap retry works
- nginx.conf: add gzip compression and immutable cache headers for assets
- .dockerignore: exclude .git, node_modules, dist, docs from build context

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ldraney deleted branch 2-drake-vibe-page 2026-06-19 02:04:53 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ldraney/my-vibes-world!4
No description provided.