load() queried the live document for the pagerite:analytics-src meta,
but the swap never touches <head> — the meta only exists in the fetched
doc, so the app never mounted unless /_a was loaded directly. Also cache
the fetched HTML so the post-swap preload doesn't re-GET the page we
just navigated to.
- keep visitor charts y-axis minimum range at 10
- keep 'all' chart x-axis minimum span at 30 days
- group crawler hits by (ip, ua) and list top pages visited, show crawler page load counts as N× prefix
- store and display geoip city, keep geoip country overwrite
- stream live updates over WebSocket /_api/ws/analytics
- include family ring arcs in transition map crop bounds
- remove top UA summary, limit crawlers to 10 and visits to 20
- human-readable relative timestamps with UTC tooltip
- Edge widths grow logarithmically with the connection count (~1 px at
a single count, uncapped); connections below 1% of total traffic are
pruned, bounding the graph to ~100 edges.
- Beads: per-direction flows emitted at a rate linear in the count,
each bead simulated independently in JS (no in-flight limit), offset
onto right-hand lanes so opposing flows don't collide, running under
the node circles with a glow.
- External links: referer origins as a node row above the map, exit
origins fanned outwards from their source page.
- Transitions are now stored per 5-minute bucket (sparse
from -> to -> bucket -> count) so the graph filters by time range
like the other series; legacy analytics files are discarded.
Add server-side visit analytics collection, a public-page ping endpoint,
and a full-screen AnalyticsView for admins.
Backend:
- Add pagerite/analytics.py: Analytics/Visit model, Store, and persistence
- Wire /_a ping endpoint and GET /_api/analytics into pagerite/app.py
Frontend:
- Add full-screen AnalyticsView with visitor charts and transition map
- Add VisitorCharts and TransitionGraph subcomponents
- Add analytics JS helpers in frontend/src/analytics/
- Send navigation pings from frontend/src/pagerite.js
- Mount AnalyticsView from frontend/src/main.js
- Document the feature in docs/analytics.md and update AGENTS.md
- Fix banner artwork sizing: explicit 100% grid track so children
stretch instead of resolving height:100% against a content-sized row
(SVG intrinsic ratio bloated the row, cropping the artwork's bottom).
Bottom-anchor via object-position, transform-origin and YMax slice.
- pagerite.js: in-memory page cache — preload every visible internal
link once, serve navigation from memory without fetching; editors'
loadPlain keeps the cache in sync (pagerite:page-fetched).
- Structure editor: delete pages directly, no two-step confirmation.
- New 'stars' banner design (drifting starfield) alongside 'eyes'.
- Rewrite seed content: welcome page, three-level docs section covering
all Markdown features (source + rendered), showcase hierarchy with
image positioning and a simple leaf-page banner example.
Paskia does not support being iframed; the login/profile buttons are now
plain anchors, and a pageshow handler re-probes auth when history
navigation restores a cached page. Drops the paskia JS dependency.
CodeMirror measures text per character; Fira Code's ligature glyphs
render wider than the measured sum of their parts, corrupting cursor
and selection rendering. The font is set on .cm-content with ligatures
off, and inner spans inherit it for consistent metrics.
- Sidebar renders the section's whole subtree as nested lists (third
level and deeper, article-list-style markers); the one-item sidebar
rule yields when that item has children of its own; only the viewed
page is highlighted (navbar keeps ancestor highlighting); first_leaf
skips unpublished branches
- Banner design selector: inherit option names the resolved design and
its true source (backend banner_design_source excludes the node's own
setting; doc payload carries banner_design_inherited)
- Article editor: fenced code block on empty lines, table size picker
popup, borderless save icon disabled by saturate(0) alone
- While editing, window scroll is locked and only #main scrolls; the
panel exactly fills the available window height; editor scroll drives
the article
- One mounted shell with four kept-alive tabs; pens open/switch tabs,
closing only hides the shell so unsaved page-editor state survives
until a real reload; admin panels never reload the page, regions are
refreshed in place via the shared swapdoc.js helper
- SiteEditor split: site settings vs. the new StructureEditor tree tab;
BannerEditor split out of the old site editor
- Article editor format bar (bold/italic/code/link/table/image) with a
table size picker and Ctrl/Cmd-B/I/S bindings; fenced code block on an
empty line; icon-only save button disabled (saturate(0)) when clean
- Media uploads use icon buttons; favicon upload by clicking the preview
tile; placeholders reserved for actual defaults in effect
- Banner/site pens and auth buttons grouped in an .editor-pens container;
page pen no longer shown to anonymous visitors
- While editing, window scroll is locked, the panel exactly fills the
available window height, and only #main scrolls; editor scroll drives
the article
- Banner design inherit label names the design and its true source;
backend banner_design_source excludes the node's own setting and the
doc payload carries banner_design_inherited
Pages now declare width=device-width, so phones render at real device
width instead of a scaled-down 980px layout viewport. Below 48rem the
content becomes a single column with the sidebar lifted above the
article as a wrapping link strip; floated .left/.right figures fall back
to plain centered figures (explicit img widths still shrink-wrap) and
.wide keeps its centered full-viewport bleed.
Data.brand_html replaces the brand link entirely (rendered in a #brand
div on top of the banner, next to the nav); editable in the site editor
via a small HTML CodeMirror with image/video upload and paste, live
preview, and debounced save through /_api/settings.
!!! note/warning/tip/... blocks (mdit-py-plugins admon) render as
lightweight callouts: accent bar + faint wash in the blockquote idiom,
recolored per type (tip green, warning pink/red via --admonition-color),
rounded and softly shadowed in the summer theme. Also enabled
gfm_autolink (bare URLs) and H~2~O / x^2^ sub/superscripts.
Tables drop the hard 1px grid: a soft accent-tinted vertical gradient
header (overridable via --table-head-a/b), a very faint diagonal
--table-tint wash lifting the cells apart, and only a whisper of a row
separator. Definition lists become a lightweight two-column grid
(minmax term column, explicit grid-column placement so runs of
dt/dd stack right) with accent terms and no borders or fills. Tables
also join the bottom-margin-only spacing convention.
overflow: overlay is dead in current Chromium, so an appearing native
scrollbar shifted the layout again. OverlayScrollbars replaces it with
floating auto-hiding scrollbars that never reserve space (native window
scroll APIs unaffected on a body target). Themed via --os-* variables in
pagerite.css; the html scrollbar-color styling stays as no-JS fallback.
(package-lock.json is gitignored.)
Standalone markdown images (captioned or not) now become block <figure>
elements; only inline-with-text images and raw author <img> HTML stay
plain. This collapses the img/figure selector duplication in pagerite.css
into a default + override structure: figure fills the column, floats go
30% (1em text gap), an explicit width attribute shrink-wraps the figure
(images with width are left untouched by CSS so the attribute hint
survives), and .wide re-anchors are grouped with the main rule.
Banner designs can now ship arbitrary markup as banner.html (canvas +
style + script), taking precedence over banner.svg; the artwork is
inlined in a div[data-design] wrapper either way, which the editor's
live preview preserves (and never re-runs its scripts).
The bundled 'eyes' design (themes/eyes/banner.html + banner.css sizing
the stage) replaces the eyes canvas script that was embedded in the
notes-on-urls page's own banner — the page now just picks the design.
Seeds set banners only on select sub pages (the-long-read gradient,
canvas-nights stars): the front page no longer gets a banner image and
shows the theme's default design. Seed entries carry a banner_design
field.
Re-creating the #pagerite-theme link anchored to #pagerite-base, which
does not exist in dev (base CSS is a Vite-injected <style>), so the
fallback prepended it before the base styles and the theme lost the
cascade. Anchor to the following sheet (banner design / custom CSS) or
append at the end instead, keeping base < theme < design < custom order.
An http middleware sets 'Server: pagerite' (dropping uvicorn's versioned
default) and content pages return Last-Modified from Node.modified. In
markdown, a {dates} line expands to the article's published/updated
dateline (updated shown only when it falls on a later day); the editor
preview resolves it for pages that exist, unsaved pages show it literal.
Demonstrated in the long-read seed page.
Long articles now span from the left gutter (which holds the overlaying
sidebar) to the right viewport edge instead of reserving a symmetric
right gutter. The .wide breakout is re-anchored accordingly: left margin
is the gutter share (20vw of the 1fr+4fr grid, 1/5 of the post-editor
width while editing) plus main's padding; the <=102rem sidebar case keeps
its constant -13.25rem margin.
The 78rem minimum on the center track prevented the article from ever
shrinking, locking long articles into two fixed-width columns that
overflowed the window. Drop the floor: the 4fr center share now scales
with the window in both directions, so column widths flex continuously
and the count falls to 1 when space runs out.
Themes move from Vite-built frontend assets to pagerite/themes/{name}/
folders holding theme.css and/or banner.css (+ banner.svg), served by the
backend at /_themes/{name}/... and re-read from disk per request (etag by
mtime), so on-disk edits show on the next page load even in prod and new
themes need no build or config. The theme and banner-design selectors
enumerate these folders via GET /_api/settings.
Banner designs: Node.banner_design picks a design per page (None inherits
from ancestors, then the front page, then the active theme's own design;
"" = none). The design's banner.css is linked in <head> (id
pagerite-banner, between theme and custom CSS) and its banner.svg inlined
into #page-banner first (marked svg[data-design]); the page's own
Node.banner HTML renders after it, so author code always wins. #page-banner
is now a stacking grid so artwork and author code overlay.
Dev/prod hot loading unified: the backend renders the theme/design links
in both modes; in dev pagerite.js only re-appends them (and the custom
CSS) after the Vite-injected base styles. Theme switches just swap the
link href. The pagerite:theme meta and Vite theme build entries are gone.
Data.favicon names a blob in the content-addressed files store (no
migration: msgspec default). PUT/DELETE /_api/settings/favicon upload and
clear it; when set, every page links it as <link rel="icon">, otherwise
browsers fall back to the build's /favicon.ico. SiteEditor shows a preview
with upload/replace/remove and applies the change to the live page head.
Replace the fixed 100rem two-column breakpoint with 'columns: 30rem' so
CSS fits as many >=30rem columns as the article's width allows, and let
long (.multicol) articles grow past the 78rem cap (4:1 share against the
gutters, keeping them symmetric for the .wide breakout math). Also treat
mid-article h1s as full-width column separators like h2s.
Picker font-variable regexes only allowed [a-z-], so --font-exo2 was
never parsed or stripped: picking another font left the old row behind
and the selection showed wrong. Now digits are allowed, stripping is
declaration-level (handles inline rows) and removes all font rows
before inserting, and the picker re-parses on every CSS edit so manual
changes update the selection live.
- Two new themes (corporate light/dark, nitro racing) and a purple
retune; theme banner SVGs moved to pagerite/themes/, inlined by the
backend, recolored per scheme via CSS classes, with scroll parallax.
- pygments.css maps tokens onto --code-* variables; light and dark
palette sets in the base stylesheet resolve via light-dark() from the
theme's color-scheme; themes just pick a set or retint --code-bg.
- Sidebar omitted unless the section has at least two published items;
fetch-navigation and the site editor handle it appearing/disappearing.
- Floated figures keep captions below at 30% of the text column; h1/h2
clear floats so images never overflow into the next section.
- Links use --link (color-mix of text and accent), weight 500, no
underlines; hover goes full accent.
- New-page flow: slug placeholder shows the slug derived live from the
title (placeholders keep browser-default styling; slug values no longer
muted). Committing PUTs empty markdown, which now creates an empty page
rendering with its title instead of 404, then hands over to the page
editor (CodeMirror focuses on mount)
- Saving never deletes: REST PUT and WS save store empty content as an
empty page; deletion is the page editor's explicit choice — saving
trimmed-empty text issues a REST DELETE
- Fonts: add Cormorant (variable 300-700, normal+italic) to the serif
picks, replacing single-weight Gloock; Playfair Display files kept on
disk but not offered
- Base stylesheet: per-family font variables (--font-source-sans etc.)
with functional slots referencing them; --font-brand defaults to
var(--font-heading). Restore Fraunces/Literata faces dropped in the
font-set swap
- Site editor: stylized A button (always Fira Code) opens a font panel
with body/heading/brand tabs; options render their own name in the
candidate font at the target's size/weight; clicking the current pick
clears it. Picks are stored as plain :root rows in the custom CSS,
parsed and rewritten on change
- Base banner: drop black text-shadow on light background; legibility
via a bottom scrim fading artwork into --bg. Purple keeps its own nav
shadow
- Headings no longer accent-colored in base (moved to purple theme)
- Keep #pagerite-user custom CSS last in <head> at every mutation point
so its :root rules win equal specificity (dev injects base/theme
styles after the server-rendered tag)
- Data.theme/Data.custom_css site settings with /_api/settings support;
theme stylesheet linked per page, custom CSS inline as #pagerite-user,
swapped during fetch-navigation
- Site editor: theme selector, custom CSS CodeMirror field; in-place
stylesheet swap (diff-based, preserves editor stylesheets); dev swaps
Vite-injected style tags since no <link>s exist there
- Dev theme delivery via <meta name="pagerite:theme"> + dynamic import
in pagerite.js instead of a hardcoded purple import
- Fix dead edit pens in prod: Vite app builds strip entry exports, so
the dynamic-imported main.js had no openEditor/closeEditor; build with
preserveEntrySignatures 'exports-only' and log editor load failures
instead of swallowing them
- Editor slide-in is one-shot: cleared on animationend so stylesheet
swaps cannot restart a finished animation
- Replace Fraunces/Literata with Source Sans 3/Serif 4, Inter and
Montserrat variable fonts; font stacks as CSS variables
- Remove the /_admin route, views.render_editor, and standalone mode from
the Vue editor (main.js / PageEditor.vue)
- Remove /_admin dev proxy and preload exclusion
- Set vite appType to 'mpa' and delete frontend/index.html so unknown
/_ paths return 404 instead of an empty Vue shell
- Update AGENTS.md and docs/design-principles.md