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

19 KiB
Raw Permalink Blame History

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
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:

+++
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):

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.

  1. Safety tag — git tag pre-hugo at current HEAD; confirm clean tree.
  2. Toolchain — install/extend Hugo (extended) locally; git submodule add hugo-coder; init submodule.
  3. 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/).
  4. 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/.
  5. Engine build — npm run build; verify dist/ bundle exists and shaders are inlined.
  6. 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.
  7. Projects migration — 5 × content/projects/*.md with frontmatter. Verify: hugo builds; /projects + each project URL render.
  8. 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.
  9. 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.
  10. Docs — write DEPLOYMENT.md (section 9 content) to repo; update README.md dev/Docker sections.
  11. 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 builds. 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):

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):

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.