levineuwirth.org/WRITING.md

35 KiB
Raw Blame History

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:

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

---
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
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            # 0100 integer (%)
importance: 3             # 15 integer (rendered as filled/empty dots ●●●○○)
evidence: 2               # 15 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:

---
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.

---
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.

---
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.

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

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.

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.

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.


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

[[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.

{{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:

{{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:

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:

#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
The quadratic formula is $x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$.

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

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

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:

{
  "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.).

```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".

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

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

![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:

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.

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

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

Annotation callouts

Editorial sidebars — context, caveats, tangential detail.

::: {.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.

::: {.score-fragment score-name="Main Theme, mm. 18" 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.

<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:

<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.

- 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 `
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.

---
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 groupingcategory 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>.

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

or a list:

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 0100. 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:

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:

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 }.

::: 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, …):

::: {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:

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)

::: {.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:

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:

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)

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

The script outputs Vega-Lite JSON to stdout:

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:


viz: true

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


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

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):

./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.