889 lines
27 KiB
Markdown
889 lines
27 KiB
Markdown
# 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
|
||
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
|
||
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 `<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.
|
||
|
||
**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.
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
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`
|
||
|
||
| Domain | Icon |
|
||
|--------|------|
|
||
| `wikipedia.org` | Wikipedia |
|
||
| `arxiv.org` | arXiv |
|
||
| `doi.org` | DOI |
|
||
| `github.com` | GitHub |
|
||
| Everything else | Generic external arrow |
|
||
|
||
### Link preview popups
|
||
|
||
Hovering over an internal link shows a popup: title, abstract, authors, tags,
|
||
reading time. Hovering over a Wikipedia link shows an article excerpt.
|
||
Citation markers (`[1]`) show the full reference. All automatic.
|
||
|
||
---
|
||
|
||
## 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
|
||

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

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

|
||
:::
|
||
```
|
||
|
||
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 display | Expanded (`<details>`) |
|
||
|-------|----------------|------------------------|
|
||
| `status` | chip (always shown if present) | — |
|
||
| `confidence` | `72%` | — |
|
||
| `importance` | `●●●○○` | — |
|
||
| `evidence` | `●●○○○` | — |
|
||
| `stability` | — | auto-computed from git history |
|
||
| `scope` | — | if set |
|
||
| `novelty` | — | if set |
|
||
| `practicality` | — | if set |
|
||
| `last-reviewed` | — | most recent commit date |
|
||
| `confidence-trend` | — | ↑/↓/→ from last two `confidence-history` entries |
|
||
|
||
**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) | BibTeX · Copy · DuckDuckGo · Here · Wikipedia |
|
||
| Prose (single word) | BibTeX · Copy · Define · DuckDuckGo · Here · 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.
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
---
|
||
|
||
## Build
|
||
|
||
```bash
|
||
make build # auto-commit content/, compile, run pagefind + embeddings, clear IGNORE.txt
|
||
make sign # GPG detach-sign every _site/**/*.html → .html.sig (requires passphrase cached)
|
||
make deploy # build + sign + optional GitHub push + rsync to VPS
|
||
make watch # Hakyll live-reload dev server at http://localhost:8000
|
||
make dev # clean build + python HTTP server at http://localhost:8000
|
||
make clean # wipe _site/ and _cache/
|
||
```
|
||
|
||
`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` pushes to GitHub if `GITHUB_TOKEN` and `GITHUB_REPO` are set in
|
||
`.env` (see `.env.example`), then rsyncs `_site/` to the VPS.
|
||
|
||
**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
|
||
```
|
||
|
||
**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.
|