The vita page carried a hand-typed duplicate of education, publications,
presentations, and experience, and had drifted from the PDFs in about
fifteen places — Shu Lab still one entry titled "Undergraduate
Researcher" three months after the degree, Weenix at ~7,000 lines, CHASE
still "accepted" after being presented, a preprint missing entirely, and
two different code URLs for the same paper.
build/Vita.hs reads yaml-source/data/*.yml — the same files the PDF
pipeline builds from — and renders education, experience (keeping the
CV's research/industry split), publications, presentations, and contact.
Drift is now structurally impossible rather than merely discouraged.
Notes on the implementation:
- The YAML is LaTeX-flavoured because xelatex is its first consumer, so
values pass through a small closed converter (\textbf, \textit,
\texttt, \href, $\times$, $\delta$, \#, {,}, ~, --, ---). Escaping
happens before conversion; unhandled commands pass through visibly
rather than silently dropping their argument.
- Scalars are parsed loosely: the same logical field is sometimes a YAML
number and sometimes a string (`year: 2026` vs `year: "2026--2027"`),
and the PDF pipeline does not care, so this does not force it to.
- personal.yml's phone number is deliberately not read. It is printed on
the CV, which is handed to a chosen reader; this page is crawled.
- Contact chips carry labels (ORCID, Forgejo, GitHub) rather than the
CV's full display values, which exist because paper cannot be clicked.
The page keeps only what nothing else on the site owns: the pointers and
the four research threads. In-progress work belongs to /current, the
engineering index to /cv/projects/, and the narrative to /me/ — whose
abstract already described this page as the formal record.
vita.css composes on item-card.css and mirrors now.css's headings and
chip geometry, so /about and /current read as one system. Link chips get
the only interaction polish on the page, being the one affordance a
printed CV cannot offer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUGesXiMmACsLBTGG1xuEU
|
||
|---|---|---|
| archive | ||
| build | ||
| content | ||
| data | ||
| nginx | ||
| paper | ||
| static | ||
| systemd | ||
| templates | ||
| tests | ||
| tools | ||
| yaml-source | ||
| .env.example | ||
| .gitignore | ||
| .python-version | ||
| ARCHIVE.md | ||
| AUDIT-2026-06-09.md | ||
| AUDIT.md | ||
| HOMEPAGE.md | ||
| LICENSE | ||
| MARKS.md | ||
| Makefile | ||
| PHOTOGRAPHY.md | ||
| README.md | ||
| WRITING.md | ||
| cabal.project | ||
| cabal.project.freeze | ||
| levineuwirth.cabal | ||
| pyproject.toml | ||
| uv.lock | ||
README.md
levineuwirth.org
Personal site of Levi Neuwirth — essays, blog posts, poetry, fiction, and music.
Built with Hakyll and Pandoc,
with a custom build system in build/ and a Haskell + JS + Python toolchain.
Quickstart
make build # one-shot production build into _site/
make dev # dev build (drafts visible) + local server on :8000
make watch # Hakyll live-reload dev server (drafts visible)
make clean # cabal run site -- clean
make deploy # clean → build → sign → push → rsync to VPS
make build always runs make clean implicitly when invoked from make deploy.
For day-to-day work, prefer make dev (which serves the site on
http://localhost:8000) or make watch (Hakyll's live-reload preview server,
which rebuilds on save and serves the site locally).
Run make build any time you add or replace binary assets (JPEG/PNG
figures, PDFs, music assets). make dev and make watch skip the
convert-images.sh / pdf-thumbs preprocessing steps, so a fresh JPEG
will have no .webp companion and a fresh PDF will have no thumbnail
until a full make build regenerates them. Once the companions exist
they survive subsequent make dev runs.
Optional features
-
Similar-links and embeddings.
tools/embed.pyprecomputes page-level embeddings for the "Related" block. To enable:uv sync # creates .venv with sentence-transformers, faiss-cpuThe build silently skips embedding when
.venvis absent. -
Client-side semantic search. Downloads a quantized ONNX model used by
static/js/semantic-search.js(run once; files are gitignored):make download-model -
Image conversion.
make buildcallstools/convert-images.shto produce.webpcompanions next to every JPEG/PNG. Requirescwebp(libwebpon Arch,webpon Debian/Ubuntu). -
PDF thumbnails.
make pdf-thumbsgenerates first-page thumbnails for PDFs instatic/papers/usingpdftoppm(poppleron Arch,poppler-utilson Debian/Ubuntu). Skipped silently when missing.
Configuration
.env (gitignored, copy from .env.example) holds the GitHub PAT and
the VPS rsync target consumed by make deploy. Never commit it.
Repository layout
build/— Haskell build system (Hakyll rules, Pandoc filters, contexts). Seebuild/Filters/for the Pandoc AST transforms (sidenotes, wikilinks, transclusion, score embedding, viz, …).content/— authored Markdown (essays, blog, poetry, fiction, music).templates/— Hakyll/Pandoc HTML templates.static/— CSS, JS, fonts, images, vendored PDF.js.tools/— Python tooling (embeddings, importers) and shell scripts.data/— generated and source data (commonplace.yaml, annotations.json, bibliographies, similar-links.json).nginx/— vhost snippets shipped to the VPS (security-headers.conf,static-assets.conf,popup-proxy.conf). The live vhost on the VPS is the source of truth; seenginx/vhost.conf.examplefor the canonical structure and the include order these snippets expect.
Architecture pointers
build/Site.hsis the Hakyll rules entry point.build/Patterns.hsdefines canonical content patterns shared by Backlinks, Authors, Tags, and Site.build/Compilers.hswires the Pandoc filter chain into Hakyll.build/Filters/Images.hsdoes WebP<picture>wrapping; requires the.webpcompanions produced bytools/convert-images.sh.
License
See LICENSE.