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 `