New page
This commit is contained in:
parent
b06b1e741c
commit
aee326bfec
117
CLAUDE.md
117
CLAUDE.md
|
|
@ -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
|
|
||||||
|
|
@ -8,11 +8,13 @@ module Contexts
|
||||||
, poetryCtx
|
, poetryCtx
|
||||||
, fictionCtx
|
, fictionCtx
|
||||||
, compositionCtx
|
, compositionCtx
|
||||||
|
, contentKindField
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Data.Aeson (Value (..))
|
import Data.Aeson (Value (..))
|
||||||
import qualified Data.Aeson.KeyMap as KM
|
import qualified Data.Aeson.KeyMap as KM
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Vector as V
|
||||||
|
import Data.List (isPrefixOf)
|
||||||
import Data.Maybe (catMaybes, fromMaybe)
|
import Data.Maybe (catMaybes, fromMaybe)
|
||||||
import Data.Time.Calendar (toGregorian)
|
import Data.Time.Calendar (toGregorian)
|
||||||
import Data.Time.Clock (getCurrentTime, utctDay)
|
import Data.Time.Clock (getCurrentTime, utctDay)
|
||||||
|
|
@ -81,6 +83,25 @@ buildTimeField = field "build-time" $ \_ ->
|
||||||
| n `mod` 10 == 3 = "rd"
|
| n `mod` 10 == 3 = "rd"
|
||||||
| otherwise = "th"
|
| 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
|
-- Site-wide context
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ import Compilers (essayCompiler, postCompiler, pageCompiler, poetryCompiler, fi
|
||||||
compositionCompiler)
|
compositionCompiler)
|
||||||
import Catalog (musicCatalogCtx)
|
import Catalog (musicCatalogCtx)
|
||||||
import Commonplace (commonplaceCtx)
|
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 Tags (buildAllTags, applyTagRules)
|
||||||
import Pagination (blogPaginateRules)
|
import Pagination (blogPaginateRules)
|
||||||
import Stats (statsRules)
|
import Stats (statsRules)
|
||||||
|
|
@ -309,6 +310,31 @@ rules = do
|
||||||
>>= loadAndApplyTemplate "templates/default.html" ctx
|
>>= loadAndApplyTemplate "templates/default.html" ctx
|
||||||
>>= relativizeUrls
|
>>= 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
|
-- Library — comprehensive portal-grouped index of all content
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ constraints: any.Glob ==0.10.2,
|
||||||
any.optparse-applicative ==0.18.1.0,
|
any.optparse-applicative ==0.18.1.0,
|
||||||
any.ordered-containers ==0.2.4,
|
any.ordered-containers ==0.2.4,
|
||||||
any.os-string ==2.0.8,
|
any.os-string ==2.0.8,
|
||||||
any.pandoc ==3.5,
|
any.pandoc ==3.6,
|
||||||
any.pandoc-types ==1.23.1,
|
any.pandoc-types ==1.23.1,
|
||||||
any.parsec ==3.1.16.1,
|
any.parsec ==3.1.16.1,
|
||||||
any.pem ==0.2.4,
|
any.pem ==0.2.4,
|
||||||
|
|
@ -177,14 +177,14 @@ constraints: any.Glob ==0.10.2,
|
||||||
any.tagsoup ==0.14.8,
|
any.tagsoup ==0.14.8,
|
||||||
any.template-haskell ==2.20.0.0,
|
any.template-haskell ==2.20.0.0,
|
||||||
any.temporary ==1.3,
|
any.temporary ==1.3,
|
||||||
any.texmath ==0.12.8.11,
|
any.texmath ==0.12.8.12,
|
||||||
any.text ==2.0.2,
|
any.text ==2.0.2,
|
||||||
any.text-conversions ==0.3.1.1,
|
any.text-conversions ==0.3.1.1,
|
||||||
any.text-icu ==0.8.0.5,
|
any.text-icu ==0.8.0.5,
|
||||||
any.text-iso8601 ==0.1.1,
|
any.text-iso8601 ==0.1.1,
|
||||||
any.text-short ==0.1.6,
|
any.text-short ==0.1.6,
|
||||||
any.th-abstraction ==0.6.0.0,
|
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-expand-syns ==0.4.12.0,
|
||||||
any.th-lift ==0.8.6,
|
any.th-lift ==0.8.6,
|
||||||
any.th-lift-instances ==0.1.20,
|
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-base ==0.4.6.1,
|
||||||
any.transformers-compat ==0.7.2,
|
any.transformers-compat ==0.7.2,
|
||||||
any.typed-process ==0.2.13.0,
|
any.typed-process ==0.2.13.0,
|
||||||
any.typst ==0.6,
|
any.typst ==0.6.1,
|
||||||
any.typst-symbols ==0.1.6,
|
any.typst-symbols ==0.1.7,
|
||||||
any.unicode-collation ==0.1.3.6,
|
any.unicode-collation ==0.1.3.6,
|
||||||
any.unicode-data ==0.6.0,
|
any.unicode-data ==0.6.0,
|
||||||
any.unicode-transforms ==0.4.0.1,
|
any.unicode-transforms ==0.4.0.1,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
<main id="markdownBody">
|
||||||
|
<h1 class="page-title">New</h1>
|
||||||
|
<div class="new-controls">
|
||||||
|
<span class="new-controls-label">Show</span>
|
||||||
|
<div class="new-controls-options" role="group" aria-label="Number of entries to show">
|
||||||
|
<button class="new-count-btn" data-count="25">25</button>
|
||||||
|
<button class="new-count-btn" data-count="50">50</button>
|
||||||
|
<button class="new-count-btn" data-count="100">100</button>
|
||||||
|
<button class="new-count-btn" data-count="all">All</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="new-list">
|
||||||
|
$for(recent-items)$
|
||||||
|
<li class="new-entry">
|
||||||
|
<span class="new-entry-kind">$item-kind$</span>
|
||||||
|
<div class="new-entry-main">
|
||||||
|
<div class="new-entry-header">
|
||||||
|
<a class="new-entry-title" href="$url$">$title$</a>
|
||||||
|
<time class="new-entry-date" datetime="$date-iso$">$date-created$</time>
|
||||||
|
</div>
|
||||||
|
$if(abstract)$<p class="new-entry-abstract">$abstract$</p>$endif$
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
$endfor$
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var STORAGE_KEY = 'new-page-count';
|
||||||
|
var DEFAULT = 25;
|
||||||
|
|
||||||
|
function applyCount(n) {
|
||||||
|
var entries = document.querySelectorAll('.new-entry');
|
||||||
|
var limit = (n === 'all') ? Infinity : parseInt(n, 10);
|
||||||
|
entries.forEach(function (el, i) {
|
||||||
|
el.hidden = i >= limit;
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.new-count-btn').forEach(function (btn) {
|
||||||
|
btn.classList.toggle('is-active', btn.dataset.count === String(n));
|
||||||
|
});
|
||||||
|
try { localStorage.setItem(STORAGE_KEY, n); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
var saved;
|
||||||
|
try { saved = localStorage.getItem(STORAGE_KEY); } catch (e) {}
|
||||||
|
applyCount(saved || DEFAULT);
|
||||||
|
|
||||||
|
document.querySelectorAll('.new-count-btn').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () { applyCount(btn.dataset.count); });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}());
|
||||||
|
</script>
|
||||||
|
|
@ -14,6 +14,7 @@ $if(home)$<title>Levi Neuwirth</title>$else$<title>$title$ — Levi Neuwirth</ti
|
||||||
<link rel="stylesheet" href="/css/images.css">
|
<link rel="stylesheet" href="/css/images.css">
|
||||||
$if(home)$<link rel="stylesheet" href="/css/home.css">$endif$
|
$if(home)$<link rel="stylesheet" href="/css/home.css">$endif$
|
||||||
$if(library)$<link rel="stylesheet" href="/css/library.css">$endif$
|
$if(library)$<link rel="stylesheet" href="/css/library.css">$endif$
|
||||||
|
$if(new-page)$<link rel="stylesheet" href="/css/new.css">$endif$
|
||||||
$if(memento-mori)$<link rel="stylesheet" href="/css/memento-mori.css">$endif$
|
$if(memento-mori)$<link rel="stylesheet" href="/css/memento-mori.css">$endif$
|
||||||
$if(catalog)$<link rel="stylesheet" href="/css/catalog.css">$endif$
|
$if(catalog)$<link rel="stylesheet" href="/css/catalog.css">$endif$
|
||||||
$if(commonplace)$<link rel="stylesheet" href="/css/commonplace.css">$endif$
|
$if(commonplace)$<link rel="stylesheet" href="/css/commonplace.css">$endif$
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue