# 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` |
| Essay (with co-located assets) | `content/essays/my-essay/index.md` | `/essays/my-essay/index.html` |
| Blog post | `content/blog/my-post.md` | `/blog/my-post.html` |
| Poetry | `content/poetry/my-poem.md` | `/poetry/my-poem.html` |
| Poetry collection | `content/poetry/collection-name/*.md` | `/poetry/collection-name/*.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` |
| Draft essay | `content/drafts/essays/my-draft.md` | `/drafts/essays/my-draft.html` (dev only) |

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.

---

## Drafts

In-progress essays go under `content/drafts/essays/`. Both flat files
(`content/drafts/essays/my-draft.md`) and directory-based essays
(`content/drafts/essays/my-draft/index.md`) are supported.

Drafts are **included** in `make watch` and `make dev` (which set `SITE_ENV=dev`)
and **excluded** from every production build (`make build`, `make deploy`).
They are also invisible to tags, author pages, backlinks, stats, and feeds.

To preview a draft:

```bash
make watch    # Hakyll live-reload with drafts visible
make dev      # clean build + Python HTTP server with drafts visible
```

When the draft is ready, move it into `content/essays/`.

---

## 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
affiliation:              # optional; shown below author in metadata block
  - "Brown University | https://cs.brown.edu"
  - "Some Research Lab"   # URL optional; scalar string also accepted
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
repository: https://git.levineuwirth.org/levi/repo  # optional; "Repository" link in metadata
preprint: /papers/my-essay.pdf  # optional; "Preprint" link in metadata (typeset PDF version)
js: scripts/my-widget.js        # optional; per-page JS file (see Page scripts)
# js: [scripts/a.js, scripts/b.js]  # or a list

# Epistemic profile — 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 `<time>` element in the post header
and blog index. Posts appear in `/feed.xml`.

### Poetry

Same fields as essays. Poetry uses a narrow-measure codex reading mode
(`reading.html`): 52ch measure, 1.85 line-height, stanza spacing, no drop cap.
`poetryCompiler` enables `Ext_hard_line_breaks` — each source newline becomes
a `<br>`, so verse lines render without trailing-space tricks.

**Poetry collections** — poems can be grouped into subdirectories:

```
content/poetry/shakespeare-sonnets/
├── index.md            ← collection landing page (uses pageCtx)
├── sonnet-1.md         ← individual poem
├── sonnet-2.md
└── …
```

Collection index pages (`content/poetry/*/index.md`) compile as standalone
pages. Poems inside collections (`content/poetry/*/*.md`, excluding
`index.md`) compile with `poetryCompiler` just like flat poems.

**External / non-original poems** — use `poet:` instead of `authors:` to credit
an external author without generating a (broken) author index page:

```yaml
---
title: Sonnet 60
date: 1609-05-20
poet: William Shakespeare
tags: [poetry]
---
```

`poet:` is a plain string. The metadata block shows "by William Shakespeare" as
plain text. Do not use `authors:` for poems you did not write; that would create
an `/authors/william-shakespeare/` index page linking to the poem.

### Fiction

Same fields as essays. Fiction uses the same codex reading mode as poetry:
62ch measure, chapter drop caps + smallcaps lead-in on `h2 + p`.

### Standalone pages

Only `title` is required. Pages use `pageCtx`: no TOC, no bibliography, no
reading-time, no epistemic footer.

```yaml
---
title: About
---
```

Add `js:` for any page-specific interactivity (see Page scripts).

### Music compositions

Compositions live in their own directory. See the Music section for full details.

```yaml
---
title: "Symphony No. 1"
date: 2026-01-15
abstract: >
  A four-movement work for large orchestra.
tags: [music]
instrumentation: "orchestra (2222/4231/timp+2perc/str)"
duration: "ca. 32'"
premiere: "2026-05-20"
commissioned-by: "—"              # optional
recording: audio/full-recording.mp3  # optional; full-piece audio player
pdf: scores/symphony.pdf          # optional; download link
category: orchestral              # orchestral | chamber | solo | vocal | choral | electronic
featured: true                    # optional; appears in Featured section of /music/
score-pages:                      # required for score reader; omit if no score
  - scores/page-01.svg
  - scores/page-02.svg
movements:                        # optional
  - name: "I. Allegro"
    page: 1                       # 1-indexed starting page in the reader
    duration: "10'"
    audio: audio/movement-1.mp3   # optional; per-movement player
  - name: "II. Adagio"
    page: 18
    duration: "12'"
---
```

