# 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 # + `. 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 `` + `/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/`, `/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.