- weeklySeries shifts overlaid weeks onto the current week's time axis so
they overlay inside the plot instead of overflowing left; oldest weeks
paint first, current week on top
- Legend moved inside the visits chart's top right: current ISO week in
accent, past weeks as a single muted "Week M" / "Week M–N" specimen
- Past week curves use the muted color instead of faded accent
- Chart height reduced ~30% (180 -> 126)
- Charts render as single SVGs with axis labels inside the viewBox,
replacing the stretched plot + HTML overlay labels
- Rolling ranges end at now, t0 aligned to UTC day; bucket size follows
the window (6h up to 31 days) so "all" at its 30-day minimum renders
identically to "month"
- X labels always centered on their true position; no edge-align shifting
- rangeWindow simplified to rolling spans ending at now
- TransitionGraph "all" visual scale floored at the 30-day plot minimum
JS-running crawlers (Googlebot, GoogleOther, Applebot) execute pagerite.js
and send navigation pings, registering as visitors. Pings whose User-Agent
matches _is_bot_ua (any "bot" token plus listed exceptions) are now
ignored, so their document GETs flush to the crawler list as intended. No
source verification: a spoofed bot UA merely lands in the crawler stats,
and path-based abuse classification catches scanners regardless.
Idle-time link preloads from pagerite.js were queued as pending crawler
hits and flushed to the crawler list whenever the user navigated more than
10s later, so real visitors' subpage loads showed up as crawler hits.
Preload fetches now carry an x-pagerite-preload header and the document
GET handler skips tracking for them; the ping sent on actual navigation
does the counting.
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.