A fork-friendly personal site and printable CV built with Next.js, Playwright, and Docker.
View the live example · Download the generated CV
The resume page is the source for both the website and the PDF. Playwright
prints /resume/ with the production print stylesheet, keeping the two formats
in sync.
- Static Next.js export that can be hosted almost anywhere.
- Central identity and presentation configuration in
data/site.mjs. - Separate career content in
data/cv.ts. - Responsive dark and light themes with accessible navigation.
- Search metadata, JSON-LD, sitemap, robots policy, and custom 404.
- A4 PDF generation with content and reading-order validation.
- Automated lint, type, configuration, responsive, link, and accessibility checks.
- Clean-checkout, unprivileged nginx container build with security headers.
- Optional production-only, cookieless Umami analytics through a same-origin collector.
- Optional GHCR publishing and GitOps deployment for repository owners who enable it.
-
Use Fork or Use this template on GitHub, then clone your copy.
-
Run
npm install. -
Replace the values in
data/site.mjs. A neutral starting point is available inexamples/site.mjs. -
Replace the experience, skills, and education in
data/cv.ts; seeexamples/cv.tsfor the expected shape. -
Replace
public/headshot.jpgwith an image you own. -
Change
pdfPathindata/site.mjsand set the matching filename when rendering, for example:CV_PDF_FILENAME=Alex-Example-CV.pdf npm run pdf
-
Run the complete verification suite:
npm run verify
The default New Zealand footer outline is optional. Set footerDecoration to
none for a text-only footer, and update location plus mobileLocation.
Requires Node.js 20.9 or newer.
npm install
npx playwright install chromium
npm run devUseful commands:
npm run lint # ESLint
npm run typecheck # TypeScript without emitting files
npm test # configuration tests
npm run build # static export to out/
npm run pdf # render the CV PDF from the built resume
npm run pdf:check # validate PDF structure, content, and section order
npm run test:site # responsive, link, route, and accessibility checks
npm run verify # run the full local/CI quality gatenpm run pdf and npm run test:site require the Chromium browser installed by
Playwright and a completed production build.
Analytics is disabled for local development, tests, preview hosts, and builds without a website ID. To include the tracker in a production static export, set the public build-time value:
NEXT_PUBLIC_UMAMI_WEBSITE_ID=your-umami-website-id npm run buildThe generated site loads /analytics/script.js, which sends events to the
same-origin /analytics/api/send endpoint. Umami derives that endpoint from
the script path, so no data-host-url override is needed. The tracker is
restricted to the apex and www hosts configured in data/analytics.mjs and
honours browser Do Not Track settings.
The website ID is public configuration, not a secret. The hosting layer must
route only those collector paths to Umami; local and CI builds work without
either the ID or collector.
For the included image-publishing workflow, configure
NEXT_PUBLIC_UMAMI_WEBSITE_ID as a GitHub repository variable. It is passed as
a Docker build argument only for the publish build.
data/site.mjs + data/cv.ts
|
v
Next.js pages
/ /resume/ /contact/ /privacy/
|
+--> static export in out/
|
+--> Playwright print --> CV PDF in out/
The application uses the Next.js App Router but exports plain static files. The PDF renderer starts a loopback-only static server, verifies identifying resume content, prints the page, and checks the resulting artifact. The smoke test uses the same server to exercise the built output at desktop and mobile sizes.
Key paths:
data/site.mjs— identity, URLs, homepage copy, assets, and display options.data/cv.ts— experience, skills, education, and earlier roles.app/— pages, metadata routes, global styles, and print layout.components/— navigation, footer, theme, and clipboard interaction.scripts/— static server, PDF generation/validation, and browser checks..github/workflows/deploy.yml— read-only validation plus opt-in publishing.
Run npm run build, then npm run pdf, and publish the contents of out/.
Any provider that serves static files can host the result.
The multi-stage image builds the site and PDF from source, then serves them from an unprivileged nginx process:
docker build -t cv-site .
docker run --rm -p 8080:8080 cv-siteOpen http://localhost:8080.
Validation runs for every pull request and push to main with read-only
repository permissions. Publishing is off by default so forks do not
accidentally push images or call private deployment systems.
To publish an image to your repository's GHCR namespace, create the repository
variable ENABLE_IMAGE_PUBLISH=true.
To also update a Kustomize image tag, configure:
- Variable
ENABLE_GITOPS_DEPLOY=true - Variable
GITOPS_REPOSITORY, such asowner/infrastructure - Variable
GITOPS_PATH, the directory containingkustomization.yaml - Secret
GITOPS_TOKEN, scoped only to the required repository
The source code is MIT licensed. The example portrait at
public/headshot.jpg is Mark Paine's personal photograph, is not licensed
under MIT, and must be replaced in forks. See ASSETS.md.
The About/Resume/Contact structure and printable-resume approach were inspired by Michael D'Angelo's personal site and open-source repository. No code was copied.
MIT © Mark Paine. Please use it, fork it, change it, and make it your own.