---

## Tags

Tags use slash-separated hierarchy. `nonfiction/philosophy` expands to both
`nonfiction` and `nonfiction/philosophy`, so the piece appears on both index
pages.

```yaml
tags:
  - nonfiction/philosophy
  - research/epistemology
```

The top-level segment maps to a **portal** in the nav:

| Portal | URL |
|--------|-----|
| AI | `/ai/` |
| Fiction | `/fiction/` |
| Miscellany | `/miscellany/` |
| Music | `/music/` |
| Nonfiction | `/nonfiction/` |
| Poetry | `/poetry/` |
| Research | `/research/` |
| Tech | `/tech/` |

Tag index pages are paginated at 20 items and auto-generated on build.

---

## Authors

The `authors` key controls the author line in the metadata block. When omitted,
it defaults to "Levi Neuwirth" linking to `/authors/levi-neuwirth/`.

Author pages are generated at `/authors/{slug}/` and list all attributed content.

Pipe syntax: `"Name | URL"` — name is the link text, URL is the `href`.
The URL part is optional.

---

## Citations

The citation pipeline uses Chicago Author-Date style. The bibliography lives at
`data/bibliography.bib` (BibLaTeX format) by default; override per-page with
`bibliography` and `csl`.

### Inline citations

```markdown
This claim is contested.[@smith2020]

Multiple sources agree.[@jones2019; @brown2021]
```

Inline citations render as numbered superscripts `[1]`, `[2]`, etc. The
bibliography section appears automatically in the page footer. `citations.js`
adds hover previews showing the full reference.

### Further reading

Keys under `further-reading` appear in a separate **Further Reading** section
in the page footer. These are not cited inline.

```yaml
further-reading:
  - keyNotCitedInline
  - anotherBackgroundSource
```

A key can appear both inline and in `further-reading`; it is numbered in the
bibliography and not duplicated.

---

## Footnotes → sidenotes

Standard Markdown footnotes are converted to sidenotes at build time.

```markdown
This sentence has a note.[^1]

[^1]: The note content, which may contain **bold**, *italic*, links, etc.
```

On wide viewports the note floats into the right margin. On narrow viewports
it falls back to a standard footnote. Sidenotes are numbered automatically.

Avoid block-level elements (headings, lists) inside footnotes.

---

## Wikilinks

Internal links can be written as wikilinks. The title is slugified to produce
the URL.

```markdown
[[Page Title]]             → links to /page-title
[[Page Title|Link text]]   → links to /page-title with custom display text
```

Slug rules: lowercase, spaces → hyphens, non-alphanumeric characters removed.
`[[My Essay (2024)]]` → `/my-essay-2024`.

Use standard Markdown link syntax `[text](/path)` for any link whose path is
not derivable from the page title.

---

## Transclusion

Embed the rendered content of another page (or a section of it) inline using
`{{slug}}` directives. The directive must be on its own line.

```markdown
{{my-essay}}                 → embeds the full body of /my-essay.html
{{essays/deep-dive}}         → embeds /essays/deep-dive.html
{{my-essay#introduction}}    → embeds only the "introduction" section
```

At build time, `Filters.Transclusion` rewrites these to `<div class="transclude"
data-src="..." [data-section="..."]></div>` placeholders. At runtime,
`transclude.js` fetches the target page and injects the content.

Transclusions inside code blocks or inline prose are not replaced — only
block-level directives (sole content of a line) are processed.

---

## PDF Embeds

Embed a hosted PDF in a full PDF.js viewer (page navigation, zoom, text
selection) using the `{{pdf:...}}` directive on its own line:

