New
+-
+ $for(recent-items)$
+
-
+ $item-kind$
+ +++ $title$ + ++ $if(abstract)$
$abstract$
$endif$ +
+ $endfor$
+
diff --git a/CLAUDE.md b/CLAUDE.md
deleted file mode 100644
index ee99a96..0000000
--- a/CLAUDE.md
+++ /dev/null
@@ -1,117 +0,0 @@
-# CLAUDE.md
-
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
-
-## Project Overview
-
-`levineuwirth.org` is a personal website built as a static site using **Hakyll** (Haskell) + **Pandoc**. The spec is in `spec.md`. The site is inspired by gwern.net in architecture: sidenotes, semantic zoom, monochrome typography, Pandoc filters, no client-side tracking.
-
-## Build Commands
-
-```bash
-make build # cabal run site -- build && pagefind --site _site
-make deploy # build + rsync -avz --delete _site/ vps:/var/www/levineuwirth.com/
-make watch # cabal run site -- watch (live-reload dev server)
-make clean # cabal run site -- clean
-```
-
-**Important:** Hakyll caches compiled items in `_cache/` keyed to source file mtimes.
-Changing a `.hs` file (filter, compiler, context) does **not** invalidate the cache for
-existing content files. Always run `make clean && make build` after any Haskell-side change,
-or content will be served from the stale cache.
-
-The Haskell build program lives in `build/`. The entry point is `build/Main.hs`.
-
-## Architecture
-
-### Build System (`build/`)
-
-The Hakyll site compiler is split into focused modules:
-
-- `Main.hs` — entry point
-- `Site.hs` — all Hakyll rules (which patterns compile to which outputs)
-- `Compilers.hs` — custom Pandoc compiler wrappers
-- `Contexts.hs` — Hakyll template contexts (includes auto-computed `word-count`, `reading-time`)
-- `Metadata.hs` — loads YAML frontmatter + merges external JSON from `data/`
-- `Tags.hs` — hierarchical tag system using Hakyll `buildTags`
-- `Pagination.hs` — 20/page for blog and tag indexes; essays all on one page
-- `Citations.hs` — citeproc + BibLaTeX + Chicago Author-Date; bib file at `data/bibliography.bib`
-- `Filters.hs` — re-exports all Pandoc AST filter modules
-- `Filters/Typography.hs` — smart quotes, dashes, etc.
-- `Filters/Sidenotes.hs` — converts footnotes to sidenotes
-- `Filters/Dropcaps.hs` — decorative drop capitals
-- `Filters/Smallcaps.hs` — smallcaps via `smcp` OT feature
-- `Filters/Wikilinks.hs` — `[[wikilink]]` syntax
-- `Filters/Links.hs` — external link classification and icon injection
-- `Filters/Math.hs` — simple LaTeX → Unicode at build time; complex math → KaTeX SSR (static HTML+MathML)
-- `Utils.hs` — shared helpers
-
-### Math Pipeline
-
-Two-tier, no client-side JS required:
-1. Simple math → Unicode/HTML via Pandoc Lua filter (inherits body font)
-2. Complex math → KaTeX server-side rendering → static HTML+MathML (KaTeX CSS loaded conditionally, only on pages that use math)
-
-### CSS (`static/css/`)
-
-| File | Purpose |
-|------|---------|
-| `base.css` | CSS variables, palette, dark mode (`[data-theme="dark"]` + `prefers-color-scheme`) |
-| `typography.css` | Spectral OT features: smallcaps (`smcp`), ligatures, figure styles, dropcaps |
-| `layout.css` | Three-column layout: sticky TOC (left) | body 650–700px (center) | sidenotes (right). Collapses on narrow screens. |
-| `sidenotes.css` | Sidenote positioning |
-| `popups.css` | Link preview popups |
-| `syntax.css` | Monochrome code highlighting (JetBrains Mono) |
-| `components.css` | Two-row nav, metadata block, collapsibles |
-
-### JavaScript (`static/js/`)
-
-| File | Source | Purpose |
-|------|--------|---------|
-| `sidenotes.js` | Adopted — Said Achmiz (MIT) | Sidenote positioning |
-| `popups.js` | Forked + simplified — Said Achmiz (MIT) | Internal previews, Wikipedia, citation previews |
-| `theme.js` | Original | Dark/light toggle with `localStorage` |
-| `toc.js` | Original | Sticky TOC + scroll tracking via `IntersectionObserver` |
-| `search.js` | Original | Pagefind integration |
-| `nav.js` | Original | Portal row expand/collapse (state in `localStorage`) |
-| `collapse.js` | Original | Section collapsing |
-
-### Typography
-
-- **Body:** Spectral (SIL OFL) — self-hosted WOFF2, subsetted with full OT features (`liga`, `smcp`, `onum`, etc.)
-- **UI/Headers:** Fira Sans (SIL OFL) — smallcaps for primary nav row
-- **Code:** JetBrains Mono (SIL OFL)
-
-All fonts self-hosted from source (not Google Fonts, which strips OT features). Subset with `pyftsubset`.
-
-### Navigation Structure
-
-```
-Home | Me | New | Index | [🔍] ← primary row (always visible), Fira Sans smallcaps
-────────────────────────────────
-▼ Fiction | Miscellany | Music | Nonfiction | Poetry | Research ← portal row
-```
-
-Portal row collapsed by default; expansion state in `localStorage`.
-
-### Content Portals
-
-Six content portals map to `content/` subdirectories: Fiction, Miscellany, Music, Nonfiction, Poetry, Research. Essays live under Nonfiction; blog posts are a separate stream.
-
-### Metadata
-
-Frontmatter keys for Phase 1: `title`, `created`, `modified`, `status`, `tags`, `abstract`.
-Auto-computed at build: `word-count`, `reading-time`.
-External data loaded from `data/annotations.yaml` and `data/bibliography.bib`.
-
-### Deployment
-
-Local build → `_site/` → `rsync` to VPS. No server-side processing; nginx serves static files. No Docker.
-
-## Key Design Constraints
-
-- **No tracking, no analytics, no fingerprinting** — enforced at the nginx CSP header level too
-- **No client-side math rendering** — KaTeX runs at build time
-- **Sidenotes right-column only** (matching gwern's `useLeftColumn: () => false`)
-- **Configuration is code** — the Makefile and Haskell build system are the deployment pipeline
-- **Content license:** CC BY-SA-NC 4.0 | **Code license:** MIT
diff --git a/build/Contexts.hs b/build/Contexts.hs
index fa0c94a..9384070 100644
--- a/build/Contexts.hs
+++ b/build/Contexts.hs
@@ -8,11 +8,13 @@ module Contexts
, poetryCtx
, fictionCtx
, compositionCtx
+ , contentKindField
) where
import Data.Aeson (Value (..))
import qualified Data.Aeson.KeyMap as KM
import qualified Data.Vector as V
+import Data.List (isPrefixOf)
import Data.Maybe (catMaybes, fromMaybe)
import Data.Time.Calendar (toGregorian)
import Data.Time.Clock (getCurrentTime, utctDay)
@@ -81,6 +83,25 @@ buildTimeField = field "build-time" $ \_ ->
| n `mod` 10 == 3 = "rd"
| otherwise = "th"
+-- ---------------------------------------------------------------------------
+-- Content kind field
+-- ---------------------------------------------------------------------------
+
+-- | @$item-kind$@: human-readable content type derived from the item's route.
+-- Used on the New page to label each entry (Essay, Post, Poem, etc.).
+contentKindField :: Context String
+contentKindField = field "item-kind" $ \item -> do
+ r <- getRoute (itemIdentifier item)
+ return $ case r of
+ Nothing -> "Page"
+ Just route
+ | "essays/" `isPrefixOf` route -> "Essay"
+ | "blog/" `isPrefixOf` route -> "Post"
+ | "poetry/" `isPrefixOf` route -> "Poem"
+ | "fiction/" `isPrefixOf` route -> "Fiction"
+ | "music/" `isPrefixOf` route -> "Composition"
+ | otherwise -> "Page"
+
-- ---------------------------------------------------------------------------
-- Site-wide context
-- ---------------------------------------------------------------------------
diff --git a/build/Site.hs b/build/Site.hs
index 3131a4f..b24d550 100644
--- a/build/Site.hs
+++ b/build/Site.hs
@@ -13,7 +13,8 @@ import Compilers (essayCompiler, postCompiler, pageCompiler, poetryCompiler, fi
compositionCompiler)
import Catalog (musicCatalogCtx)
import Commonplace (commonplaceCtx)
-import Contexts (siteCtx, essayCtx, postCtx, pageCtx, poetryCtx, fictionCtx, compositionCtx)
+import Contexts (siteCtx, essayCtx, postCtx, pageCtx, poetryCtx, fictionCtx, compositionCtx,
+ contentKindField)
import Tags (buildAllTags, applyTagRules)
import Pagination (blogPaginateRules)
import Stats (statsRules)
@@ -309,6 +310,31 @@ rules = do
>>= loadAndApplyTemplate "templates/default.html" ctx
>>= relativizeUrls
+ -- ---------------------------------------------------------------------------
+ -- New page — all content sorted by creation date, newest first
+ -- ---------------------------------------------------------------------------
+ create ["new.html"] $ do
+ route idRoute
+ compile $ do
+ let allContent = ( "content/essays/*.md"
+ .||. "content/blog/*.md"
+ .||. "content/fiction/*.md"
+ .||. allPoetry
+ .||. "content/music/*/index.md"
+ ) .&&. hasNoVersion
+ items <- recentFirst =<< loadAll allContent
+ let itemCtx = contentKindField
+ <> dateField "date-iso" "%Y-%m-%d"
+ <> essayCtx
+ ctx = listField "recent-items" itemCtx (return items)
+ <> constField "title" "New"
+ <> constField "new-page" "true"
+ <> siteCtx
+ makeItem ""
+ >>= loadAndApplyTemplate "templates/new.html" ctx
+ >>= loadAndApplyTemplate "templates/default.html" ctx
+ >>= relativizeUrls
+
-- ---------------------------------------------------------------------------
-- Library — comprehensive portal-grouped index of all content
-- ---------------------------------------------------------------------------
diff --git a/cabal.project.freeze b/cabal.project.freeze
index b0fe771..71f1281 100644
--- a/cabal.project.freeze
+++ b/cabal.project.freeze
@@ -132,7 +132,7 @@ constraints: any.Glob ==0.10.2,
any.optparse-applicative ==0.18.1.0,
any.ordered-containers ==0.2.4,
any.os-string ==2.0.8,
- any.pandoc ==3.5,
+ any.pandoc ==3.6,
any.pandoc-types ==1.23.1,
any.parsec ==3.1.16.1,
any.pem ==0.2.4,
@@ -177,14 +177,14 @@ constraints: any.Glob ==0.10.2,
any.tagsoup ==0.14.8,
any.template-haskell ==2.20.0.0,
any.temporary ==1.3,
- any.texmath ==0.12.8.11,
+ any.texmath ==0.12.8.12,
any.text ==2.0.2,
any.text-conversions ==0.3.1.1,
any.text-icu ==0.8.0.5,
any.text-iso8601 ==0.1.1,
any.text-short ==0.1.6,
any.th-abstraction ==0.6.0.0,
- any.th-compat ==0.1.6,
+ any.th-compat ==0.1.7,
any.th-expand-syns ==0.4.12.0,
any.th-lift ==0.8.6,
any.th-lift-instances ==0.1.20,
@@ -201,8 +201,8 @@ constraints: any.Glob ==0.10.2,
any.transformers-base ==0.4.6.1,
any.transformers-compat ==0.7.2,
any.typed-process ==0.2.13.0,
- any.typst ==0.6,
- any.typst-symbols ==0.1.6,
+ any.typst ==0.6.1,
+ any.typst-symbols ==0.1.7,
any.unicode-collation ==0.1.3.6,
any.unicode-data ==0.6.0,
any.unicode-transforms ==0.4.0.1,
diff --git a/static/css/new.css b/static/css/new.css
new file mode 100644
index 0000000..a75582c
--- /dev/null
+++ b/static/css/new.css
@@ -0,0 +1,149 @@
+/* new.css — Recently published content page */
+
+.new-intro {
+ font-family: var(--font-sans);
+ font-size: var(--text-size-small);
+ color: var(--text-muted);
+ margin-bottom: 2rem;
+}
+
+/* ============================================================
+ COUNT CONTROL
+ ============================================================ */
+
+.new-controls {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ margin-bottom: 1.75rem;
+}
+
+.new-controls-label {
+ font-family: var(--font-sans);
+ font-size: 0.75rem;
+ color: var(--text-faint);
+}
+
+.new-controls-options {
+ display: flex;
+ gap: 0.3rem;
+}
+
+.new-count-btn {
+ font-family: var(--font-sans);
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ background: none;
+ border: 1px solid var(--border);
+ border-radius: 2px;
+ padding: 0.15em 0.55em;
+ cursor: pointer;
+ transition: border-color 0.1s, color 0.1s;
+}
+
+.new-count-btn:hover {
+ border-color: var(--border-muted);
+ color: var(--text);
+}
+
+.new-count-btn.is-active {
+ border-color: var(--text-muted);
+ color: var(--text);
+ font-weight: 600;
+}
+
+/* ============================================================
+ ENTRY LIST
+ ============================================================ */
+
+.new-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.new-entry {
+ display: flex;
+ gap: 0.9rem;
+ align-items: flex-start;
+ padding: 0.85rem 0;
+ border-bottom: 1px solid var(--border);
+}
+
+.new-entry:first-child {
+ border-top: 1px solid var(--border);
+}
+
+/* ============================================================
+ KIND BADGE
+ ============================================================ */
+
+.new-entry-kind {
+ font-family: var(--font-sans);
+ font-size: 0.63rem;
+ font-variant: all-small-caps;
+ letter-spacing: 0.07em;
+ color: var(--text-faint);
+ background: var(--bg-offset);
+ border: 1px solid var(--border);
+ border-radius: 2px;
+ padding: 0.15em 0.5em;
+ flex-shrink: 0;
+ margin-top: 0.25em;
+ min-width: 5.5rem;
+ text-align: center;
+ line-height: 1.6;
+}
+
+/* ============================================================
+ ENTRY CONTENT
+ ============================================================ */
+
+.new-entry-main {
+ flex: 1;
+ min-width: 0;
+}
+
+.new-entry-header {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.new-entry-title {
+ font-family: var(--font-serif);
+ font-size: 1rem;
+ color: var(--text);
+ text-decoration: none;
+ line-height: 1.35;
+}
+
+.new-entry-title:hover {
+ text-decoration: underline;
+ text-underline-offset: 0.15em;
+}
+
+.new-entry-date {
+ font-family: var(--font-sans);
+ font-size: 0.72rem;
+ color: var(--text-faint);
+ white-space: nowrap;
+ flex-shrink: 0;
+ font-variant-numeric: tabular-nums;
+}
+
+.new-entry-abstract {
+ font-family: var(--font-sans);
+ font-size: var(--text-size-small);
+ color: var(--text-muted);
+ margin: 0.3rem 0 0;
+ line-height: 1.55;
+ /* Clamp to two lines */
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
diff --git a/templates/new.html b/templates/new.html
new file mode 100644
index 0000000..5a59016
--- /dev/null
+++ b/templates/new.html
@@ -0,0 +1,54 @@
+ $abstract$New
+
+ $for(recent-items)$
+
+