Commit Graph
63 Commits
Author SHA1 Message Date
LeoVasanko f8da552cfe contact messages: mail server setting, browser notifications for new messages
The mail server field is plumbing only (mailbridge URL in settings +
editor UI); actual email sending is deferred. Browser notifications:
permission is asked on the user's first send, then the visitor is
notified of admin replies while not viewing the thread and the admin of
new visitor messages while the inbox is open.
2026-09-29 02:06:54 +00:00
LeoVasanko 719546a0ee contact messages: document the PoW quota, email reveal and trail marker
docs/messages.md matches the quota protocol (proof frames, quota acks,
contact-email, upload 429 re-auth); docs/analytics.md notes the
server-originated contact trail marker; AGENTS.md's tracking/messages
bullets match.
2026-09-28 14:42:47 +00:00
LeoVasanko 106712a359 contact messages: document the settings, localization and toggle
docs/messages.md gains the site-settings/localization section (enabled
toggle, mailto link, chunk-keyed string translations and their manual
override UI); docs/localization.md notes the site strings riding the
title/nav pipeline; AGENTS.md's i18n/translate bullets match.
2026-09-28 06:35:55 +00:00
LeoVasanko afa6b967e6 contact messages: unified overlay, corner ✉️ icon for everyone
The admin inbox leaves the short-lived /_m page and joins the visitor panel as one overlay (ContactOverlay shell over paskia's shared blur backdrop, centered dialog, full-screen on mobile): the corner ✉️ icon opens the visitor thread for visitors and the wider sidebar inbox for admins, while {contact:} trigger buttons always open the visitor side so admins can test both. The icon highlights unread state without extra connections: visitors via localStorage seen/last-admin timestamps, admins via the contact_unread count added to the /_api/settings probe, both kept live by the bundle once loaded. MessagesView owns its admin socket only while the overlay is open.
2026-09-28 01:37:37 +00:00
LeoVasanko 4ad4e3bdff contact messages: standalone /_m admin inbox, site-name reply label
The inbox moves out of the analytics tab to its own page at /_m (own pens-row icon, own entry and socket lifecycle; the admin-gated /_api/ws/analytics socket keeps serving both, with typed messaging frames ignored by AnalyticsView). Admin replies are labeled with the currently configured site name in the wire payload (the SSO name stays stored for audit), and the thread view is capped to roughly the visitor panel's width.
2026-09-27 18:59:22 +00:00
LeoVasanko e3080d70c0 contact messages: hold the PoW challenge as a WS-handler local
Move the PoW state from a module-level dict keyed by WebSocket in
messages.py to a plain per-connection local in tracking.activity_ws,
making PoW a WS-level concern instead of being buried in the message
handler:

- activity_ws validates and burns the solution (raw.pop('pow')) before
  forwarding a message frame, and issues a fresh challenge after every
  registration and every vetted message frame; a failed validation is
  answered with a 'required' challenge and the frame never reaches
  messages.py.
- messages.py loses the _pow dict, _issue_pow and the disconnect
  cleanup for it; visitor_frame needs no return value since a vetted
  frame always triggers a fresh challenge (a too-long message simply
  comes with contact-error and required=false, so the client does not
  retry it).

The wire protocol and the frontend are unchanged.
2026-09-27 16:35:14 +00:00
LeoVasanko ec35bc71e8 contact messages: easypow proof-of-work for sends, pending-upload cleanup
New dependency: easypow (PBKDF2-SHA512 challenges, bundled JS solver).

Sends are PoW-gated: registration/resync and every accepted message
come with a fresh single-use challenge (~5 s solve), held in per-socket
RAM state only — nothing in the database. A message burns one
solution; a missing, stale or spent solution is dropped and answered
with a fresh 'required' challenge. The bundled browser solver is
served at /_contact/easypow.js straight from the installed package, so
client and validator versions always match.

Uploads are deliberately not PoW-gated (the transfer rate is the real
cost). Instead, uploads are tracked as pending per session and a quota
caps pending files at 10 — answered 429, not 403, which could trip
auth flows. A sent message strikes the files it references off the
pending set; when the session's last socket closes, the remaining
pending files are deleted from the store.
2026-09-27 06:13:53 +00:00
LeoVasanko 6b34719397 contact messages: inert .file attachment blobs, universal forced download
Rework attachment storage and serving per the refined separation
scheme:

- Non-media uploads are stored as {hash}.file blobs (store_attachment)
  — the original extension never lands on the filesystem, so the store
  directory holds nothing double-click-executable. The original name
  lives only in the URL: /_f/{hash}/{original-name}. The .file suffix
  itself is rejected in URLs.
- Serving is strictly separated: /_f/{name} serves media only (hash
  with or without extension, .orig. still blocked) and 404s .file
  names; /_f/{name}/{filename} serves only {name}.file blobs, never
  media, never by hash alone.
- Every /_f/ response now carries bare 'content-disposition:
 attachment' (the URL's last segment already names the download) and
  x-content-type-options: nosniff — media included. Subresource
  fetches (<img>, <video>, favicons, the zoom viewer) ignore the
  disposition, so inline media is unaffected, while a direct
  navigation can never render or sniff a store file into something
  executable in the site's origin. The mime-based script-carrier
  forcing and the header filename are gone (subsumed).