```markdown
{{pdf:/papers/smith2023.pdf}}
{{pdf:/papers/smith2023.pdf#5}}        ← open at page 5 (bare integer)
{{pdf:/papers/smith2023.pdf#page=5}}   ← explicit form, same result
```

The directive produces an iframe pointing at the vendored PDF.js viewer at
`/pdfjs/web/viewer.html?file=...`.  The PDF must be served from the same
origin (i.e. it must be a file you host).

**Storing papers.** Drop PDFs in `static/papers/`; Hakyll's static rule copies
everything under `static/` to `_site/` unchanged.  Reference them as
`/papers/filename.pdf`.

**One-time vendor setup.** PDF.js is not included in the repo.  Install it once:

```bash
npm install pdfjs-dist
mkdir -p static/pdfjs
cp -r node_modules/pdfjs-dist/web   static/pdfjs/
cp -r node_modules/pdfjs-dist/build static/pdfjs/
rm -rf node_modules package-lock.json
```

Then commit `static/pdfjs/` (it is static and changes only when you want to
upgrade PDF.js).  Hakyll's existing `static/**` rule copies it through without
any new build rules.

**Optional: disable the "Open file" button** in the viewer.  Edit
`static/pdfjs/web/viewer.html` and set `AppOptions.set('disableOpenFile', true)`
in the `webViewerLoad` callback, or add a thin CSS rule to `viewer.css`:

```css
#openFile, #secondaryOpenFile { display: none !important; }
```

---

## Math

Pandoc parses LaTeX math and wraps it in `class="math inline"` / `class="math display"`
spans. KaTeX CSS is loaded conditionally on pages that contain math — this styles the
pre-rendered output. Client-side KaTeX JS rendering is not yet loaded; complex math
will appear as LaTeX source. Build-time server-side rendering is planned but not yet
implemented. Simple math currently renders through Pandoc's built-in KaTeX span output.

`math: true` is auto-set for all essays and blog posts. Standalone pages that use
math must set it explicitly in frontmatter to load the KaTeX CSS.

| Syntax | Usage |
|--------|-------|
| `$...$` | Inline math |
| `$$...$$` on its own line | Display math |

```markdown
The quadratic formula is $x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$.

$$
\int_0^\infty e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2}
$$
```

---

## Links

Internal links: standard Markdown or wikilinks. Internal links get a
`link-internal` class and an internal icon.

External links are classified automatically at build time:

- Opened in a new tab (`target="_blank" rel="noopener noreferrer"`)
- Decorated with a domain icon via CSS `mask-image`

Links to sibling subdomains (e.g. `git.levineuwirth.org`) are classified as
external — only `levineuwirth.org` and `www.levineuwirth.org` are treated as
the content host.

| Domain | Icon |
|--------|------|
| `wikipedia.org` | Wikipedia |
| `arxiv.org` | arXiv |
| `doi.org` | DOI |
| `worldcat.org` | WorldCat |
| `orcid.org` | ORCID |
| `archive.org` | Internet Archive |
| `github.com` | GitHub |
| `git.levineuwirth.org` | Forgejo |
| `tensorflow.org` | TensorFlow |
| `anthropic.com` | Anthropic |
| `openai.com` | OpenAI |
| `twitter.com` / `x.com` | Twitter/X |
| `reddit.com` | Reddit |
| `youtube.com` / `youtu.be` | YouTube |
| `tiktok.com` | TikTok |
| `substack.com` | Substack |
| `news.ycombinator.com` | Hacker News |
| `nytimes.com` | New York Times |
| `nasa.gov` | NASA |
| `apple.com` | Apple |
| Everything else | Generic external arrow |

### Link preview popups

Hovering over a link shows a context-aware popup. All automatic — no
markup needed.

