Personal portfolio site for JD Gonzalez. Hosted on GitHub Pages at JD-Gonz.github.io.
The site is a static HTML page, but content is driven by JSON files and the static HTML is regenerated by a build step. Editors never touch HTML by hand — they update data/*.json, push, and a GitHub Actions workflow rebuilds and deploys automatically.
┌────────────────┐ ┌───────────────────────┐ ┌─────────────────────────┐
│ data/*.json │ ─► │ scripts/build.js │ ─► │ index.html (local + CI; │
│ + src/index. │ │ (uses lib/render.js)│ │ not committed) │
│ template.html│ │ │ └─────────────────────────┘
└────────────────┘ └───────────────────────┘
There is also an Express server (server.js + app.js) that reads the same template and data at runtime. Use it for local preview and for the backend JSON API. It is not used in production — GitHub Pages serves index.html directly.
.
├── index.html # GENERATED — `npm run build` (not in git)
├── src/
│ └── index.template.html # source template with @@TOKEN@@ comments
├── data/
│ ├── site.json # meta/OG, header, about, banner, contact, copyright
│ ├── highlights.json # personal-highlights cards
│ └── projects.json # portfolio carousel cards
├── scripts/
│ └── build.js # renders template + data → index.html
├── lib/
│ ├── content.js # JSON loader + cache (fs.watch in dev)
│ └── render.js # template renderer (escape / raw-HTML tokens)
├── routes/
│ └── api.js # /api/site, /api/highlights, /api/projects
├── app.js # Express app factory (local dev / tests)
├── server.js # process entry (listen, signals)
├── test/
│ └── app.test.js # node:test integration suite
├── public/ # static assets (css, js, images, fonts, pdf)
└── .github/workflows/
└── deploy.yml # CI: build + deploy to GitHub Pages on every push
Edit JSON, commit, and push — CI runs npm run build before deploy. Optionally run npm run build locally to refresh ./index.html.
| Section | Edit |
|---|---|
Page <title>, OpenGraph tags |
data/site.json → meta |
| Header logo / tagline | data/site.json → header |
| About Me copy + image | data/site.json → about |
| Personal Highlights intro | data/site.json → highlightsIntro |
| Personal Highlights cards (3) | data/highlights.json |
| Portfolio banner | data/site.json → banner |
| Portfolio carousel cards | data/projects.json |
| Contact links / resume link | data/site.json → contact |
| Copyright footer | data/site.json → copyright |
| Layout / structural changes | src/index.template.html |
npm install
# edit data/*.json …
npm run build # optional locally — writes ./index.html for static preview
git add data/
git commit -m "content: update projects list"
git pushindex.html is listed in .gitignore because CI always runs npm run build before deploy, so the live site never depends on a committed copy. Run npm run build locally when you want ./index.html on disk (for opening the file directly or for sanity-checking a render before push).
- String fields are HTML-escaped when rendered.
- Fields whose name ends in
Html(e.g.banner.bodyHtml,copyright.itemsHtml) are inserted as raw HTML and must be trusted. projects[*].imageHrefis optional — supply it only when the image should link somewhere different from the title (e.g. a live demo vs. its GitHub repo).
npm install
npm run build # optional: materialize ./index.html for static file preview
npm run dev # nodemon; http://localhost:8080The dev server renders from src/index.template.html and does not require ./index.html to exist. Edits to src/, data/, lib/, routes/, app.js, or server.js trigger a nodemon restart. Edits to data/*.json alone are picked up live without a restart (a fs.watch in lib/content.js drops the in-memory caches).
The Express server also exposes the JSON API — useful for building alternative clients or debugging.
npm testtest/app.test.js boots the app on an ephemeral port and asserts:
- every project and highlight title from
/datareaches the rendered HTML, - site copy from
data/site.jsonis present, - no
@@TOKEN@@comments leak through, /api/*endpoints return the expected payloads,- security headers are attached.
The site is deployed by the GitHub Actions Pages source, not from a branch root. Every push to master or main runs tests, rebuilds the static site, and uploads it as a Pages artifact — nothing is committed back to the repo.
In the repo's settings:
- Settings → Pages → Build and deployment → Source → choose "GitHub Actions".
That is the only change required in the GitHub UI. The workflow file (.github/workflows/deploy.yml) handles everything else.
Triggers on push to master or main and on manual workflow_dispatch. Two jobs:
build- Checks out the repo, installs Node 20, runs
npm ci. - Runs
npm test— a failing test aborts the workflow so a broken build never reaches Pages. - Runs
npm run buildto regenerateindex.htmlfrom the current template and data. - Stages exactly what ships into
_site/:index.html,public/, plus an empty.nojekyllmarker. - Uploads
_site/viaactions/upload-pages-artifact@v3.
- Checks out the repo, installs Node 20, runs
deploy(depends onbuild)actions/deploy-pages@v4publishes the artifact to thegithub-pagesenvironment.- The deploy URL is exposed as a job output so it shows up as a clickable link in the Actions UI.
Concurrency is scoped to the pages group with cancel-in-progress: false, matching GitHub's official recommendation — a running deploy won't be cancelled mid-flight by a newer push, but only one deploy is in progress at a time.
- No bot commits back to
master. History stays clean — every commit is intentional. - Failed tests block the deploy. The previous "branch root" model happily served a broken
index.htmlbecause Pages only cared about file bytes, not the build. - The workflow file contains the whole deploy contract; nothing depends on the Pages "Source" dropdown besides the one-time toggle.
These apply when you run the Express server — they are not part of the GitHub Pages deployment.
POST /api/_cache/clear— drop in-memory JSON + rendered-template caches without restarting the process.SIGTERM/SIGINT— graceful shutdown; in-flight requests drain before exit.
helmet— security headers (CSP intentionally disabled; tightening it is a separate pass).compression— gzip/Brotli on responses.express.static('/public')— 7-dayCache-Controlwith ETag.
| Method | Path | Response |
|---|---|---|
| GET | /api/site |
data/site.json |
| GET | /api/highlights |
data/highlights.json |
| GET | /api/projects |
data/projects.json |
| POST | /api/_cache/clear |
{ ok: true } and invalidates caches |
| * | /api/<unknown> |
404 { error: "Not Found", path } |
ISC