- Message uploads treat SVG as media (rasterized derivatives; an
  as-is-served .svg is script-inert inside <img>), and undecodable
  fake images fall through to the .file attachment path.
2026-09-26 03:35:08 +00:00
LeoVasanko 95554b4165 contact messages: allow all attachment types, enforce downloads
Drop the script-carrier blocklist: any file type (html, exe, svg, ...)
may be attached now, with safety from enforced downloads instead of
type filtering, verified twice:

- the attachment chip anchor carries a download attribute (already),
- the link is /_f/{name}/{original-filename}, a new file-store route
  that answers content-disposition: attachment naming the original
  file (ASCII-safe fallback + RFC 5987 UTF-8 form). The filename rides
  in the URL itself — no filename registry in the database.

Close the residual gap on the plain /_f/{name} route: a stored file
whose mime executes script in origin (html/xml/svg/js) is forced to
download there too, so stripping the filename segment off an
attachment URL cannot turn it into an in-origin page. All /_f/
responses now carry x-content-type-options: nosniff. Subresource
fetches (<img>, favicons) ignore Content-Disposition, so inline
images and icons are unaffected.

Undecodable fake images (store_image falls back to as-is storage) now
attach as files instead of being marked inline images.
2026-09-26 01:59:08 +00:00
LeoVasanko 975ebfe5e7 contact messages: docs
docs/messages.md covers the trigger, both socket protocols, the storage
model, the XSS stance (grammar restriction via a strict renderer, never
HTML sanitization), uploads, and the MVP gaps (PoW seams, moderation,
orphan GC, i18n). AGENTS.md and frontend-runtime.md learn the new
module, bundles and asset keys.
2026-09-26 01:17:30 +00:00
LeoVasanko d4add54f10 Editor sockets: auth-aware reconnect, shared with the analytics view
An expired session failed the /_api/ws/editor handshake exactly like a dead
network, so the page/banner editors retried forever — the WS API cannot
open the login dialog. reconnect.js gains authReconnect(): on close, probe
/_api/settings (dialog-less fetchJson); a 401/403 runs the paskia login
flow and reconnects the moment it completes, a declined login holds at a
'session expired' ConnNote, and only transient failures take the paced
backoff. A paskia auth-success message from anywhere in the page
reconnects a waiting editor at once. AnalyticsView moves from its inline
copy of the same flow onto the shared helper.
2026-09-25 18:19:05 +00:00
LeoVasanko 31b56c3e5e Serve each article's Markdown at .md URLs (and via Accept: text/markdown)
/pages.md (front page /.md) serves the page's source Markdown — per-language
hybrid assembly, title injected as '# title' when the content has no h1 —
with the same ETag/Last-Modified/no-cache scheme as the HTML, which now
carries vary: accept. Pages advertise the counterpart via
<link rel=alternate type=text/markdown> and llms.txt links point at the
.md URLs.
2026-09-25 18:04:47 +00:00
LeoVasanko 9ff22016d1 Add llms.txt, JSON Feed and RSS feed exports
New pagerite/feeds.py: /llms.txt (Markdown site map with excerpts for
LLM agents), /feed.json (JSON Feed 1.1) and /feed.xml (RSS 2.0 +
atom:link), all carrying every published article with full content and
absolutized URLs. Linked from every page <head> (feed alternates +
llms-txt) and from the sitemap, recorded in analytics like page GETs
and emoji-marked in the trails (🧠 llms.txt, 📡 feeds).
2026-09-23 07:25:08 +00:00
LeoVasanko 91a16f56c5 Redesign translation overrides: keyed, structural, hash-anchored
Replace the old "patches" format (path:lang composite keys, ordered
hunk lists, text-anchored matching) with Data.overrides:
path -> lang -> LangEdits, keyed throughout so a save's database diff
touches only the edited chunks. The old "patches" key is ignored on
decode, discarding legacy data without a migration.

- Whole-paragraph additions/deletions are structural: a drop flag on
  the original chunk hash, and additions in their own dict anchored
  from the neighboring chunks' before/after (first live referrer wins),
  so they stay in place across retranslation and one-sided original
  edits.
- Within-paragraph edits (up to a full paragraph rewrite or split) are
  full-chunk replace patches applied by chunk hash alone: a
  retranslation is overridden wholesale, so user edits survive AI
  re-runs; editing the original changes the hash and orphans the patch.
  The old search-matching staleness gate is gone.