| Target | Popup content |
|--------|--------------|
| Internal page | Title, abstract, authors, tags, word count, reading time |
| Citation marker (`[1]`) | Full bibliography reference (multi-cite groups supported) |
| Wikipedia | Lead section extract via MediaWiki API |
| arXiv | Title, authors, abstract via Atom API |
| DOI / CrossRef | Title, authors, journal, year, abstract |
| GitHub | Repo name, description, language, stars |
| Forgejo (`git.levineuwirth.org`) | Repo name, description, language, stars |
| Open Library | Book title, description |
| bioRxiv / medRxiv | Title, authors, abstract |
| YouTube | Video title, channel name (oEmbed) |
| Internet Archive | Title, creator, description |
| PubMed | Title, authors, journal, year |
| PDF link | First-page thumbnail (from build-time `pdftoppm`) |
| Epistemic jump link (`#epistemic`) | Clone of the full epistemic profile |
| Epistemic term label (`data-ep-term`) | Term definition from colophon |
| PGP signature link | ASCII armor of the `.sig` file |

**Custom annotations** — author-defined previews for any URL. Add entries
to `data/annotations.json`:

```json
{
  "https://example.com/article": {
    "title": "Article Title",
    "annotation": "A brief note about why this link is relevant."
  }
}
```

Annotation entries take priority over all other popup providers.

Popups are disabled on touch-primary devices and inside nav/TOC/footer
elements.

---

## Code

Fenced code blocks with a language identifier get syntax highlighting (JetBrains
Mono). The language annotation also enables the selection popup's docs-search
feature (maps to MDN, Hoogle, docs.python.org, etc.).

````markdown
```haskell
fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)
```
````

Inline code: `` `like this` ``

---

## Images

Place images in `static/images/`. All images automatically get `loading="lazy"`.

```markdown
![Alt text](/images/my-image.png)
```

For a captioned figure, add a title string. Pandoc wraps it in `<figure>`/`<figcaption>`:

```markdown
![Alt text](/images/my-image.png "Caption shown below.")
```

**Lightbox:** standalone images automatically get `data-lightbox="true"`.
Clicking opens a fullscreen overlay; close with ×, click outside, or Escape.
Images inside hyperlinks get lazy loading only — no lightbox.

---

## Section collapsing

By default, every `h2` and `h3` heading in an essay gets a collapse toggle.
State persists in `localStorage`.

To disable on a specific page:

```yaml
no-collapse: true
```

---

## Exhibits and annotations

Pandoc fenced divs (`:::`) create structured callout blocks.

### Exhibits

Always-visible numbered blocks with overlay zoom on click. Use for equations or
proofs you want to reference structurally. Listed under their parent heading
in the TOC.

```markdown
::: {.exhibit .exhibit--equation}
$$E = mc^2$$
:::

::: {.exhibit .exhibit--proof}
*Proof.* Suppose for contradiction that …  ∎
:::
```

### Annotation callouts

Editorial sidebars — context, caveats, tangential detail.

```markdown
::: {.annotation .annotation--static}
Always visible. Use for important caveats or context.
:::

::: {.annotation .annotation--collapsible}
Collapsed by default. Use for tangential detail.
:::
```

---

## Score fragments

Inline SVG music notation, integrated with the gallery/exhibit system. Clicking
a fragment opens the shared overlay alongside any math exhibits on the page.

```markdown
::: {.score-fragment score-name="Main Theme, mm. 1–8" score-caption="The opening gesture."}
![](scores/main-theme.svg)
:::
```

Both attributes are optional, but `score-name` is strongly recommended — it
drives the overlay label and the TOC badge.

The image path is resolved **relative to the source file's directory**:

| Source file | SVG path | Reference as |
|---|---|---|
| `content/essays/my-essay.md` | `content/essays/scores/theme.svg` | `scores/theme.svg` |
| `content/music/symphony/index.md` | `content/music/symphony/scores/motif.svg` | `scores/motif.svg` |
| `content/me/index.md` | `content/me/scores/vln.svg` | `scores/vln.svg` |

SVGs are inlined at build time. Black `fill`/`stroke` values are replaced with
`currentColor` so notation renders correctly in dark mode.

---

## Excerpts

Pull-quotes from other works, displayed as indented blockquotes with a small
attribution line and a stretched invisible link covering the whole figure.

### Poem excerpt

