- 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
19 KiB
Hugo Transition Plan — WebGPU Portfolio
For Hermes: implement this plan via
opencodeCLI, task-by-task, small verifiable chunks wrapped withtimeout. Verify each task (typecheck/build/hugo/curl) before moving on. After any timeout, inspectgit statusbefore 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) —UiStateinterface lives hereengine/gl-renderer.ts(WebGL2 fallback)engine/types.ts(RendererLike)engine/shaders/fullscreen.wgsl+fullscreen.glsl— shader math stays byte-identicalui/input.ts(pointer tracking; tiny, feedsstate.mouse)
New file — slim bootstrap gpu-bg/src/main.ts (replaces app.ts + main.ts):
- Look up
#gpucanvas; 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).requestAnimationFrameloop: feedresolution,time,mouse, and fixed uniforms:scene = 0,dim = 0.0, zeroedstemMultipliers(32 floats).- No router, no overlay, no scene switching. The
scene/dim/stemMultipliersplumbing 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 toarchive/; new engine gets its own undergpu-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.tomlkey points:baseURL = "https://djosen.one/"title = "Djosen",theme = "hugo-coder"params.author = "Djosen",params.info = "<tagline>",params.description,params.keywordsparams.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):
layouts/partials/gpu-background.html—<canvas id="gpu" aria-hidden="true"></canvas>+<script src="{{ "gpu/engine.js" | relURL }}" defer></script>.layouts/index.html— override homepage: include the partial; canvas isposition: fixed; inset: 0; z-index: -1; content container gets transparent/dark glass background so the spiral shows through with readable text.static/css/custom.css— the canvas + glass styling above; loaded via the theme's head hook (layouts/partials/head.htmloverride, or hugo-coder'scustom_cssparam if present — verify exact hook during implementation).layouts/resume/single.html— rendersite.Data.resume(summary, work, education, skills) matching the current card/pill visual but in hugo-coder's palette.layouts/projects/list.html— only if the default list doesn't showperiod+description+tagsacceptably.
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), thenhugo serverfrom root (watches content + the built bundle instatic/).- Optional single
npm/Makefileorchestration 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.
- Safety tag —
git tag pre-hugoat current HEAD; confirm clean tree. - Toolchain — install/extend Hugo (extended) locally;
git submodule addhugo-coder; init submodule. - Archive SPA — move
src/,index.html, rootpackage.json/package-lock.json/tsconfig.json/vite.config.ts→archive/; update.gitignore(public/,static/gpu/,resources/,gpu-bg/dist/,gpu-bg/node_modules/). - Engine sub-package — create
gpu-bg/with copied renderer/gl-renderer/types/input/shaders + slimmain.ts+package.json/tsconfig.json/vite.config.ts. Verify:npm run typecheckingpu-bg/. - Engine build —
npm run build; verifydist/bundle exists and shaders are inlined. - Hugo scaffold —
config.toml;content/_index.md,about.md,resume.md,projects/_index.md,posts/_index.md;data/resume.yaml. Verify:hugobuilds, homepage/about render. - Projects migration — 5 ×
content/projects/*.mdwith frontmatter. Verify:hugobuilds;/projects+ each project URL render. - Custom layouts — gpu-background partial, index.html override + custom.css, resume layout, projects list tweak. Verify:
hugobuilds; homepage contains<canvas id="gpu">+/gpu/engine.jslink. - Docker + nginx — rewrite Dockerfile (3-stage), update nginx.default.conf,
.gitignore. Verify:docker buildsucceeds;docker compose up -d;curl -sf http://127.0.0.1:8080/,/about,/projects,/resume,/posts,/gpu/engine.jsall 200; a bogus path → 404. - Docs — write
DEPLOYMENT.md(section 9 content) to repo; updateREADME.mddev/Docker sections. - Final verification —
npm run typecheck+npm run build(engine),hugo,docker composesmoke test, and a manual browser check of WebGPU render + WebGL fallback + dark theme + canvas-behind-content.
8. Verification checklist (acceptance)
hugobuild exits 0;public/populated with real.htmlper 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.jsreturns 200 (hashed, cacheable).- Resume renders from
data/resume.yamlwith real name + full work/education/skills. - Projects list groups under the
programmingcategory; tags shown. - Docker image builds from a recursive clone; container healthcheck passes.
git statusclean 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→hugoextended →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:
-
Clone with submodules. The hugo-coder submodule must be present before
docker build. Change the checkout step to recursive:- Gitea Actions:
checkout@v3withsubmodules: recursive(or addgit submodule update --init --recursive). - Plain webhook/script:
git clone --recursive ...orgit pull && git submodule update --init --recursive. Without this,hugofails with "theme not found" and the build breaks.
- Gitea Actions:
-
No other secrets/hosts change. Registry creds, SSH/webhook target, and the
docker compose up -ddeploy 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
curlcan'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.