favicon, logo, internal popups

This commit is contained in:
Levi Neuwirth 2026-04-12 14:57:01 -04:00
parent e25a311dd9
commit 41bbbd799b
18 changed files with 436 additions and 3 deletions

View File

@ -53,12 +53,29 @@ classifyLink (Link (ident, classes, kvs) ils (url, title))
++ [("data-link-icon", icon)] ++ [("data-link-icon", icon)]
++ [("data-link-icon-type", "svg")] ++ [("data-link-icon-type", "svg")]
in Link (ident, classes', kvs') ils (url, title) in Link (ident, classes', kvs') ils (url, title)
| isInternalPage url =
let classes' = classes ++ ["link-internal"]
kvs' = kvs
++ [("data-link-icon", "internal")]
++ [("data-link-icon-type", "svg")]
in Link (ident, classes', kvs') ils (url, title)
classifyLink x = x classifyLink x = x
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
-- Helpers -- Helpers
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
-- | True if the URL is a root-relative or relative path to another page
-- (not an anchor-only link like @#section@ or @#ref-foo@).
isInternalPage :: Text -> Bool
isInternalPage url
| T.null url = False
| "#" `T.isPrefixOf` url = False -- anchor-only
| "mailto:" `T.isPrefixOf` url = False
| "http://" `T.isPrefixOf` url = False -- handled by isExternal
| "https://" `T.isPrefixOf` url = False
| otherwise = True
-- | True if the URL points outside the site's domain. -- | True if the URL points outside the site's domain.
-- --
-- Uses a strict hostname comparison rather than substring matching, so -- Uses a strict hostname comparison rather than substring matching, so

View File

@ -0,0 +1,279 @@
---
title: "Ozymandias: A Static Site Framework"
date: 2026-04-12
abstract: >
Ozymandias is the static site framework underlying this website, now extracted and released under the MIT license. It
is a full-featured Hakyll and Pandoc setup for long-form writing: sidenotes, epistemic profiles, backlinks,
wikilinks, a swipeable score reader, a semantic search pipeline, and more — configurable from a single YAML file
and deployable with a single make command.
tags: [meta, code]
status: "Working model"
confidence: 80
importance: 3
evidence: 3
scope: average
novelty: moderate
practicality: high
---
::: dropcap
*"My name is Ozymandias, King of Kings; / Look on my Works, ye Mighty, and despair!"* The name is a joke. Every
framework is a monument that its author believes will outlast the work produced in it. The name is also a warning:
the writing you put in a framework might actually outlast the framework itself, which is why the framework should be
small, coherent, and legible — not a cathedral built to impress.
:::
The core of this website has been extracted and released as [Ozymandias](https://git.levineuwirth.org/neuwirth/ozymandias), a static site framework under the MIT license. It is the full pipeline: the Haskell build system, the Pandoc filter stack, all templates, all stylesheets, all client-side JavaScript — minus my personal content. If you want a website that works like this one and want to understand exactly how it works, Ozymandias is where to start.
This page describes what Ozymandias is, how it diverged from this site during extraction, and how to use it.
---
## What It Is
Ozymandias is a static site generator for long-form writing. It is built on two mature Haskell tools: [Hakyll](https://jaspervdj.be/hakyll/) for build orchestration and [Pandoc](https://pandoc.org/) for document processing. The framework handles routing, templating, and pagination through Hakyll, and applies a custom sequence of Pandoc [AST]{.smallcaps} transforms during compilation. The output is a directory of plain [HTML]{.smallcaps} files that can be served by any web server.
The short version of what comes with it:
- **Sidenotes** — Pandoc footnotes render as margin notes on wide screens; on narrow screens they collapse to a numbered footnotes section at the bottom.
- **Epistemic profiles** — essays can declare confidence, evidence quality, importance, scope, novelty, and practicality; these appear as a structured footer before the reader commits to the full text.
- **Backlinks** — every page accumulates a list of other pages that link to it, with surrounding paragraph context, via a two-pass compilation strategy.
- **Wikilinks**`[[Page Title]]` and `[[Page Title|display text]]` syntax resolved at build time.
- **Citations** — Pandoc citeproc with Chicago Notes; footnote-style in-text markers with a bibliography section and a separate "further reading" block.
- **Score reader** — a swipeable [SVG]{.smallcaps} viewer for music compositions with dark-mode-compatible notation.
- **Typography** — dropcaps, automatic smallcaps detection for abbreviations, Latin abbreviation tooltips, old-style figures via the `onum` OpenType feature.
- **Mathematics** — [KaTeX]{.smallcaps} rendering at build time; no math rendering in the browser.
- **Full-text search** — Pagefind, client-side, no external service.
- **Semantic search** — an optional embedding pipeline using `sentence-transformers` and [FAISS]{.smallcaps} for a "similar pages" section and semantic query matching.
- **Hierarchical tags**`research/mathematics` expands to both `research` and `research/mathematics`; tag pages are generated and paginated automatically.
- **Library portal** — a configurable taxonomy page that groups all content by tag hierarchy.
- **Dark mode, reading mode, settings** — client-side, persisted in `localStorage`.
- **GPG signing** — optional per-page detached signatures, with pubkey linked from the footer.
- **Atom feeds** — site-wide and per-section (music gets its own feed by default).
The prerequisite list is short: [GHC]{.smallcaps} 9.6+, cabal-install, and Pagefind. Image conversion and the embedding pipeline are both optional and add their own dependencies (`cwebp` and Python with `uv`, respectively).
---
## How It Diverged From This Site
When I extracted Ozymandias, the primary engineering work was disentangling site-specific configuration from the framework machinery. In levineuwirth.org, several values — the site [URL]{.smallcaps}, the author name, the navigation structure, the feed title — were compiled directly into the Haskell source. That is fine for a personal site and irritating for a reusable framework. The extraction introduced a `Config.hs` module and a `site.yaml` file that together hold all identity and navigation configuration. The rest of the build system reads from these at startup and never hardcodes a domain or author name.
The result is that you can fork the repository, edit one file, and have a working site with a completely different identity. The Haskell source does not need to be touched unless you want to extend or modify the framework itself.
Beyond configuration, the content in `content/` was replaced with a small set of demo pages that exercise the filter pipeline without constituting a personal corpus. The `data/bibliography.bib` file was emptied and replaced with a placeholder. Everything in `static/` — the fonts, stylesheets, scripts, and link icons — shipped intact. No features were removed during the extraction. Ozymandias has the full pipeline.
### What Remains Shared
The two repositories share the same filter modules, the same templates (minus identity strings), and the same static assets. Changes to the filter pipeline in one are intended to be ported to the other. The practical result is that this site is an Ozymandias instance — it runs on the same engine, only with the configuration file pointing at `levineuwirth.org` rather than `example.com`. This page is compiled by the same code that compiles an Ozymandias site built from the framework.
### What Diverges Intentionally
Several features of this site are too specific to my personal corpus to include in the framework defaults. The similarity embedding index — which requires running a neural model over all page content — is present in Ozymandias as an optional pipeline but ships with an empty index. The music catalog, the commonplace book, and the statistics page are included in the framework because they are useful to authors in general, but they contain no data by default. The semantic search [ONNX]{.smallcaps} model weights are downloaded by a separate `make download-model` target rather than committed to the repository.
---
## The Filter Pipeline
The filters are the heart of the framework. Pandoc compiles Markdown to an abstract syntax tree, and the filters walk and transform that tree before Pandoc serializes it to [HTML]{.smallcaps}. They are applied in a fixed sequence; the order matters.
**Source-level preprocessors** run before Pandoc sees the file. They transform raw Markdown strings:
- **Wikilinks** — converts `[[Page Name]]` and `[[Page Name|display text]]` to standard Markdown links using slugification: lowercase, spaces to hyphens, punctuation stripped. The destination path follows the same routing rules as the content item it targets.
- **EmbedPdf** — converts `{{pdf:/path/to/file.pdf}}` syntax (optionally with a page anchor) to an iframe pointed at the vendored PDF.js viewer, preserving the original path in a `data-pdf-src` attribute for the popup thumbnail system.
- **Transclusion** — converts `{{essay-slug}}` or `{{essay-slug#section}}` to placeholder divs that the client-side `transclude.js` script resolves at page load. This allows shared content to be authored once and embedded anywhere without duplicating the source.
**[AST]{.smallcaps}-level filters** run after parsing. They are pure functions over the Pandoc [AST]{.smallcaps}:
- **Images** — wraps each image in a `<picture>` element with a [WebP]{.smallcaps} source if a `.webp` companion file exists alongside the original. Adds `loading="lazy"` to images below the fold and marks them for the lightbox system.
- **Sidenotes** — transforms Pandoc's footnote syntax (`[^1]: note text`) into inline `<span class="sidenote">` elements with alphabetic labels (a, b, c, … z, aa, ab, …). A `<section class="footnotes">` fallback is preserved at document end for narrow screens where margin placement is impractical.
- **Typography** — matches exact Pandoc `Str` tokens against a table of Latin abbreviations and wraps them in `<abbr title="…">` elements. The table covers *e.g.*, *i.e.*, *cf.*, *viz.*, *NB*, *et al.*, and the rest of the common scholarly shorthand.
- **Links** — classifies external links (any `http`/`https` [URL]{.smallcaps} not on the site's own domain) and adds `class="link-external"`, `target="_blank"`, `rel="noopener noreferrer"`, and a `data-link-icon` attribute that the [CSS]{.smallcaps} uses to render a per-domain icon. A separate pass rewrites root-relative [PDF]{.smallcaps} links to the viewer [URL]{.smallcaps}. Domain classification is by exact hostname match, not substring, so lookalike domains are correctly identified as external.
- **Smallcaps** — detects runs of three or more uppercase letters and wraps them in `<abbr class="smallcaps">`. Trailing punctuation is stripped before matching so `HTML,` and `API.` are caught correctly. Short all-caps tokens (`OK`, `I`) and mixed-case tokens (`JavaScript`) are not converted.
- **Dropcaps** — the filter itself is an identity transform; the real work is done by the [CSS]{.smallcaps} `.dropcap` class applied via fenced div syntax (`::: dropcap`). The filter's presence in the pipeline documents the intent.
- **Math** — another near-identity transform; inline and display math is passed through as-is for [KaTeX]{.smallcaps} to process at render time.
- **Code** — prepends `language-` to code block class names so Prism.js can pick up the language for syntax highlighting without each author needing to write `language-haskell` instead of just `haskell`.
- **Score** (music-specific) — reads [SVG]{.smallcaps} score fragment files from disk and inlines them into the document, replacing `#000000` and `black` fills and strokes with `currentColor` so notation renders correctly in both light and dark mode.
- **Viz** (visualization-specific) — executes Python scripts referenced in fenced code blocks and captures stdout. A Matplotlib script produces an [SVG]{.smallcaps} that is inlined directly; a Vega-Lite script produces a [JSON]{.smallcaps} spec that is embedded for Vega-Embed to render client-side.
The IO-performing filters (Score, Viz, Images) run before the pure ones. This ordering ensures that downstream filters see a stable [AST]{.smallcaps} without pending file reads.
---
## Epistemic Profiles
The epistemic profile is a structured block that appears in the footer of any essay or post whose frontmatter includes a `status` field. It is the most distinctive feature of the framework philosophically, and the one most worth understanding before deploying it.
The fields:
- **Status** — a controlled vocabulary: *Draft*, *Working model*, *Durable*, *Refined*, *Superseded*, *Deprecated*. The distinction between *Working model* and *Durable* matters: the former is a position I currently hold but would not stake much on; the latter is something I expect to hold up under scrutiny.
- **Confidence** — an integer from 0 to 100 representing credence in the central thesis. When a `confidence-history` list is present in the frontmatter, the framework derives a trend arrow (↑ ↓ →) from the last two entries automatically.
- **Importance** — a 15 dot scale for how much the work matters.
- **Evidence** — a 15 dot scale for how well-evidenced the claims are. An essay with high importance and low evidence is a speculative position and should be read accordingly.
- **Trust score** — derived automatically as (confidence × 0.6) + (rescaled evidence × 0.4). It is a narrow answer to "how much should you trust the central claim?" and deliberately does not incorporate importance, scope, novelty, or practicality, which are separate axes intentionally not blended into a composite.
- **Scope, Novelty, Practicality** — orientation fields, not ratings. *Scope* ranges from *personal* to *civilizational*; *novelty* from *conventional* to *innovative*; *practicality* from *abstract* to *exceptional*. They appear in the footer alongside the numeric fields.
- **Stability** — auto-computed from `git log --follow` at every build. The heuristic: a very new or barely-touched document is *volatile*; an actively-revised document is *revising*; older documents with more commits settle into *fairly stable*, *stable*, or *established*. This requires no manual maintenance.
The version history block, just above the epistemic footer, uses a three-tier fallback: authored `history:` notes in the frontmatter, then the raw git log, then the `date:` field as a creation record.
The point is not precision — a 72% confidence rating is not false exactness. It is an attempt to make explicit what most writing leaves implicit: where the author actually stands, and whether that position is stable or still shifting.
---
## Backlinks
Backlinks require a two-pass architecture, because a page cannot know which pages will link to it until all pages have been compiled.
Pass one compiles every content item in a special "links" version that extracts all internal links together with the surrounding paragraph [HTML]{.smallcaps}. Pass two inverts this map — grouping sources by their targets — and produces `data/backlinks.json`. The final compilation pass loads this file as a dependency and injects the backlinks section into each page's template context.
The practical consequence for authors is that internal links automatically generate backlink sections with source titles and context snippets, without any manual cross-referencing. The `[[Wikilinks]]` syntax makes it natural to link between pages; the backlinks system makes those connections visible to readers moving in either direction.
---
## Semantic Search and Similar Links
Both features are optional and require Python with `uv`:
```sh
uv sync # install dependencies from pyproject.toml
make download-model # fetch ONNX weights for client-side search
```
**Full-text search** uses Pagefind, which indexes the compiled [HTML]{.smallcaps} and produces a static search index that runs entirely in the browser. No external service is involved.
**Semantic search** runs a `sentence-transformers` model (`all-MiniLM-L6-v2`, 384 dimensions) over extracted page text, builds a [FAISS]{.smallcaps} similarity index, and stores page-level neighbors in `data/similar-links.json`. At render time, this file is loaded as a Hakyll dependency and the top similar pages are injected into each essay's template context as a "Related" section. The same model can be run client-side in the browser via [ONNX]{.smallcaps} Runtime Web for semantic query matching — the weights are served from the same origin, which means no external [API]{.smallcaps} calls.^[This is the design decision I care most about. Bolting semantic search onto a static site usually means sending queries to a third-party service. Serving the model weights from the same origin means the feature works without any network request beyond what is needed to load the page.]
---
## Content Types
Ozymandias supports six content types, each with its own template and routing convention:
| Type | Path | Route | Template |
|:-----|:-----|:------|:---------|
| Essay | `content/essays/*.md` | `/essays/{slug}.html` | `essay.html` |
| Blog post | `content/blog/*.md` | `/blog/YYYY-MM-DD-{slug}.html` | `blog-post.html` |
| Poetry | `content/poetry/*.md` | `/poetry/{slug}.html` | `reading.html` |
| Fiction | `content/fiction/*.md` | `/fiction/{slug}.html` | `reading.html` |
| Composition | `content/music/{slug}/index.md` | `/music/{slug}/index.html` | `composition.html` |
| Page | `content/*.md` | `/{slug}.html` | `page.html` |
Essays and blog posts support the full feature set: [TOC]{.smallcaps}, epistemic profiles, backlinks, similar links, citations, version history. Poetry and fiction use a `reading` [CSS]{.smallcaps} class that adjusts line spacing and disables indentation, making stanza structure visible. Music compositions get a separate score-reader view at `/music/{slug}/score/` — a minimal interface with swipe navigation through [SVG]{.smallcaps} score pages.
Several pages are generated automatically without source files: `/essays/index.html`, `/blog/index.html` (paginated, 20 per page), `/new.html` (all content sorted by creation date), `/library.html` (portal taxonomy), tag index pages at `/{tag}/index.html`, author pages at `/authors/{slug}/index.html`, and `/feed.xml`.
Drafts live in `content/drafts/essays/` and are only visible when the `SITE_ENV=dev` environment variable is set. Production builds exclude them entirely — they do not appear in feeds, tag pages, backlinks, or the library.
---
## Configuration
All site identity and navigation lives in `site.yaml`. The full schema:
```yaml
site-name: "My Site"
site-url: "https://example.com"
site-description: "A personal site built with Ozymandias"
site-language: "en"
author-name: "Your Name"
author-email: "you@example.com"
feed-title: "My Site"
feed-description: "Essays, notes, and creative work"
license: "CC BY-SA 4.0"
source-url: "" # optional link to git repository
gpg-fingerprint: "" # leave empty to omit sig links
gpg-pubkey-url: "/gpg/pubkey.asc"
nav:
- { href: "/", label: "Home" }
- { href: "/library.html", label: "Library" }
- { href: "/new.html", label: "New" }
- { href: "/search.html", label: "Search" }
portals:
- { slug: "writing", name: "Writing" }
- { slug: "code", name: "Code" }
- { slug: "notes", name: "Notes" }
```
Portals are the library taxonomy. Each portal collects all content whose tags include the portal's slug or any tag with that slug as a prefix. Content tagged `writing/essays` and `writing/fiction` both appear under the `writing` portal.
---
## Getting Started
```sh
git clone https://git.levineuwirth.org/neuwirth/ozymandias my-site
cd my-site
$EDITOR site.yaml # set site-name, site-url, author-name, author-email
make dev # build with drafts visible; serve on :8000
```
`make dev` builds with `SITE_ENV=dev` (so drafts are included) and starts a local server. `make build` produces the production output in `_site/`. `make watch` adds incremental rebuilds on file changes.
For deployment, the included `make deploy` target runs `make clean && make build`, optionally signs each page with [GPG]{.smallcaps}, rsyncs `_site/` to a [VPS]{.smallcaps} configured via `.env`, and pushes to the git remote. Set `VPS_USER`, `VPS_HOST`, and `VPS_PATH` in `.env` to configure the destination.^[The `make deploy` target always begins with `make clean` to avoid stale build artifacts. Incremental Hakyll rebuilds are safe for development but can produce subtly incorrect output — particularly for pages whose template context depends on the full backlink graph — if the dependency graph is not fully consistent. The clean ensures the graph is always recomputed from scratch for production.]
---
## Writing Content
An essay with the full feature set looks like this:
```yaml
---
title: "On the Virtues of Careful Writing"
date: 2026-04-12
abstract: >
A brief description that appears on index pages and in the epistemic header.
tags: [writing, research/rhetoric]
authors: ["Your Name", "Collaborator | https://example.com"]
affiliation: "Institution | https://institution.edu"
status: "Working model"
confidence: 65
importance: 4
evidence: 3
scope: average
novelty: moderate
practicality: high
confidence-history: [50, 65]
history:
- date: "2026-04-12"
note: Initial draft
bibliography: data/bibliography.bib
further-reading: [key1, key2]
---
::: dropcap
Opening paragraph here. Sidenotes use the standard Pandoc footnote syntax.^[Like this.]
:::
## First Section
Wikilinks to other pages: [[About This Site]]. External links work normally.
Citations use Pandoc's citeproc syntax: [@author2024].
```
The `authors` field defaults to the `author-name` in `site.yaml` when absent. The `affiliation` field takes a `Name | URL` format. The `history:` block overrides git-derived version history when the git log alone would not convey what changed.
---
## License
The framework code — everything in `build/`, `templates/`, `static/`, `tools/`, and the configuration files — is [MIT]{.smallcaps} licensed. The demo content under `content/` is public domain. Your content is yours; add whatever license you choose.
The [MIT]{.smallcaps} license was chosen deliberately: it imposes no obligations, carries no viral clauses, and makes no claims on the writing produced with it. Frameworks should not take a stake in the work they compile.
---
## The Relationship Between Ozymandias and This Site
This site is Ozymandias with my configuration and my content. Changes flow in both directions, with the understanding that the framework is the more conservative of the two repositories: features that turn out to be site-specific stay in levineuwirth.org; features that generalize get ported to Ozymandias. The filter pipeline and the template system are intended to stay in sync.
The divergence is, in a sense, the point. A personal website is a *position*, as I write in the [[Colophon]]. Ozymandias is the mechanism; the position is what you put in it.

BIN
favicon.zip Normal file

Binary file not shown.

BIN
static/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -86,6 +86,8 @@
/* Color palette */ /* Color palette */
--bg: #faf8f4; --bg: #faf8f4;
--bg-nav: #faf8f4; --bg-nav: #faf8f4;
/* Nav logo silhouette color — override per theme to change logo appearance */
--nav-logo-fg: #1a1a1a;
--bg-offset: #f2f0eb; --bg-offset: #f2f0eb;
--text: #1a1a1a; --text: #1a1a1a;
--text-muted: #555555; --text-muted: #555555;
@ -163,6 +165,7 @@
[data-theme="dark"] { [data-theme="dark"] {
--bg: #121212; --bg: #121212;
--bg-nav: #181818; --bg-nav: #181818;
--nav-logo-fg: #d4d0c8;
--bg-offset: #1a1a1a; --bg-offset: #1a1a1a;
/* --text-faint was previously #6a6660, which yields ~2.8:1 contrast /* --text-faint was previously #6a6660, which yields ~2.8:1 contrast
on the #121212 background and fails WCAG AA. Bumped to #8b8680 for on the #121212 background and fails WCAG AA. Bumped to #8b8680 for
@ -198,6 +201,7 @@
:root:not([data-theme="light"]) { :root:not([data-theme="light"]) {
--bg: #121212; --bg: #121212;
--bg-nav: #181818; --bg-nav: #181818;
--nav-logo-fg: #d4d0c8;
--bg-offset: #1a1a1a; --bg-offset: #1a1a1a;
--text: #d4d0c8; --text: #d4d0c8;
--text-muted: #8c8881; --text-muted: #8c8881;

View File

@ -69,6 +69,37 @@ nav.site-nav {
border-left: 1px solid var(--border); border-left: 1px solid var(--border);
} }
/* Home logo square button flush into the top-left corner of the nav bar.
The L silhouette is rendered via ::before mask-image so the background
matches --bg-nav exactly and the foreground follows --nav-logo-fg (set
per theme in base.css override there to restyle for light mode). */
.nav-logo {
position: absolute;
left: 0;
top: 0;
bottom: 0;
aspect-ratio: 1 / 1;
display: block;
overflow: hidden;
flex-shrink: 0;
text-decoration: none;
background-color: var(--bg-nav);
}
.nav-logo::before {
content: '';
position: absolute;
inset: 12%;
background-color: var(--nav-logo-fg);
mask-image: url('/images/link-icons/internal.svg');
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
-webkit-mask-image: url('/images/link-icons/internal.svg');
-webkit-mask-size: contain;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
}
/* Controls cluster: portals toggle + theme toggle, pinned right */ /* Controls cluster: portals toggle + theme toggle, pinned right */
.nav-controls { .nav-controls {
position: absolute; position: absolute;
@ -259,6 +290,13 @@ nav.site-nav {
*/ */
@media (max-width: 540px) { @media (max-width: 540px) {
.nav-logo {
position: static;
height: 2rem;
width: 2rem;
flex-shrink: 0;
}
.nav-row-primary { .nav-row-primary {
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;

View File

@ -51,8 +51,9 @@
line-height: 1.55; line-height: 1.55;
} }
/* Source label ("Wikipedia", "arXiv") */ /* Source label ("Wikipedia", "arXiv") and tag line in internal popups */
.popup-source { .popup-source,
.popup-tags {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.3em; gap: 0.3em;
@ -122,6 +123,10 @@
mask-image: url('/images/link-icons/orcid.svg'); mask-image: url('/images/link-icons/orcid.svg');
-webkit-mask-image: url('/images/link-icons/orcid.svg'); -webkit-mask-image: url('/images/link-icons/orcid.svg');
} }
.popup-source[data-popup-source="internal"]::before {
mask-image: url('/images/link-icons/internal.svg');
-webkit-mask-image: url('/images/link-icons/internal.svg');
}
/* Author list (arXiv, DOI, GitHub, etc.) */ /* Author list (arXiv, DOI, GitHub, etc.) */
.popup-authors { .popup-authors {

View File

@ -726,3 +726,8 @@ a[data-link-icon="apple"]::after {
mask-image: url('/images/link-icons/apple.svg'); mask-image: url('/images/link-icons/apple.svg');
-webkit-mask-image: url('/images/link-icons/apple.svg'); -webkit-mask-image: url('/images/link-icons/apple.svg');
} }
a[data-link-icon="internal"]::after {
mask-image: url('/images/link-icons/internal.svg');
-webkit-mask-image: url('/images/link-icons/internal.svg');
}

BIN
static/favicon-96x96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 15 KiB

1
static/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 114 KiB

View File

@ -0,0 +1,56 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<g transform="translate(0,512) scale(0.1,-0.1)" fill="black" stroke="none">
<path d="M1980 4795 c-36 -7 -105 -29 -153 -48 -388 -157 -699 -499 -824 -905
-24 -79 -27 -103 -27 -252 -1 -185 8 -231 75 -377 81 -177 207 -329 389 -467
286 -216 583 -314 870 -287 58 5 106 8 108 7 4 -5 -103 -353 -190 -616 -65
-195 -151 -417 -199 -511 l-18 -37 -142 49 c-162 56 -432 130 -564 155 -176
32 -428 17 -566 -35 -185 -70 -334 -241 -385 -441 -24 -93 -15 -219 19 -294
58 -125 145 -220 254 -275 61 -31 66 -26 8 8 -48 28 -86 61 -70 61 4 0 45 -18
90 -39 102 -49 180 -70 303 -80 101 -9 160 -4 112 10 -17 5 -6 7 36 8 149 2
334 52 486 130 53 27 100 46 104 43 4 -4 4 -2 1 4 -3 6 8 19 26 29 31 18 31
18 17 0 -9 -11 22 1 77 30 51 27 96 46 99 42 19 -19 -316 -205 -482 -267 -43
-16 -74 -29 -67 -30 26 -1 165 56 288 118 147 73 284 161 399 257 43 36 82 65
86 65 4 0 91 -40 192 -89 203 -98 409 -191 507 -228 117 -44 72 -18 -82 47
-84 35 -217 95 -297 133 -80 38 -179 85 -220 104 -89 42 -87 40 -72 55 9 9 16
8 29 -4 59 -52 637 -303 838 -363 50 -15 117 -36 150 -46 70 -21 190 -43 328
-60 87 -10 208 -8 292 5 17 3 49 7 73 11 23 3 40 10 37 15 -4 6 6 7 22 4 18
-4 53 4 97 20 38 14 79 26 92 26 12 0 26 4 29 10 3 5 28 21 56 35 28 14 69 40
92 58 56 45 77 56 52 28 -32 -37 -104 -86 -200 -136 -49 -26 -79 -44 -65 -40
109 32 306 172 388 276 187 238 233 564 118 845 -45 110 -90 179 -175 267
-110 115 -268 206 -456 263 -78 24 -108 28 -225 28 -158 1 -222 -13 -350 -76
-65 -32 -101 -58 -160 -117 -129 -131 -190 -258 -198 -411 -6 -119 8 -189 58
-288 76 -151 245 -264 431 -286 73 -9 182 14 259 55 75 40 163 131 198 206 23
50 27 71 27 153 0 113 -17 153 -94 222 -50 45 -132 78 -192 78 -50 0 -128 -31
-187 -74 -78 -58 -122 -140 -122 -229 0 -83 37 -150 93 -170 7 -2 9 -8 5 -12
-14 -14 -69 18 -102 59 -66 82 -77 121 -67 246 9 108 46 191 122 274 94 103
219 136 383 102 98 -20 147 -42 241 -107 168 -116 249 -265 248 -459 0 -149
-49 -266 -152 -367 -150 -147 -348 -207 -600 -183 -224 22 -397 80 -751 250
-135 64 -272 128 -305 140 -80 31 -84 35 -53 67 112 119 293 487 418 848 78
227 174 570 165 586 -3 4 32 20 77 35 170 56 517 228 738 366 356 221 629 492
759 752 70 140 92 224 98 377 7 151 -10 242 -63 349 -85 169 -335 310 -552
310 -184 0 -422 -92 -625 -243 -209 -155 -454 -466 -612 -778 -88 -173 -133
-285 -203 -504 -57 -181 -162 -542 -162 -560 0 -25 -153 -33 -251 -15 -123 23
-226 106 -254 204 -22 75 -26 180 -8 205 20 30 30 27 79 -24 41 -41 87 -65
100 -52 11 10 -45 114 -75 140 -107 89 -269 36 -315 -105 -9 -28 -16 -63 -16
-79 0 -16 -4 -29 -8 -29 -13 0 -106 100 -150 162 -63 86 -108 173 -143 278
-28 84 -33 111 -37 233 -5 158 6 250 49 379 92 283 268 482 501 566 65 23 88
27 203 27 149 0 195 -12 282 -76 74 -55 110 -96 143 -164 86 -175 76 -368 -28
-570 -48 -92 -166 -212 -244 -248 -141 -66 -262 -57 -357 29 -71 64 -92 97
-107 168 -17 78 -17 230 0 278 16 45 66 131 73 125 2 -3 -5 -23 -16 -46 -31
-60 -27 -107 17 -192 80 -158 231 -208 363 -120 82 55 128 165 116 277 -7 65
-34 118 -89 171 -68 66 -126 88 -233 88 -82 0 -97 -3 -157 -33 -123 -60 -211
-179 -260 -347 -18 -66 -22 -103 -22 -210 1 -117 3 -136 27 -194 93 -223 314
-330 554 -268 59 16 163 62 163 72 0 3 -18 -4 -40 -15 -77 -40 -63 -19 23 33
48 28 118 79 154 112 37 33 70 59 74 58 4 -1 6 3 6 10 -2 20 126 277 135 271
4 -3 5 2 2 10 -3 9 -2 16 3 16 5 0 14 15 20 33 43 123 50 353 14 486 -32 120
-62 173 -149 260 -93 93 -186 145 -298 166 -92 18 -285 18 -374 0z m2265 -264
c50 -23 130 -108 155 -164 86 -194 25 -476 -157 -735 -66 -94 -289 -314 -410
-405 -204 -152 -533 -330 -698 -378 -30 -8 -30 -8 -9 9 15 11 32 53 53 128 44
156 159 496 226 663 196 496 421 821 610 881 78 25 178 25 230 1z m-2560
-1908 c140 -71 275 -114 430 -138 49 -8 66 -13 44 -14 -106 -3 -354 68 -484
139 -79 43 -191 115 -200 129 -3 7 24 -8 62 -32 37 -24 104 -62 148 -84z
m-394 -1469 c197 -37 519 -135 519 -158 0 -19 -172 -175 -263 -237 -167 -115
-285 -154 -442 -146 -249 11 -395 138 -356 311 22 99 131 193 268 231 66 18
175 18 274 -1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -341,7 +341,8 @@
return store(href, return store(href,
'<div class="popup-internal">' '<div class="popup-internal">'
+ (tags ? '<div class="popup-source">' + esc(tags) + '</div>' : '') + srcHtml('internal', 'levineuwirth.org')
+ (tags ? '<div class="popup-tags">' + esc(tags) + '</div>' : '')
+ '<div class="popup-title">' + esc(titleEl.textContent.trim()) + '</div>' + '<div class="popup-title">' + esc(titleEl.textContent.trim()) + '</div>'
+ (authors ? '<div class="popup-authors">' + esc(authors) + '</div>' : '') + (authors ? '<div class="popup-authors">' + esc(authors) + '</div>' : '')
+ (abstract ? '<div class="popup-abstract">' + esc(abstract) + '</div>' : '') + (abstract ? '<div class="popup-abstract">' + esc(abstract) + '</div>' : '')

21
static/site.webmanifest Normal file
View File

@ -0,0 +1,21 @@
{
"name": "levineuwirth.org",
"short_name": "ln",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -1,6 +1,11 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
$if(home)$<title>Levi Neuwirth</title>$else$<title>$title$ — Levi Neuwirth</title>$endif$ $if(home)$<title>Levi Neuwirth</title>$else$<title>$title$ — Levi Neuwirth</title>$endif$
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="shortcut icon" href="/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="stylesheet" href="/css/base.css"> <link rel="stylesheet" href="/css/base.css">
<link rel="stylesheet" href="/css/layout.css"> <link rel="stylesheet" href="/css/layout.css">
<link rel="stylesheet" href="/css/typography.css"> <link rel="stylesheet" href="/css/typography.css">

View File

@ -2,6 +2,7 @@
<nav class="site-nav"> <nav class="site-nav">
<!-- Row 1: primary links --> <!-- Row 1: primary links -->
<div class="nav-row-primary"> <div class="nav-row-primary">
<a href="/" class="nav-logo" aria-label="Home"></a>
<div class="nav-primary"> <div class="nav-primary">
<a href="/">Home</a> <a href="/">Home</a>
<a href="/library.html">Library</a> <a href="/library.html">Library</a>