Use when quoting verse that has a dedicated page on the site. The entire figure
becomes a click target pointing to the poem; the attribution line remains an
independent link.

```html
<figure class="poem-excerpt">
<blockquote>

Like as the waves make towards the pebbled shore,
So do our minutes hasten to their end;

</blockquote>
<figcaption><a href="/poetry/sonnet-60.html">William Shakespeare — <em>Sonnet 60</em></a></figcaption>
</figure>
```

The blockquote renders with the default dashed left border and italic muted text.
No box or background. The `figcaption` is small-caps and left-aligned.

### Prose excerpt

For quoting prose or for excerpts that do not have a page on the site:

```html
<figure class="prose-excerpt">
<blockquote>

The passage you want to quote goes here.

</blockquote>
<figcaption><a href="https://example.com">Author — <em>Source Title</em></a></figcaption>
</figure>
```

`prose-excerpt` is full-width (`width: auto; max-width: 100%`) rather than
`fit-content`-wide like `poem-excerpt`. Both reset the image-figure box styles
(no background, no border, no padding).

### Commonplace book

The `/commonplace` page is a YAML-driven quotation collection. Add entries to
`data/commonplace.yaml`; the page rebuilds automatically.

```yaml
- text: |-
    Like as the waves make towards the pebbled shore,
    So do our minutes hasten to their end;
    Each changing place with that which goes before,
    In sequent toil all forwards do contend.
  attribution: William Shakespeare
  source: Sonnet 60
  source-url: /poetry/sonnet-60.html
  tags: [time, mortality]
  commentary: >
    Optional note. Shown below the quote in muted italic. Omit entirely if
    not needed — most entries will not have one.
  date-added: 2026-03-17
```

| Field | Required | Notes |
|-------|----------|-------|
| `text` | yes | Use `|-` block scalar; newlines become `<br>` |
| `attribution` | yes | Author name, plain text |
| `source` | no | Title of the source work |
| `source-url` | no | Makes `source` a link |
| `tags` | no | Separate from content tags; used for "by theme" grouping |
| `commentary` | no | Your own remark on the passage |
| `date-added` | no | ISO date; used for chronological sort |

The page has a **by theme / chronological** toggle (state persists in
`localStorage`). Untagged entries appear under "miscellany" in the themed view.

---

## Music

### Catalog index (`/music/`)

`content/music/index.md` is the catalog homepage. Write prose about your
compositional work in the body; provide an `abstract` for the intro line.
The composition listing is auto-generated from all `content/music/*/index.md`
files — no manual list needed.

```yaml
---
title: Music
abstract: Compositions spanning orchestral, chamber, and solo writing.
tags: [music]
---

[Your prose here — influences, preoccupations, approach to the craft.]
```

### Composition pages

Each composition lives in its own subdirectory:

```
content/music/symphony-no-1/
├── index.md            ← frontmatter + program notes prose
├── scores/
│   ├── page-01.svg     ← one file per score page
│   └── symphony.pdf    ← optional PDF download
└── audio/
    ├── full.mp3        ← optional full-piece recording
    └── movement-1.mp3  ← optional per-movement recordings
```

Two URLs are generated automatically from one source:
- `/music/symphony-no-1/` — prose landing page with metadata, audio players, movements
- `/music/symphony-no-1/score/` — minimal page-turn score reader

The score reader is only generated when `score-pages` is non-empty.

**Catalog indicators** — the `/music/` catalog auto-derives:
- ◼ (score available): `score-pages` list is non-empty
- ♫ (recording available): `recording` key is present, or any movement has `audio`

**Catalog grouping** — `category` controls which section the work appears in.
Valid values: `orchestral`, `chamber`, `solo`, `vocal`, `choral`, `electronic`.
Anything else appears under "Other". Omitting `category` defaults to "other".

**Featured works** — set `featured: true` to also appear in the Featured section
at the top of the catalog.

---

## Page scripts

For pages that need custom JavaScript (interactive widgets, visualisations, etc.),
place the JS file alongside the content and reference it via the `js:` frontmatter
key. The file is copied to `_site/` and injected as a deferred `<script>` at the
bottom of `<body>`.

