From 714824a0b58d794f83d1c2af647dc9be43f83484 Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Tue, 17 Mar 2026 21:56:14 -0400 Subject: [PATCH] initial deploy! whoop --- .env.example | 13 + .gitignore | 16 + CLAUDE.md | 117 ++ Makefile | 32 + WRITING.md | 745 +++++++++++++ build/Authors.hs | 127 +++ build/Backlinks.hs | 301 ++++++ build/Catalog.hs | 202 ++++ build/Citations.hs | 217 ++++ build/Commonplace.hs | 161 +++ build/Compilers.hs | 206 ++++ build/Contexts.hs | 317 ++++++ build/Filters.hs | 37 + build/Filters/Code.hs | 29 + build/Filters/Dropcaps.hs | 15 + build/Filters/Images.hs | 56 + build/Filters/Links.hs | 51 + build/Filters/Math.hs | 14 + build/Filters/Score.hs | 92 ++ build/Filters/Sidenotes.hs | 67 ++ build/Filters/Smallcaps.hs | 71 ++ build/Filters/Typography.hs | 58 + build/Filters/Wikilinks.hs | 59 + build/Main.hs | 7 + build/Metadata.hs | 2 + build/Pagination.hs | 48 + build/Site.hs | 362 +++++++ build/Stability.hs | 178 +++ build/Tags.hs | 123 +++ build/Utils.hs | 26 + cabal.project | 8 + cabal.project.freeze | 235 ++++ data/annotations.json | 1 + data/bibliography.bib | 199 ++++ data/chicago-notes.csl | 248 +++++ data/commonplace.yaml | 10 + levineuwirth.cabal | 60 ++ sample_music.svg | 1044 ++++++++++++++++++ spec.md | 638 +++++++++++ static/css/annotations.css | 103 ++ static/css/base.css | 217 ++++ static/css/catalog.css | 119 ++ static/css/commonplace.css | 141 +++ static/css/components.css | 1075 +++++++++++++++++++ static/css/gallery.css | 537 +++++++++ static/css/home.css | 191 ++++ static/css/images.css | 115 ++ static/css/layout.css | 213 ++++ static/css/library.css | 88 ++ static/css/memento-mori.css | 297 +++++ static/css/popups.css | 105 ++ static/css/print.css | 145 +++ static/css/reading.css | 122 +++ static/css/score-reader.css | 246 +++++ static/css/selection-popup.css | 184 ++++ static/css/sidenotes.css | 157 +++ static/css/syntax.css | 93 ++ static/css/typography.css | 615 +++++++++++ static/favicon.ico | Bin 0 -> 292774 bytes static/fonts/fira-sans-regular.woff2 | Bin 0 -> 15800 bytes static/fonts/fira-sans-semibold.woff2 | Bin 0 -> 16108 bytes static/fonts/jetbrains-mono-italic.woff2 | Bin 0 -> 19768 bytes static/fonts/jetbrains-mono-regular.woff2 | Bin 0 -> 18512 bytes static/fonts/spectral-bold-italic.woff2 | Bin 0 -> 23564 bytes static/fonts/spectral-bold.woff2 | Bin 0 -> 22264 bytes static/fonts/spectral-italic.woff2 | Bin 0 -> 21768 bytes static/fonts/spectral-regular.woff2 | Bin 0 -> 20540 bytes static/fonts/spectral-semibold-italic.woff2 | Bin 0 -> 23928 bytes static/fonts/spectral-semibold.woff2 | Bin 0 -> 22332 bytes static/gpg/pubkey.asc | 6 + static/images/canto31.jpg | Bin 0 -> 4173621 bytes static/images/link-icons/arxiv.svg | 14 + static/images/link-icons/document.svg | 3 + static/images/link-icons/doi.svg | 12 + static/images/link-icons/email.svg | 3 + static/images/link-icons/external.svg | 14 + static/images/link-icons/github.svg | 4 + static/images/link-icons/key.svg | 3 + static/images/link-icons/orcid.svg | 3 + static/images/link-icons/person.svg | 3 + static/images/link-icons/wikipedia.svg | 19 + static/js/annotations.js | 243 +++++ static/js/citations.js | 86 ++ static/js/collapse.js | 103 ++ static/js/gallery.js | 458 ++++++++ static/js/lightbox.js | 112 ++ static/js/nav.js | 37 + static/js/popups.js | 553 ++++++++++ static/js/prism.min.js | 48 + static/js/random.js | 21 + static/js/reading.js | 19 + static/js/score-reader.js | 135 +++ static/js/search.js | 22 + static/js/selection-popup.js | 369 +++++++ static/js/settings.js | 158 +++ static/js/sidenotes.js | 123 +++ static/js/theme.js | 29 + static/js/toc.js | 116 ++ templates/author-index.html | 26 + templates/blog-index.html | 17 + templates/blog-post.html | 15 + templates/commonplace.html | 35 + templates/composition.html | 72 ++ templates/default.html | 34 + templates/essay-index.html | 16 + templates/essay.html | 17 + templates/home.html | 3 + templates/library.html | 117 ++ templates/music-catalog.html | 20 + templates/page.html | 4 + templates/partials/footer.html | 12 + templates/partials/head.html | 33 + templates/partials/metadata.html | 21 + templates/partials/nav.html | 61 ++ templates/partials/page-footer.html | 67 ++ templates/partials/paginate-nav.html | 14 + templates/reading.html | 7 + templates/score-reader-default.html | 18 + templates/score-reader.html | 35 + templates/tag-index.html | 26 + tools/subset-fonts.sh | 52 + 121 files changed, 14393 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 Makefile create mode 100644 WRITING.md create mode 100644 build/Authors.hs create mode 100644 build/Backlinks.hs create mode 100644 build/Catalog.hs create mode 100644 build/Citations.hs create mode 100644 build/Commonplace.hs create mode 100644 build/Compilers.hs create mode 100644 build/Contexts.hs create mode 100644 build/Filters.hs create mode 100644 build/Filters/Code.hs create mode 100644 build/Filters/Dropcaps.hs create mode 100644 build/Filters/Images.hs create mode 100644 build/Filters/Links.hs create mode 100644 build/Filters/Math.hs create mode 100644 build/Filters/Score.hs create mode 100644 build/Filters/Sidenotes.hs create mode 100644 build/Filters/Smallcaps.hs create mode 100644 build/Filters/Typography.hs create mode 100644 build/Filters/Wikilinks.hs create mode 100644 build/Main.hs create mode 100644 build/Metadata.hs create mode 100644 build/Pagination.hs create mode 100644 build/Site.hs create mode 100644 build/Stability.hs create mode 100644 build/Tags.hs create mode 100644 build/Utils.hs create mode 100644 cabal.project create mode 100644 cabal.project.freeze create mode 100644 data/annotations.json create mode 100644 data/bibliography.bib create mode 100644 data/chicago-notes.csl create mode 100644 data/commonplace.yaml create mode 100644 levineuwirth.cabal create mode 100644 sample_music.svg create mode 100644 spec.md create mode 100644 static/css/annotations.css create mode 100644 static/css/base.css create mode 100644 static/css/catalog.css create mode 100644 static/css/commonplace.css create mode 100644 static/css/components.css create mode 100644 static/css/gallery.css create mode 100644 static/css/home.css create mode 100644 static/css/images.css create mode 100644 static/css/layout.css create mode 100644 static/css/library.css create mode 100644 static/css/memento-mori.css create mode 100644 static/css/popups.css create mode 100644 static/css/print.css create mode 100644 static/css/reading.css create mode 100644 static/css/score-reader.css create mode 100644 static/css/selection-popup.css create mode 100644 static/css/sidenotes.css create mode 100644 static/css/syntax.css create mode 100644 static/css/typography.css create mode 100644 static/favicon.ico create mode 100644 static/fonts/fira-sans-regular.woff2 create mode 100644 static/fonts/fira-sans-semibold.woff2 create mode 100644 static/fonts/jetbrains-mono-italic.woff2 create mode 100644 static/fonts/jetbrains-mono-regular.woff2 create mode 100644 static/fonts/spectral-bold-italic.woff2 create mode 100644 static/fonts/spectral-bold.woff2 create mode 100644 static/fonts/spectral-italic.woff2 create mode 100644 static/fonts/spectral-regular.woff2 create mode 100644 static/fonts/spectral-semibold-italic.woff2 create mode 100644 static/fonts/spectral-semibold.woff2 create mode 100644 static/gpg/pubkey.asc create mode 100644 static/images/canto31.jpg create mode 100644 static/images/link-icons/arxiv.svg create mode 100644 static/images/link-icons/document.svg create mode 100644 static/images/link-icons/doi.svg create mode 100644 static/images/link-icons/email.svg create mode 100644 static/images/link-icons/external.svg create mode 100644 static/images/link-icons/github.svg create mode 100644 static/images/link-icons/key.svg create mode 100644 static/images/link-icons/orcid.svg create mode 100644 static/images/link-icons/person.svg create mode 100644 static/images/link-icons/wikipedia.svg create mode 100644 static/js/annotations.js create mode 100644 static/js/citations.js create mode 100644 static/js/collapse.js create mode 100644 static/js/gallery.js create mode 100644 static/js/lightbox.js create mode 100644 static/js/nav.js create mode 100644 static/js/popups.js create mode 100644 static/js/prism.min.js create mode 100644 static/js/random.js create mode 100644 static/js/reading.js create mode 100644 static/js/score-reader.js create mode 100644 static/js/search.js create mode 100644 static/js/selection-popup.js create mode 100644 static/js/settings.js create mode 100644 static/js/sidenotes.js create mode 100644 static/js/theme.js create mode 100644 static/js/toc.js create mode 100644 templates/author-index.html create mode 100644 templates/blog-index.html create mode 100644 templates/blog-post.html create mode 100644 templates/commonplace.html create mode 100644 templates/composition.html create mode 100644 templates/default.html create mode 100644 templates/essay-index.html create mode 100644 templates/essay.html create mode 100644 templates/home.html create mode 100644 templates/library.html create mode 100644 templates/music-catalog.html create mode 100644 templates/page.html create mode 100644 templates/partials/footer.html create mode 100644 templates/partials/head.html create mode 100644 templates/partials/metadata.html create mode 100644 templates/partials/nav.html create mode 100644 templates/partials/page-footer.html create mode 100644 templates/partials/paginate-nav.html create mode 100644 templates/reading.html create mode 100644 templates/score-reader-default.html create mode 100644 templates/score-reader.html create mode 100644 templates/tag-index.html create mode 100755 tools/subset-fonts.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f10f523 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Copy this file to .env and fill in the values. +# .env is gitignored — never commit it. +# +# Used by `make deploy` to push to GitHub before rsyncing to the VPS. +# If either variable is unset, the push step is skipped (rsync still runs). + +# A GitHub fine-grained personal access token with Contents: read+write +# on the levineuwirth.org repository. +# Generate at: https://github.com/settings/tokens +GITHUB_TOKEN= + +# The GitHub repository in owner/repo format. +GITHUB_REPO=levineuwirth/levineuwirth.org diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ef93c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +dist-newstyle/ +_site/ +_cache/ +.DS_Store +.env + +# Editor backup/swap files +*~ +*.swp +*.swo + +# Data files that are generated or external (not version-controlled) +data/embeddings.json +data/similar-links.json +data/backlinks.json +data/build-stats.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ee99a96 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,117 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +`levineuwirth.org` is a personal website built as a static site using **Hakyll** (Haskell) + **Pandoc**. The spec is in `spec.md`. The site is inspired by gwern.net in architecture: sidenotes, semantic zoom, monochrome typography, Pandoc filters, no client-side tracking. + +## Build Commands + +```bash +make build # cabal run site -- build && pagefind --site _site +make deploy # build + rsync -avz --delete _site/ vps:/var/www/levineuwirth.com/ +make watch # cabal run site -- watch (live-reload dev server) +make clean # cabal run site -- clean +``` + +**Important:** Hakyll caches compiled items in `_cache/` keyed to source file mtimes. +Changing a `.hs` file (filter, compiler, context) does **not** invalidate the cache for +existing content files. Always run `make clean && make build` after any Haskell-side change, +or content will be served from the stale cache. + +The Haskell build program lives in `build/`. The entry point is `build/Main.hs`. + +## Architecture + +### Build System (`build/`) + +The Hakyll site compiler is split into focused modules: + +- `Main.hs` — entry point +- `Site.hs` — all Hakyll rules (which patterns compile to which outputs) +- `Compilers.hs` — custom Pandoc compiler wrappers +- `Contexts.hs` — Hakyll template contexts (includes auto-computed `word-count`, `reading-time`) +- `Metadata.hs` — loads YAML frontmatter + merges external JSON from `data/` +- `Tags.hs` — hierarchical tag system using Hakyll `buildTags` +- `Pagination.hs` — 20/page for blog and tag indexes; essays all on one page +- `Citations.hs` — citeproc + BibLaTeX + Chicago Author-Date; bib file at `data/bibliography.bib` +- `Filters.hs` — re-exports all Pandoc AST filter modules +- `Filters/Typography.hs` — smart quotes, dashes, etc. +- `Filters/Sidenotes.hs` — converts footnotes to sidenotes +- `Filters/Dropcaps.hs` — decorative drop capitals +- `Filters/Smallcaps.hs` — smallcaps via `smcp` OT feature +- `Filters/Wikilinks.hs` — `[[wikilink]]` syntax +- `Filters/Links.hs` — external link classification and icon injection +- `Filters/Math.hs` — simple LaTeX → Unicode at build time; complex math → KaTeX SSR (static HTML+MathML) +- `Utils.hs` — shared helpers + +### Math Pipeline + +Two-tier, no client-side JS required: +1. Simple math → Unicode/HTML via Pandoc Lua filter (inherits body font) +2. Complex math → KaTeX server-side rendering → static HTML+MathML (KaTeX CSS loaded conditionally, only on pages that use math) + +### CSS (`static/css/`) + +| File | Purpose | +|------|---------| +| `base.css` | CSS variables, palette, dark mode (`[data-theme="dark"]` + `prefers-color-scheme`) | +| `typography.css` | Spectral OT features: smallcaps (`smcp`), ligatures, figure styles, dropcaps | +| `layout.css` | Three-column layout: sticky TOC (left) | body 650–700px (center) | sidenotes (right). Collapses on narrow screens. | +| `sidenotes.css` | Sidenote positioning | +| `popups.css` | Link preview popups | +| `syntax.css` | Monochrome code highlighting (JetBrains Mono) | +| `components.css` | Two-row nav, metadata block, collapsibles | + +### JavaScript (`static/js/`) + +| File | Source | Purpose | +|------|--------|---------| +| `sidenotes.js` | Adopted — Said Achmiz (MIT) | Sidenote positioning | +| `popups.js` | Forked + simplified — Said Achmiz (MIT) | Internal previews, Wikipedia, citation previews | +| `theme.js` | Original | Dark/light toggle with `localStorage` | +| `toc.js` | Original | Sticky TOC + scroll tracking via `IntersectionObserver` | +| `search.js` | Original | Pagefind integration | +| `nav.js` | Original | Portal row expand/collapse (state in `localStorage`) | +| `collapse.js` | Original | Section collapsing | + +### Typography + +- **Body:** Spectral (SIL OFL) — self-hosted WOFF2, subsetted with full OT features (`liga`, `smcp`, `onum`, etc.) +- **UI/Headers:** Fira Sans (SIL OFL) — smallcaps for primary nav row +- **Code:** JetBrains Mono (SIL OFL) + +All fonts self-hosted from source (not Google Fonts, which strips OT features). Subset with `pyftsubset`. + +### Navigation Structure + +``` +Home | Me | New | Index | [🔍] ← primary row (always visible), Fira Sans smallcaps +──────────────────────────────── +▼ Fiction | Miscellany | Music | Nonfiction | Poetry | Research ← portal row +``` + +Portal row collapsed by default; expansion state in `localStorage`. + +### Content Portals + +Six content portals map to `content/` subdirectories: Fiction, Miscellany, Music, Nonfiction, Poetry, Research. Essays live under Nonfiction; blog posts are a separate stream. + +### Metadata + +Frontmatter keys for Phase 1: `title`, `created`, `modified`, `status`, `tags`, `abstract`. +Auto-computed at build: `word-count`, `reading-time`. +External data loaded from `data/annotations.yaml` and `data/bibliography.bib`. + +### Deployment + +Local build → `_site/` → `rsync` to VPS. No server-side processing; nginx serves static files. No Docker. + +## Key Design Constraints + +- **No tracking, no analytics, no fingerprinting** — enforced at the nginx CSP header level too +- **No client-side math rendering** — KaTeX runs at build time +- **Sidenotes right-column only** (matching gwern's `useLeftColumn: () => false`) +- **Configuration is code** — the Makefile and Haskell build system are the deployment pipeline +- **Content license:** CC BY-SA-NC 4.0 | **Code license:** MIT diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..73dd4d4 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +.PHONY: build deploy watch clean dev + +# Source .env for GITHUB_TOKEN and GITHUB_REPO if it exists. +# .env format: KEY=value (one per line, no `export` prefix, no quotes needed). +-include .env +export + +build: + @git add content/ + @git diff --cached --quiet || git commit -m "auto: $$(date -u +%Y-%m-%dT%H:%M:%SZ)" + cabal run site -- build + pagefind --site _site + > IGNORE.txt + +deploy: build + @if [ -z "$(GITHUB_TOKEN)" ] || [ -z "$(GITHUB_REPO)" ]; then \ + echo "Skipping push: set GITHUB_TOKEN and GITHUB_REPO in .env"; \ + else \ + git push "https://$(GITHUB_TOKEN)@github.com/$(GITHUB_REPO).git" main; \ + fi + rsync -avz --delete _site/ vps:/var/www/levineuwirth.org/ + +watch: + cabal run site -- watch + +clean: + cabal run site -- clean + +dev: + cabal run site -- clean + cabal run site -- build + python3 -m http.server 8000 --directory _site diff --git a/WRITING.md b/WRITING.md new file mode 100644 index 0000000..2d61bc0 --- /dev/null +++ b/WRITING.md @@ -0,0 +1,745 @@ +# Writing Guide + +Reference for creating content on levineuwirth.org. Covers file placement, all +frontmatter fields, and every authoring feature available in the Markdown source. + +--- + +## File placement + +| Type | Location | Output URL | +|------|----------|------------| +| Essay | `content/essays/my-essay.md` | `/essays/my-essay.html` | +| Blog post | `content/blog/my-post.md` | `/blog/my-post.html` | +| Poetry | `content/poetry/my-poem.md` | `/poetry/my-poem.html` | +| Fiction | `content/fiction/my-story.md` | `/fiction/my-story.html` | +| Composition | `content/music/{slug}/index.md` | `/music/{slug}/` | +| Standalone page | `content/my-page.md` | `/my-page.html` | +| Standalone page (with co-located assets) | `content/my-page/index.md` | `/my-page.html` | + +File names become URL slugs. Use lowercase, hyphen-separated words. + +If a standalone page embeds co-located SVG score fragments or other relative assets, +place it in its own directory (`content/my-page/index.md`) rather than as a flat file. +Score fragment paths are resolved relative to the source file's directory; a flat +`content/my-page.md` would resolve them from `content/`, which is wrong. + +--- + +## Frontmatter + +Every file begins with a YAML block fenced by `---`. Keys are read by Hakyll +and passed to templates; unknown keys are silently ignored and can be used as +custom template variables. + +### Essays + +```yaml +--- +title: "The Title of the Essay" +date: 2026-03-15 # required; used for ordering, feed, and display +abstract: > # optional; shown in the metadata block and link previews + A one-paragraph description of the piece. +tags: # optional; see Tags section + - nonfiction + - nonfiction/philosophy +authors: # optional; overrides the default "Levi Neuwirth" link + - "Levi Neuwirth | /me.html" + - "Collaborator | https://their.site" + - "Plain Name" # URL optional; omit for plain-text credit +further-reading: # optional; see Citations section + - someKey + - anotherKey +bibliography: data/custom.bib # optional; overrides data/bibliography.bib +csl: data/custom.csl # optional; overrides Chicago Author-Date +no-collapse: true # optional; disables collapsible h2/h3 sections +js: scripts/my-widget.js # optional; per-page JS file (see Page scripts) +# js: [scripts/a.js, scripts/b.js] # or a list + +# Epistemic Effort — all optional; the entire section is hidden unless `status` is set +status: "Working model" # Draft | Working model | Durable | Refined | Superseded | Deprecated +confidence: 72 # 0–100 integer (%) +importance: 3 # 1–5 integer (rendered as filled/empty dots ●●●○○) +evidence: 2 # 1–5 integer (same) +scope: average # personal | local | average | broad | civilizational +novelty: moderate # conventional | moderate | idiosyncratic | innovative +practicality: moderate # abstract | low | moderate | high | exceptional +confidence-history: # list of integers; trend arrow derived from last two entries + - 55 + - 63 + - 72 + +# Version history — optional; falls back to git log, then to date frontmatter +history: + - date: "2026-03-01" # ISO date as a quoted string (prevent YAML date parsing) + note: Initial draft + - date: "2026-03-14" + note: Expanded typography section; added citations +--- +``` + +### Blog posts + +Same fields as essays. `date` formats the `