- Saving a translation on a page without original chunks is rejected
  (REST 400 / WS error); emptying the original afterwards renders the
  translation empty, with the orphaned overrides inert.
2026-09-21 18:14:48 +00:00
LeoVasanko d0ce619db7 Improved auto translation support (#2)
- Added full article / with markdown mode for advanced LLMs, with local ollama and API support, new llm_translator script.
- Translate navigational titles as a whole (preserving site structure), and then retranslate as article headings with article context - potentially changing nav label
- Improved human overrides to translations, in particular when structure is affected

Reviewed-on: https://git.zi.fi/LeoVasanko/pagerite/pulls/2
2026-09-21 14:22:00 +00:00
LeoVasanko 4bccf6855e Auth via paskia-js: profile() dialog, apiFetch/apiJson in admin components
- Banner-corner auth link is now a button opening paskia-js's profile()
  dialog (handles login too); auth re-probed when the dialog closes.
- Editor/analytics components call /_api via apiFetch/apiJson: an
  expired session opens the login dialog and the request retries.
- pagerite.js: task-checkbox toggle uses apiJson (explicit edit attempt,
  reverts on any failure incl. cancelled login); auth probes use
  fetchJson (never a dialog); page-cache/navigation stay on plain fetch.
- Add the paskia npm dependency; document the convention.
2026-09-18 20:14:52 +00:00
LeoVasanko 69583fb9fe Replace ua-parser with uarite for UA formatting and bot detection 2026-09-05 06:15:43 +00:00
LeoVasanko db57125953 Public language selector, linked with the editor's language selection. Shared popup menu operation with edit toolbar with consistent closing logic. 2026-09-04 17:52:51 +00:00
LeoVasanko 986e28c220 Serve /favicon.ico as a redirect to the configured site icon 2026-09-04 14:59:38 +00:00
LeoVasanko 4413c7efdf Cleanup: break up the massive app.py into separate modules of manageable size. 2026-09-03 13:36:29 +00:00
LeoVasanko d3e2196c83 Localization (#1)
Implement comprehensive content localization, admin panels for editing each language, AI translation interface with automatic updates when base language version is changed.
- SEO tags for all language URLs
- Uses accept-language by default, ?lang=en overrides temporarily
- User edits patched on top of translations
- RTL language supportReviewed-on: https://git.zi.fi/LeoVasanko/pagerite/pulls/1
2026-09-03 03:54:27 +00:00
LeoVasanko 399aa95d44 Encapsulate all migrations in kanta migrate_vN, drop persisted Data.version
migrate_v1 now also rebuilds the legacy flat pages store as the menu
tree (moved from the app lifespan, raw-dict level); migrate_v2 now also
backfills missing AVIF/WebP/JPEG derivatives on disk (moved from the
lifespan) and drops the obsolete version field. The version render
counter was cache-invalidation state, not database state: replaced by an
in-memory render generation that clears the page-body LRU and feeds page
ETags. The legacy Page struct and Data.pages/version fields are removed;
old databases lose the stale keys on re-serialization.
2026-08-29 20:16:13 +00:00
LeoVasanko 775dc3f65a Look up themes and theme per-file overrides in system, user and site directories. 2026-08-29 02:41:29 +00:00
LeoVasanko 6910ccad73 Navigable section anchors: slug ids + self-links on h1/h2, scroll-tracked location hash.
Long-enough articles (3+ in-body h1/h2) get slug ids (python-slugify,
mirroring the editor's slugify.js) and the heading text becomes a
self-link (a.anchor) for clean link copying; {#id} always wins,
duplicates get -2/-3 suffixes, h3+ never. The implicit page title is
now injected as '# {title}' into the markdown (render(title=...)), so
it takes the same first-h1 path as an explicit one: no id, href=""
self-link scrolling to top, not counted toward the threshold. The
.body wrapper is gone — segments are direct article children — and the
editor preview swaps the whole article in one go. pagerite.js tracks
the reading position in the location hash (last tagged heading above
the viewport middle; cleared at the top, never on unscrollable pages)
and scrolls to anchors after fetch-navigation.
2026-08-28 16:12:17 +00:00
LeoVasanko 2060dc815c Per-hostname data directory <hostname>/{content.kantadb, analytics.json, files} from CLI arg; drop learned site_url.
The first positional CLI argument (default localhost) names the site's
public hostname and its data directory under the cwd, replacing the
CWD-relative pagerite.* files. The public origin (https://<hostname>)
is now authoritative configuration instead of a value learned from
admin browsers: POST /_api/site-url, Data.site_url and the pagerite.js
reporter are removed, and page rendering, sitemap, robots.txt and
analytics own_origin all use SITE_URL consistently (localhost falls
back to the request's base URL).
2026-08-28 14:57:04 +00:00
LeoVasanko f78229bd30 Refactor analytics to /_a instead of under article pages. 2026-08-20 23:16:51 +00:00
LeoVasanko b4e8fad090 Implement analytics feature
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
2026-08-20 18:43:57 +00:00
LeoVasanko 81f08e7760 Updated docs 2026-08-19 16:11:42 +00:00
LeoVasanko 1d65a57fdf SEO/social meta for content pages; full-height site editor
- views.py: description, canonical, Open Graph and twitter:card tags
  from heuristics over the rendered article — first paragraph as
  description, share image prefers a {.hero} image, then first raster,
  then first SVG; first <video> becomes og:video; published/modified
  times from the node. Absolute URLs from the request base.
- SiteEditor: panel fills the full window height; the brand-HTML and
  custom-CSS CodeMirror windows grow to share leftover space equally
  instead of fixed max-heights.
2026-08-19 15:52:24 +00:00
LeoVasanko 512ed91b31 Bottom-anchor banner artwork, instant navigation via in-memory page cache, seed/stars/structure tweaks
- 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.
2026-08-19 01:40:26 +00:00
LeoVasanko 3b074c02ed Seed demo content only on database creation via @kanta.bootstrap
Previously startup appended any missing seed pages into existing
databases, resurrecting deleted content on running sites.
2026-08-19 00:57:35 +00:00
LeoVasanko a9693e1dcb Nested sidebar nav, banner inherit label fix, editor UX polish
- 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
2026-08-19 00:25:07 +00:00
LeoVasanko 4dac16a9ac Unify admin editors into a tabbed EditorShell (site, structure, article, banner)
- 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
2026-08-19 00:14:05 +00:00
LeoVasanko f2715088d2 Add site-wide custom brand HTML with media upload
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.
2026-08-18 22:20:38 +00:00
LeoVasanko f8cdfd53c0 Add admonitions plus autolink and sub/superscript markdown extensions
!!! 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.
2026-08-18 22:00:00 +00:00
LeoVasanko e9297cac61 Add the summer theme: illustrated meadow banner, cohesive palette
A light, playful theme whose every color is sampled from its banner.svg
meadow scene (sky/grass/sun/flower pink): fixed daylit landscape page
background with a sun-glow echo, tilted gradient brand, flower bullets,
a dense grass strip (grass.svg, served via /_themes) under h1s, rounded
shadowed tables with a sunny hover sheen, and a layered-parallax banner
(sun rises, clouds drift, nearer hills move less) with idle animations
wrapped in prefers-reduced-motion.

The /_themes route now serves any theme asset (previously only
theme.css/banner.css) so CSS can reference sibling files like grass.svg.
2026-08-18 21:54:44 +00:00
LeoVasanko a75ad6f5da Overlay scrollbars via OverlayScrollbars
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.)
2026-08-18 20:30:08 +00:00
LeoVasanko 7f140001fa Simplify image/figure styling: markdown always renders standalone images as figures
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.
2026-08-18 20:29:56 +00:00
LeoVasanko 0ec9be330e Add the 'eyes' banner design; seed banners only on select sub pages
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.
2026-08-18 06:36:05 +00:00
LeoVasanko 87c0aac3c0 Add Server/Last-Modified headers and a {dates} dateline tag
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.
2026-08-18 06:15:45 +00:00
LeoVasanko 30947df16b Backend-served themes and selectable, inheritable banner designs
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.
2026-08-18 05:46:02 +00:00
LeoVasanko c0817330e7 Add favicon upload to the site editor
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.
2026-08-18 05:22:25 +00:00
LeoVasanko f125966bb7 Fix various inconsistencies of sidebar and link handling on categories with only one child. 2026-08-18 03:57:41 +00:00
LeoVasanko f11b772493 Integrate Paskia auth UI flows, hide delete for empty pages. 2026-08-17 22:32:18 +00:00
LeoVasanko a4fab3dcc9 Ping our own API for auth check, not auth backend (forward auth). 2026-08-17 21:54:26 +00:00
LeoVasanko 40617f8080 Replace UnifrakturMaguntia with New Rocker, fix font picker duplicates
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.
2026-08-17 19:50:40 +00:00
LeoVasanko 6a68a7832a Brand shrink to fit, added new fonts. 2026-08-17 19:36:18 +00:00
LeoVasanko c0bb3b73c1 Add corporate and nitro themes, inline theme banners, code palette sets
- 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.
2026-08-17 19:16:45 +00:00
LeoVasanko dcd1e5afc9 Prepare for forward-auth; edit buttons and functions conditional on server 401, login button if needed. 2026-08-17 03:58:59 +00:00
LeoVasanko 7db08ea821 Fix new-page flow, save/delete semantics, add Cormorant font
- 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
2026-08-17 02:54:12 +00:00