```yaml
js: scripts/memento-mori.js          # single file
```

or a list:

```yaml
js:
  - scripts/widget-a.js
  - scripts/widget-b.js
```

Paths are relative to the content file. A composition at
`content/music/symphony/index.md` with `js: scripts/widget.js` serves the
script at `/music/symphony/scripts/widget.js`.

No changes to the build system are needed — the `content/**/*.js` glob rule
copies all JS files from `content/` to `_site/` automatically.

---

## Epistemic profile

The epistemic footer section appears when `status` is set. All other fields
are optional and are shown or hidden independently.

| Field | Compact rows | Expanded `<dl>` |
|-------|-------------|------------------|
| *(trust score)* | bordered chip + small-caps "trust" label on the primary row | — |
| `status` | small-caps chip on the primary row, alongside the trust chip | — |
| `confidence` | `· 72% confidence` | — |
| `importance` | `· ●●●○○ importance` | — |
| `evidence` | `· ●●○○○ evidence quality` | — |
| `scope` | `· average scope` (if set) | — |
| `novelty` | `· moderate novelty` (if set) | — |
| `practicality` | `· moderate practicality` (if set) | — |
| `stability` | — | auto-computed from git history |
| `last-reviewed` | — | most recent commit date |
| `confidence-trend` | — | ↑/↓/→ from last two `confidence-history` entries |

The **trust score** is auto-computed as `confidence × 0.6 + ((evidence − 1) / 4) × 0.4`,
clamped 0–100. It is deliberately narrow: it answers "how much should you trust the
central claim?" and nothing else. Importance, scope, novelty, and practicality are
*not* folded in — they are orientation signals shown alongside the chip so a high
trust score on a personal essay cannot be misread as broad significance.

**Stability** is auto-computed from `git log --follow` at every build. The
heuristic: ≤1 commits or age <14 days → *volatile*; ≤5 commits and age <90
days → *revising*; ≤15 commits or age <365 days → *fairly stable*; ≤30
commits or age <730 days → *stable*; otherwise → *established*.

To pin a manual stability label for one build, add the file path to `IGNORE.txt`
(one path per line). The file is cleared automatically by `make build`.

---

## Version history

The version history footer section uses a three-tier fallback:

1. **`history:` frontmatter** — your authored notes, shown exactly as written.
2. **Git log** — if no `history:` key, dates are extracted from `git log --follow`.
   Entries have no message (date only).
3. **`date:` frontmatter** — if git has no commits for the file, falls back to
   the `date` field as a single "Created" entry.

`make build` auto-commits `content/` before running the Hakyll build, so the
git log stays current without manual commits.

Write authored notes when the git log would be too noisy or insufficiently
descriptive:

```yaml
history:
  - date: "2026-03-01"
    note: Initial draft
  - date: "2026-03-14"
    note: Expanded section 3; incorporated feedback from peer review
```

---

## Typography features

Applied automatically at build time; no markup needed.

| Feature | What it does |
|---------|-------------|
| Smart quotes | `"foo"` / `'bar'` → curly quotes |
| Em dashes | `---` → — |
| En dashes | `--` → – |
| Smallcaps | `[TEXT]{.smallcaps}` → `font-variant-caps: small-caps` |
| Drop cap | First letter of the first `<p>` gets a decorative large initial |
| Explicit drop cap | `::: dropcap` fenced div applies drop cap to any paragraph |
| Ligatures | Spectral `liga`, `dlig` OT features active for body text |
| Old-style figures | Spectral `onum` active; use `.lining-nums` class to override |

To mark a phrase as small caps explicitly:

```markdown
The [CPU]{.smallcaps} handles this.
```

### Explicit drop cap

Wrap any paragraph in a `::: dropcap` fenced div to get a drop cap regardless
of its position in the document. The first line is automatically rendered in
small caps via `::first-line { font-variant-caps: small-caps }`.

```markdown
::: dropcap
A personal website is not a publication. It is a position — something you
inhabit, argue from, and occasionally revise in public.
:::
```

