This project is a Hugo-powered personal homepage with automated GitHub Pages deployment via GitHub Actions.
Edit hugo.yaml:
baseURL: set to https://<your-username>.github.io/<repo-name>/title: your nameparams.description: your short taglineIf the repository is named <your-username>.github.io, use:
baseURL: "https://<your-username>.github.io/"Edit:
layouts/index.html for sections and textstatic/css/site.css for styles and the Zenburn color palettestatic/fonts/FiraCode-Variable.woff2 — self-hosted Fira Code variable font
(weights 400–700 in one file), SIL Open Font License. Sourced from Google
Fonts; swap it for another @font-face in site.css if you want a
different look.data/resume_skills.yaml — the resume’s Skills section, one entry per
tool/language with an optional link and an optional
Nerd Font icon (a PUA codepoint, e.g. U+E738
for Java, entered in the YAML as the literal character). No url/icon
fields at all just renders as a plain tag — used
for practices/concepts that don’t have a vendor page or logo (e.g.
“distributed systems”). An item can also set tier: "more" to keep a
category’s default view short — those items render inside a “Show N more”
<details> toggle instead of the always-visible list, so a big skill dump
doesn’t drown out the handful that actually matter. Leave tier off for
anything that should always be visible. To add a skill, add an entry here;
the `` shortcode in content/resume.md renders all of
it (via the layouts/partials/skill-item.html partial). Look up new icons
at nerdfonts.com/cheat-sheet, and
regenerate static/fonts/NerdFontsSymbols-Subset.woff2 (currently ~23KB,
subset to only the glyphs actually referenced) with fonttools’
pyftsubset if you add icons outside the current subset.The “Résumé” page renders twice from the same content/resume.md and
data/resume_skills.yaml: once as the styled web page, and once through a
second Hugo output format
(PRINT, configured in hugo.yaml) that produces a plain, black-on-white,
single-column HTML document with no nav/icons/decorative chrome — see
layouts/_default/single.print.html. That plain document exists so the PDF
built from it survives ATS résumé parsers: no Nerd Font icon glyphs in the
text layer, no navigation text ahead of your name, contact info as plain
visible text, and the full (untiered) skills list as comma-separated text
rather than badges. CI (.github/workflows/hugo.yml) renders that page to
public/resume.pdf with headless Chromium
(scripts/render-resume-pdf.mjs, via Puppeteer) after
the Hugo build, so it ships as part of the same Pages deploy — no separate
hosting or repo needed.
To customize what’s on the PDF:
params.fullName in hugo.yaml — the properly capitalized name printed
at the top (separate from firstName/brand, which stay lowercase-styled
for the site itself).params.contact.email in hugo.yaml — shown as a mailto: link on the
PDF only. Leave blank to omit it; it’s deliberately not published anywhere
else on the site (the contact page uses a form instead).params.social.linkedin / params.social.github — already used elsewhere
on the site; reused here as plain text links.content/resume.md that would otherwise tell the PDF to
go download itself) is done with paired .foo-screen / .foo-print CSS
classes — see layouts/shortcodes/skill-groups.html and
layouts/shortcodes/resume-pdf-note.html. static/css/site.css hides the
-print classes on the web page; single.print.html’s own inline
stylesheet hides the -screen classes. Follow that pattern for any other
content that should differ between the two.params.resumePdfUrl (already set to resume.pdf) is what the web page
links to for the download; it doesn’t need to change unless you rename the
output file.
To generate the PDF locally: hugo (builds public/print/resume/index.html
among the rest), then npm ci once, then
node scripts/render-resume-pdf.mjs public/print/resume/index.html public/resume.pdf.
Headless Chromium needs a handful of system shared libraries not installed
by default in a minimal container (libnspr4, libnss3, libcairo2,
libasound2t64, and similar) — install those via your OS package manager if
puppeteer.launch() fails with a “cannot open shared object file” error;
GitHub Actions’ ubuntu-latest runners already have them.
GitHub Pages serves static files only, so there is no server to send mail from. The contact page posts to Formspree, which relays each submission to your inbox.
/f/ in the endpoint it gives you
(https://formspree.io/f/abcdwxyz → abcdwxyz).Put it in hugo.yaml:
params:
contact:
formspreeID: "abcdwxyz"
Until formspreeID is set, the contact page renders a short “not configured
yet” notice instead of the form. The form ID is not a secret — it is a public
endpoint, safe to commit.
Notes:
_gotcha honeypot field is included; Formspree discards anything
that fills it in.Two independent, optional trackers, both configured under params.analytics
in hugo.yaml. Leave either blank to skip it — nothing renders until it’s
configured.
Page-view analytics — GoatCounter
Free, cookieless, no consent banner required.
https://<code>.goatcounter.com).Put just the code in hugo.yaml:
params:
analytics:
goatcounterCode: "your-code"
https://<code>.goatcounter.com.Visiting-company identification — Leadfeeder
Flags which organization a visit came from when it’s identifiable from a corporate network (home/mobile/VPN traffic won’t be identified — that’s a limit of the underlying IP-to-org data, not a bug). Free “Lite” plan covers the last 100 identified companies/month with 7-day history — plenty for a personal site.
Paste the entire thing into hugo.yaml as a block scalar:
params:
analytics:
leadfeederScript: |
<script>...the script Leadfeeder gave you...</script>
A privacy note: GoatCounter needs no disclosure since it doesn’t process personal data. Leadfeeder-style identification does derive information from visitor IP addresses, so if you’re aiming for strict GDPR compliance you may want a short privacy note on the site; for a low-traffic personal portfolio the practical risk is minimal, but it’s your call to make.
main branch.main again (or use Run workflow in Actions).For day-to-day content editing, install Hugo and run:
hugo server -D
Open http://localhost:1313. This is a live-reloading dev server that
renders everything in memory — it never touches public/ and never runs the
PDF-render script, so the “download the detailed PDF résumé” link will 404
here. That’s expected, not a bug.
To preview the site as it will actually deploy — including a working PDF link — build it for real and serve the static output instead:
npm ci # once
npm run preview
This runs hugo --gc (with whatever baseURL is set in hugo.yaml — the
real production one), renders public/resume.pdf with Puppeteer, and serves
public/ at http://localhost:3000. Because baseURL is the bare
https://jmuise.github.io/ with no repo-name subpath, the site’s internal
links are already root-relative (/css/site.css, not /some-repo/css/...),
so they resolve the same way locally as they will in production — no
baseURL override needed for local serving.
npm run build does the same build + PDF render without serving anything —
useful if you just want the files on disk (it’s also what CI runs).
.github/workflows/hugo.yml: build and deploy workflow (Hugo build, PDF
render, Pages deploy)hugo.yaml: Hugo site config, nav menu, contact form ID, analytics config,
the PRINT output format used for the PDF résumélayouts/_default/baseof.html: shared page shell (head, nav)layouts/index.html: homepage templatelayouts/_default/single.html: template for regular content pageslayouts/_default/single.print.html: self-contained, ATS-plain template
used only to generate the PDF résumélayouts/shortcodes/contact-form.html: the contact form markup and submit scriptlayouts/shortcodes/skill-groups.html: renders the Skills section, in
both its tiered/badged (screen) and flat/plain-text (print) formslayouts/shortcodes/resume-pdf-note.html: the screen/print variants of
the sentence pointing at the PDF downloadscripts/render-resume-pdf.mjs: headless-Chromium script that renders the
print output format to public/resume.pdfstatic/css/site.css: site stylingcontent/_index.md: homepage content metadatacontent/contact.md: contact page copy, embeds the form shortcode