Files
webgpu-portfolio/.hermes/plans/2026-08-16_hugo-transition.md
djoser 9ea85db5ce Migrate Vite SPA to Hugo (hugo-coder, dark) with WebGPU background
- Hugo static site at root: config, content (home/about/resume/blog + 5 projects),
  data/resume.yaml, custom layouts (gpu background, resume renderer, projects list),
  dark theme with canvas behind content
- gpu-bg/: extracted WebGPU/WebGL2 spiral engine as a Vite sub-package; slim bootstrap
  drops router/overlay/text renderer; fixed uniforms (scene0/dim0); self-contained engine.js
- archive/: retired Vite SPA preserved (router, overlay, data, engine/text orphaned)
- 3-stage Dockerfile (node -> hugo extended/dart-sass -> nginx), static nginx config
- DEPLOYMENT.md handover + README rewrite; pre-hugo rollback tag
2026-08-21 03:39:42 +02:00

308 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Hugo Transition Plan — WebGPU Portfolio
> **For Hermes:** implement this plan via `opencode` CLI, task-by-task, small verifiable chunks wrapped with `timeout`. Verify each task (typecheck/build/hugo/curl) before moving on. After any timeout, inspect `git status` before continuing.
**Created:** 2026-08-16
**Goal:** Replace the Vite/WebGPU single-page app with a Hugo static site (hugo-coder theme, dark), keeping the WebGPU spiral visuals as a full-page homepage background behind the theme. Everything else — content, nav, routing, resume, projects, blog — becomes Hugo.
---
## 1. Locked decisions (recap)
| # | Decision | Choice |
|---|----------|--------|
| 1 | Engine scope | Minimal background shader only (spiral + mouse + accent/dim). Drop text-renderer, router, DOM overlay, TS data files |
| 2 | Engine build | Keep Vite + TS as a sub-package; emit a static JS bundle into Hugo `static/` |
| 3 | Repo layout | In-place monorepo; Hugo at root, engine under `gpu-bg/` |
| 4 | Homepage canvas | Full-page background, dark theme overall |
| 5 | Identity | "Djosen" as site title + displayed author (full alias); resume keeps real name "Ahmed Mohamed" |
| 6 | Resume | Structured `data/resume.yaml` + custom Hugo layout |
| 7 | Projects | `categories` taxonomy (programming/hardware/visuals/music), tech → `tags` |
| 8 | Menu + blog | Home, About, Projects, Resume, Blog — blog enabled |
| 9 | Accent | hugo-coder default blue accent; background stays emerald (two-tone, revisit after render) |
| 10 | baseURL | `https://djosen.one/` |
| 11 | Theme install | git submodule at `themes/hugo-coder` |
| 12 | Old SPA code | Archive (tag + `archive/` folder), do not delete |
| 13 | CI/CD | Deploy workflow lives server-side; produce a handover doc to adjust it |
---
## 2. Target architecture
```
webgpu/ (repo root = Hugo site)
├── config.toml # hugo-coder config (TOML)
├── content/
│ ├── _index.md # homepage welcome text
│ ├── about.md
│ ├── resume.md # layout = "resume", renders data/resume.yaml
│ ├── projects/
│ │ ├── radiance-caching.md
│ │ ├── point-cloud.md
│ │ ├── evol-engine.md
│ │ ├── auv-simulation.md
│ │ └── improv-gfx.md
│ └── posts/
│ └── _index.md # blog landing (empty starter)
├── data/
│ └── resume.yaml
├── layouts/
│ ├── index.html # homepage override → includes gpu-background partial
│ ├── partials/
│ │ └── gpu-background.html # <canvas> + <script src=/gpu/engine.js>
│ ├── resume/
│ │ └── single.html # data-driven resume renderer
│ └── projects/
│ └── list.html # shows period + description + tags (if needed)
├── static/
│ ├── css/custom.css # canvas-behind-content + dark glass styling
│ └── gpu/ # engine build output (gitignored)
│ └── engine.js (+ hashed assets)
├── gpu-bg/ # engine sub-package (Vite + TS)
│ ├── package.json
│ ├── tsconfig.json
│ ├── vite.config.ts
│ ├── index.html # local dev harness only
│ └── src/
│ ├── main.ts # slim bootstrap (replaces app.ts + main.ts)
│ ├── renderer.ts # WebGPU (copied, unmodified)
│ ├── gl-renderer.ts # WebGL2 fallback (copied, unmodified)
│ ├── types.ts # RendererLike (copied)
│ ├── input.ts # pointer tracking (copied)
│ └── shaders/
│ ├── fullscreen.wgsl # UNCHANGED
│ └── fullscreen.glsl # UNCHANGED
├── archive/ # old SPA (per decision 12)
│ ├── index.html
│ ├── src/ # app.ts, router.ts, ui/overlay.ts, data/, engine/text/*
│ ├── package.json / package-lock.json / tsconfig.json / vite.config.ts
├── themes/hugo-coder/ # git submodule
├── Dockerfile # 3-stage: node → hugo(ext) → nginx
├── docker-compose.yml # UNCHANGED
├── nginx.default.conf # drop SPA fallback → plain static
├── DEPLOYMENT.md # handover (section 9 below)
├── .gitmodules
└── .gitignore # + public/, static/gpu/, resources/
```
---
## 3. Engine extraction — what survives vs. is archived
**Survives into `gpu-bg/` (copied unmodified):**
- `engine/renderer.ts` (WebGPU) — `UiState` interface lives here
- `engine/gl-renderer.ts` (WebGL2 fallback)
- `engine/types.ts` (`RendererLike`)
- `engine/shaders/fullscreen.wgsl` + `fullscreen.glsl` — **shader math stays byte-identical**
- `ui/input.ts` (pointer tracking; tiny, feeds `state.mouse`)
**New file — slim bootstrap `gpu-bg/src/main.ts` (replaces app.ts + main.ts):**
- Look up `#gpu` canvas; if absent, no-op (partial only included on homepage).
- `init()` WebGPU → fallback WebGL2; only if both fail, hide canvas (no separate #fallback UI — Hugo content still shows).
- `resize()` + DPR cap 2 (reuse logic from app.ts).
- `requestAnimationFrame` loop: feed `resolution`, `time`, `mouse`, and **fixed uniforms**: `scene = 0`, `dim = 0.0`, zeroed `stemMultipliers` (32 floats).
- No router, no overlay, no scene switching. The `scene`/`dim`/`stemMultipliers` plumbing is collapsed to constants at the bootstrap boundary — the renderer and shader remain untouched.
**Archived into `archive/` (deleted from active tree):**
- `app.ts`, `main.ts` (superseded by slim bootstrap)
- `router.ts` (Hugo does routing)
- `ui/overlay.ts` (Hugo renders content)
- `data/**` (all content → Hugo; see section 4)
- `engine/text/**` (text-renderer, text-layout, slug-generator — confirmed orphaned: only self-references, nothing imports them)
- root `index.html`, `package.json`, `package-lock.json`, `tsconfig.json`, `vite.config.ts` (move to `archive/`; new engine gets its own under `gpu-bg/`)
**Note on shaders:** the WGSL/GLSL scene-palette `if/else` chain will now always resolve to `scene0` (emerald). Do NOT simplify the shader during this transition — it risks introducing bugs in working math. Flag as an optional later cleanup.
---
## 4. Content migration map
| Source | Destination | Notes |
|--------|-------------|-------|
| `pages.ts` home entry + `scenes/hero.ts` | `content/_index.md` + `params.info` | tagline → `params.info = "Creative Developer | Game Developer | Software Engineer"`; welcome body → `_index.md` |
| `scenes/about.ts` | `content/about.md` | Convert HTML `<p>/<ul>/<li>/<a>` → Markdown. Content: aliases, TUM masters, graphics/XR/games/AI background, Forza/Turn10, Siemens AI, hobbies list, emails + discord |
| `scenes/contact.ts` | — (drop) | It's a placeholder (`hello@example.com`); real contact lives in about + social config |
| `scenes/projects.ts` (intro) | `content/projects/_index.md` | Keep the "not an exhaustive list…" intro text |
| `projects/index.ts` categories | taxonomy config | `categories` = programming, hardware, visuals, music (empty ones become invisible terms until populated) |
| `projects/radiance-caching.ts` | `content/projects/radiance-caching.md` | frontmatter below |
| `projects/point-cloud.ts` | `content/projects/point-cloud.md` | " |
| `projects/evol-engine.ts` | `content/projects/evol-engine.md` | " |
| `projects/auv-simulation.ts` | `content/projects/auv-simulation.md` | " |
| `projects/improv-gfx.ts` | `content/projects/improv-gfx.md` | " |
| `resume.ts` | `data/resume.yaml` + `layouts/resume/single.html` | full structure preserved |
| `scenes/resume.ts` (title/subtitle) | `content/resume.md` frontmatter | title "Resume" |
**Project frontmatter shape:**
```toml
+++
title = "<name>"
description = "<tagline>"
date = "<parseable date>" # for Hugo ordering
period = "<human range>" # e.g. "12/2025 — 06/2026"; source dates are ranges/years
tags = ["<tech...>"] # tech[] → tags
categories = ["programming"] # all 5 currently live in "programming"
+++
<markdown body from content>
```
**Date handling:** source `date` values are ranges (`12/2025 — 06/2026`) or bare years (`2024`, `2021`). Hugo needs a real date for list ordering — set `date` to the range end (or a sane parseable date) and preserve the human string in a custom `period` frontmatter field, displayed by the projects list template. `tech` → `tags`, category → `categories`.
**Resume name divergence (intentional):** site identity = "Djosen" (decision 5), but `data/resume.yaml` keeps `name: "Ahmed Mohamed"` — a CV must carry the real name. Flag if you want otherwise.
---
## 5. Theme setup (hugo-coder, dark)
- `git submodule add https://github.com/luizdepra/hugo-coder themes/hugo-coder`
- **Requires Hugo extended** (theme compiles SCSS via Hugo Pipes).
- `config.toml` key points:
- `baseURL = "https://djosen.one/"`
- `title = "Djosen"`, `theme = "hugo-coder"`
- `params.author = "Djosen"`, `params.info = "<tagline>"`, `params.description`, `params.keywords`
- `params.colorScheme = "dark"` (pin dark; user wants dark overall)
- `params.avatarURL = ""` (no avatar for now — leave off; add later)
- `params.social`: github (`github.com/inedited`), linkedin (`linkedin.com/in/inedited/`), email (`ineditedofficial@gmail.com`)
- `menu.main`: Home `/`, About `/about`, Projects `/projects`, Resume `/resume`, Blog `/posts`
**Custom layouts (the only real theme surgery):**
1. `layouts/partials/gpu-background.html` — `<canvas id="gpu" aria-hidden="true"></canvas>` + `<script src="{{ "gpu/engine.js" | relURL }}" defer></script>`.
2. `layouts/index.html` — override homepage: include the partial; canvas is `position: fixed; inset: 0; z-index: -1`; content container gets transparent/dark glass background so the spiral shows through with readable text.
3. `static/css/custom.css` — the canvas + glass styling above; loaded via the theme's head hook (`layouts/partials/head.html` override, or hugo-coder's `custom_css` param if present — **verify exact hook during implementation**).
4. `layouts/resume/single.html` — render `site.Data.resume` (summary, work, education, skills) matching the current card/pill visual but in hugo-coder's palette.
5. `layouts/projects/list.html` — only if the default list doesn't show `period` + `description` + `tags` acceptably.
---
## 6. Build orchestration
**Engine (`gpu-bg/`):** `vite build` → a single self-contained bundle (WGSL/GLSL inlined via `?raw`) emitted to `gpu-bg/dist/`, which the Hugo build copies into `static/gpu/`. Configure `vite.config.ts` build `outDir` + `assetsInlineLimit` so shaders inline; `assetsInclude: ['**/*.glsl', '**/*.wgsl']`.
**Dev workflow:**
- `cd gpu-bg && npm run build` (engine), then `hugo server` from root (watches content + the built bundle in `static/`).
- Optional single `npm`/`Makefile` orchestration script: `build:engine && hugo server`.
**Dockerfile (3-stage):**
```dockerfile
FROM node:22-alpine AS engine
WORKDIR /gpu
COPY gpu-bg/package.json gpu-bg/package-lock.json* ./
RUN npm ci
COPY gpu-bg/ ./
RUN npm run build
FROM gohugoio/hugo:ext-alpine AS site # pin a version tag
WORKDIR /site
COPY . .
COPY --from=engine /gpu/dist ./static/gpu/
RUN hugo --minify
FROM nginx:alpine-slim AS serve
COPY --from=site /site/public /usr/share/nginx/html
COPY nginx.default.conf /etc/nginx/conf.d/default.conf
RUN nginx -t
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]
```
**Critical:** the build context must contain the initialized `themes/hugo-coder` submodule. Any clone/deploy step must use `--recursive` or run `git submodule update --init --recursive` before `docker build`.
**nginx.default.conf changes:** replace the SPA fallback (`try_files $uri $uri/ /index.html`) with plain static `try_files $uri $uri/ =404` (Hugo emits real `.html` per page; 404s are now legitimate). Keep security headers + gzip. Adjust immutable-cache rule to Hugo's output dirs (`/css/`, `/js/`, `/gpu/`) with fingerprint-aware caching (Hugo fingerprints via filename hash).
---
## 7. Task breakdown (for opencode)
Each task = one opencode run, self-contained, with an explicit verify step.
0. **Safety tag** — `git tag pre-hugo` at current HEAD; confirm clean tree.
1. **Toolchain** — install/extend Hugo (extended) locally; `git submodule add` hugo-coder; init submodule.
2. **Archive SPA** — move `src/`, `index.html`, root `package.json`/`package-lock.json`/`tsconfig.json`/`vite.config.ts` → `archive/`; update `.gitignore` (`public/`, `static/gpu/`, `resources/`, `gpu-bg/dist/`, `gpu-bg/node_modules/`).
3. **Engine sub-package** — create `gpu-bg/` with copied renderer/gl-renderer/types/input/shaders + slim `main.ts` + `package.json`/`tsconfig.json`/`vite.config.ts`. Verify: `npm run typecheck` in `gpu-bg/`.
4. **Engine build** — `npm run build`; verify `dist/` bundle exists and shaders are inlined.
5. **Hugo scaffold** — `config.toml`; `content/_index.md`, `about.md`, `resume.md`, `projects/_index.md`, `posts/_index.md`; `data/resume.yaml`. Verify: `hugo` builds, homepage/about render.
6. **Projects migration** — 5 × `content/projects/*.md` with frontmatter. Verify: `hugo` builds; `/projects` + each project URL render.
7. **Custom layouts** — gpu-background partial, index.html override + custom.css, resume layout, projects list tweak. Verify: `hugo` builds; homepage contains `<canvas id="gpu">` + `/gpu/engine.js` link.
8. **Docker + nginx** — rewrite Dockerfile (3-stage), update nginx.default.conf, `.gitignore`. Verify: `docker build` succeeds; `docker compose up -d`; `curl -sf http://127.0.0.1:8080/`, `/about`, `/projects`, `/resume`, `/posts`, `/gpu/engine.js` all 200; a bogus path → 404.
9. **Docs** — write `DEPLOYMENT.md` (section 9 content) to repo; update `README.md` dev/Docker sections.
10. **Final verification** — `npm run typecheck` + `npm run build` (engine), `hugo`, `docker compose` smoke test, and a manual browser check of WebGPU render + WebGL fallback + dark theme + canvas-behind-content.
---
## 8. Verification checklist (acceptance)
- `hugo` build exits 0; `public/` populated with real `.html` per page (no SPA fallback).
- Homepage renders the WebGPU spiral behind hugo-coder content; text readable (dark theme).
- WebGL2 fallback works when WebGPU is unavailable (manual: disable WebGPU in a browser).
- `/about`, `/projects`, `/projects/<slug>`, `/resume`, `/posts`, `/` all return 200; unknown path → 404.
- `/gpu/engine.js` returns 200 (hashed, cacheable).
- Resume renders from `data/resume.yaml` with real name + full work/education/skills.
- Projects list groups under the `programming` category; tags shown.
- Docker image builds from a recursive clone; container healthcheck passes.
- `git status` clean of build artifacts (public/, static/gpu/, gpu-bg/dist/, node_modules/ all ignored).
---
## 9. DEPLOYMENT HANDOVER (copy verbatim into repo as `DEPLOYMENT.md`)
> How the site now works, and how to adjust the server-side deploy workflow (Gitea Actions + webhook) for the new build.
### 9.1 How it works now (unchanged parts)
Serving topology is **unchanged**. The host's system nginx (Certbot-managed TLS at `djosen.one`) still proxies `location /` → `127.0.0.1:8080`, where the Docker `web` container listens. Nothing in the system nginx (`nginx/system-default.conf` / `nginx/reverse.conf`) or Certbot setup needs to change.
What changes is **what's inside the container**: instead of a Vite SPA (single `index.html` + JS), the container now serves a Hugo static site (`public/` — one real `.html` per page).
### 9.2 What changed in the build
- **Build chain is now two steps:** (1) `gpu-bg/` Vite build → static JS bundle, (2) `hugo --minify` → `public/`. The Dockerfile does both in stages (`node` → `hugo` extended → `nginx`).
- **Hugo extended is required** (the theme compiles SCSS). The Docker stage uses a Hugo-extended image (pinned tag).
- **The hugo-coder theme is a git submodule** at `themes/hugo-coder`.
- **nginx in the container no longer does SPA fallback** — plain static serving with fingerprint-aware cache rules.
### 9.3 How to adjust the server-side deploy workflow
The deploy workflow (Gitea Actions `deploy.yml` / webhook script) currently clones and `docker build`s. Two adjustments are mandatory:
1. **Clone with submodules.** The hugo-coder submodule must be present before `docker build`. Change the checkout step to recursive:
- Gitea Actions: `checkout@v3` with `submodules: recursive` (or add `git submodule update --init --recursive`).
- Plain webhook/script: `git clone --recursive ...` or `git pull && git submodule update --init --recursive`.
Without this, `hugo` fails with "theme not found" and the build breaks.
2. **No other secrets/hosts change.** Registry creds, SSH/webhook target, and the `docker compose up -d` deploy command are all the same. The image just gets bigger (Hugo extended + node build layers are discarded; final nginx stage stays small).
### 9.4 How to test the deployment
**Local (before pushing):**
```bash
git submodule update --init --recursive
cd gpu-bg && npm install && npm run build && cd ..
hugo # builds public/
docker compose build # 3-stage build
docker compose up -d # maps 127.0.0.1:8080:8080
```
**Smoke test (after `up -d`):**
```bash
curl -sf http://127.0.0.1:8080/ | grep -q 'id="gpu"' && echo "canvas OK"
curl -sf http://127.0.0.1:8080/gpu/engine.js -o /dev/null && echo "engine bundle OK"
curl -sf http://127.0.0.1:8080/about/ -o /dev/null && echo "about OK"
curl -sf http://127.0.0.1:8080/projects/ -o /dev/null && echo "projects OK"
curl -sf http://127.0.0.1:8080/resume/ -o /dev/null && echo "resume OK"
curl -sf http://127.0.0.1:8080/posts/ -o /dev/null && echo "posts OK"
curl -s http://127.0.0.1:8080/nonexistent -o /dev/null -w "%{http_code}\n" # expect 404
```
**Browser check (manual, WebGPU is a real-GPU feature):**
- Homepage: spiral animates behind content; dark theme; text readable.
- Disable WebGPU (or use a non-WebGPU browser) → WebGL2 fallback still animates.
- No-WebGL browser → static dark page (content still readable; canvas simply doesn't render).
**After deploy (remote):**
- `curl -s https://djosen.one/health` → `ok` (system nginx health endpoint, unchanged).
- Verify `/`, `/about/`, `/projects/`, `/resume/`, `/posts/` over HTTPS.
- Confirm the spiral renders in a real browser (the one thing `curl` can't check).
### 9.5 Rollback
The previous site is preserved as the git tag `pre-hugo` and the `archive/` folder. To roll back: `git checkout pre-hugo` and redeploy with the old single-stage `Dockerfile` (also recoverable from git history). No system nginx / DNS changes were made, so rollback is purely a container-image swap.