Write in normal mixed case. The CSS applies `font-variant-caps: small-caps` to
the entire first rendered line, converting lowercase letters to small-cap glyphs.
Use `[WORD]{.smallcaps}` spans to force specific words into small-caps anywhere
in the paragraph.

A paragraph that immediately follows a `::: dropcap` block will be indented
correctly (`text-indent: 1.5em`), matching the paragraph-after-paragraph rule.

---

## Text selection popup

Selecting any text (≥ 2 characters) shows a context-aware toolbar after 450 ms.

| Context | Buttons |
|---------|---------|
| Prose (multi-word) | Annotate · BibTeX · Copy · DuckDuckGo · Here · \[Translate\] · Wikipedia |
| Prose (single word) | Annotate · BibTeX · Copy · Define · DuckDuckGo · Here · \[Translate\] · Wikipedia |
| Math | Copy · nLab · OEIS · Wolfram |
| Code (known language) | Copy · \<MDN / Hoogle / Docs…\> |
| Code (unknown) | Copy |

**BibTeX** generates a `@online{...}` BibLaTeX entry with the selected text in
`note={\enquote{...}}` and copies it to the clipboard. **Define** opens English
Wiktionary. **Here** opens the Pagefind search page pre-filled with the selection.
**Translate** appears only for selections inside a non-English `[lang]` subtree
(see below) and opens DeepL with the source lang pre-set and the target as English.

---

## Non-English passages

Wrap non-English text in a Pandoc fenced div with a `lang` attribute. The
primary subtag is a BCP-47 code (`es`, `fr`, `la`, `de`, `zh`, …):

```markdown
::: {lang="es"}
> *El universo (que otros llaman la Biblioteca) se compone de un número
>  indefinido, y tal vez infinito, de galerías hexagonales…*
:::
```

Pandoc emits `<div lang="es">…</div>`. For inline passages inside an
otherwise-English paragraph, use the span form:

```markdown
He opened with a cheerful [bonjour, mon ami]{lang="fr"} and kept going.
```

which produces `<span lang="fr">…</span>`.

The page root is `<html lang="en">`, so any subtree with a different primary
lang subtag activates the **Translate** button in the selection popup —
clicking it opens DeepL with the detected source language and English as the
target. Languages DeepL does not support (e.g. Latin) fall back to DeepL's
auto-detect. Matching the page root (`lang="en"`) does nothing — there is no
point translating English into English.

---

## Visualizations

Two types of figure are supported, authored as fenced divs. The Python script
runs at build time via the Pandoc filter; no client-side computation is needed
for static figures.

### Static figures (matplotlib)

```markdown
::: {.figure script="figures/my-plot.py" caption="Caption text."}
:::
```

The script path is resolved relative to the source file's directory. It should
import `viz_theme` from `tools/` and write SVG to stdout:

```python
import sys
sys.path.insert(0, 'tools')
from viz_theme import apply_monochrome, save_svg
import matplotlib.pyplot as plt

apply_monochrome()
fig, ax = plt.subplots()
ax.plot([1, 2, 3], [1, 4, 9])
save_svg(fig)
```

`apply_monochrome()` sets transparent backgrounds and pure black elements so
the figure inherits the page's dark/light mode via CSS `currentColor`.
Multi-series charts should use `LINESTYLE_CYCLE` instead of color:

```python
from viz_theme import apply_monochrome, save_svg, LINESTYLE_CYCLE
apply_monochrome()
fig, ax = plt.subplots()
for i, style in enumerate(LINESTYLE_CYCLE[:3]):
    ax.plot(x, data[i], **style, label=f"Series {i+1}")
save_svg(fig)
```

### Interactive figures (Altair / Vega-Lite)

```markdown
::: {.visualization script="figures/my-chart.py" caption="Caption text."}
:::
```

The script outputs Vega-Lite JSON to stdout:

```python
import sys, json
import altair as alt
import pandas as pd

df = pd.read_csv('figures/data.csv')
chart = alt.Chart(df).mark_line().encode(x='year:O', y='value:Q')
print(json.dumps(chart.to_dict()))
```

The site's monochrome Vega config is applied automatically, overriding the
spec's own `config`. Dark mode re-renders automatically.

Add `viz: true` to the frontmatter of any page using `.visualization` divs —
this loads the Vega CDN scripts:

```yaml

viz: true
```

Pages with only static `.figure` divs do not need `viz: true`.

---

## Page footer sections

Essays get a structured footer. Sections with no data are hidden.

| Section | Shown when |
|---------|-----------|
| Version history | Always (falls back through three-tier system) |
| Epistemic | `status` frontmatter key is present |
| Bibliography | At least one inline citation |
| Further Reading | `further-reading` key is present |
| Backlinks | Other pages link to this page |
| Related | Similar pages exist (embedding-based; computed at build time) |

Backlinks are auto-generated at build time. No markup needed — any internal
link from another page creates an entry here, showing the source title and the
surrounding paragraph as context.

Related pages are computed by `tools/embed.py` using semantic embeddings
(`nomic-embed-text-v1.5` + FAISS). Runs automatically during `make build`
when the Python environment is set up (`uv sync`). No markup needed.

---

## Auto-generated pages

These pages are built automatically and require no content files or markup:

| Page | URL | Description |
|------|-----|-------------|
| Essay index | `/essays/` | All essays, newest first |
| Blog index | `/blog/` | Paginated blog posts |
| New | `/new.html` | All content types sorted by date, newest first |
| Library | `/library.html` | All content grouped by portal (AI, Fiction, Music, etc.) |
| Build telemetry | `/build/` | Corpus stats, word-length distribution, tag frequencies, link analysis, epistemic coverage, output metrics, repository overview, build timing, and a 52-week writing activity heatmap |
| Tag indexes | `/<tag>/` | Paginated pages per tag, auto-generated |
| Author indexes | `/authors/<slug>/` | All content attributed to an author |
| Random manifest | `/random-pages.json` | JSON array of page URLs for the random-page button |
| Atom feeds | `/feed.xml`, `/music/feed.xml` | All content feed + music-only feed |
| Search | `/search.html` | Pagefind full-text search + client-side semantic search (`nomic-embed-text-v1.5` ONNX model) |

---

## Build

```bash
make build    # auto-commit content/, convert images, PDF thumbs, compile,
              #   run pagefind + embeddings, clear IGNORE.txt
make sign     # GPG detach-sign every _site/**/*.html → .html.sig
make deploy   # clean + build + sign + rsync to VPS + git push
make watch    # Hakyll live-reload dev server (SITE_ENV=dev — includes drafts)
make dev      # clean build + Python HTTP server (SITE_ENV=dev — includes drafts)
make clean    # wipe _site/ and _cache/
```

`make watch` and `make dev` set `SITE_ENV=dev`, which includes drafts under
`content/drafts/essays/` in the build. All other targets exclude drafts.

`make watch` hot-reloads changes to Markdown, CSS, JS, and templates.
**After any change to a `.hs` file, always run `make clean && make build`** —
Hakyll's cache is keyed to source file mtimes and will serve stale output after
Haskell-side changes.

`make deploy` requires `VPS_USER`, `VPS_HOST`, and `VPS_PATH` to be set in
`.env` (see `.env.example`). It runs `clean → build → sign`, sends a desktop
notification, rsyncs `_site/` to the VPS, and pushes to `origin main`.

**GPG signing:** `make sign` and `make deploy` require the signing subkey
passphrase to be cached. Run once per boot (or per 24h expiry):

```bash
./tools/preset-signing-passphrase.sh
```

**Image conversion:** `make build` automatically runs `tools/convert-images.sh`
to generate WebP companions for JPEG/PNG images (requires `cwebp`). It also
generates first-page PDF thumbnails via `pdftoppm` (requires `poppler`).
Both are skipped silently when the tools are not installed.

**Python environment:** the embedding pipeline requires `uv sync` to be run
once. After that, `make build` invokes `uv run python tools/embed.py`
automatically. If `.venv` is absent, the step is skipped with a warning and
the build continues normally.
