15 KiB
15 KiB
Changelog
All notable changes to Volumen are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[development]
Fixed
- An installation running the newest release was offered an update to the
same version: the recorded version and the release tag compared unequal
through their leading
v. The comparison now accepts both shapes, so v1.0.0 against 1.0.0 is no update at all, and the update button can no longer reinstall the version that is already running. - A profile photo upload landed in the media library as a tile. Photos now
live under
avatars/inside the media directory and the library never lists them; photos of installations that predate the directory stay out of the tiles as well, and a media delete can no longer remove a whole directory through a crafted URL.
Added
[1.0.0] - 2026-09-29
Added
- First release of the platform, the API, the admin and the command line.
Content
- Posts are Markdown files with a TOML frontmatter block:
title,slug,date,lang,author,tags,excerpt,coverwith alt text and caption,serieswithseries_order,draft,publish_at,all_langs,aliases,translations,fediverse_creator,doi,orcid,refs, and any key of your own, preserved in the order you wrote it. - Mathematics:
$…$and$$…$$in a post body render as MathML Core. The conversion is server-side and carried by scriptorium, whose symbol tables cover the standard TeX surface, so equations reach every consumer of the API as native HTML with no JavaScript and no external service. A display equation may span lines, the shape the papers in the corpus are written in. A construct no MathML element can carry is never guessed at: it stays visible as its verbatim TeX in the equation, marked as an error, exactly where the author wrote it. - Diagrams: a fenced
mermaidcode block renders server-side to an inline SVG, with flowcharts (including the historicalgraphspelling) and sequence diagrams carried in full: every node shape and edge kind, subgraphs, classes and styles on one side, participants, all arrow kinds, notes, activations, the block constructs, coloured rects, autonumbering and dividers on the other. A diagram type outside the two families stays the code block the author wrote, and a line that does not parse keeps it too, so nothing is half-drawn. - Scholarly identifiers are first-class: a post can carry
doiandorcidin its frontmatter and the editor, and an account can carry its owner's ORCID, which pre-fills the field of new posts. A DOI is normalised from a doi.org URL or doi: prefix to the bare10.…/…form; an ORCID is checked to its ISO 7064 check digit. Validation is syntactic and offline: the engine never calls doi.org or orcid.org. - Bibliography is first-class: a post carries a
refsarray of tables in its frontmatter and marks where the list belongs with a[[refs]]line in the body. The engine renders a numbered reference section, links every inline[n]citation to its entry, and turns each entry's DOI, arXiv id and ORCID into resolver links; hand-written entries keep their verbatim text with their identifiers made live. A reference whose DOI belongs to another post of the same instance links to that post instead of leaving for the resolver. - Multi-language posts: one file per language, either in the content root or
in a per-language subdirectory, linked by a
translationsmap, withall_langsfor a post that belongs to every language. - Scheduled publishing:
volumen publish-duefor cron or a systemd timer, and the in-process[scheduler], which publishes the due posts at start-up and then every interval. - Post revisions: every save archives the previous version under
posts/.revisions/<slug>/, pruned torevision_limitversions; deleting a post moves it into that archive, so it stays undoable. - Post templates, an import of a
.mdfile, and a download of any post with its frontmatter. A template pre-fills the new-post form: its name, title, slug, tags and body, plus TOMLkey = valuelines for any editor input (series,doi,orcid,author,lang,cover,excerpt, …), stored intemplates.tomlunder[templates.fields]and shown on the template's row, so a scientific volume or a short note starts with its series and author already in place. - A media library of uploaded images, and cover images per post. Uploads are
stored under a UUID with the extension their bytes carry, WebP, AVIF and
SVG being the recognised formats, an SVG recognised by its root element;
each tile shows the pixel size read from the container headers or from the
SVG root's size attributes and
viewBox, and the library takes several uploads at once, filters by name, and offers copy-link, open and delete on every tile.
Public API under /api/volumen/
- Site metadata, paginated post lists with
lang,tagandqfilters, single posts with raw Markdown, rendered HTML and a table of contents, several posts in one request, tag and series listings, RSS 2.0, Atom 1.0, JSON Feed 1.1 and an XML sitemap. - The
qsearch ranks its results by relevance across the title, the tags, the excerpt and the body: a title hit leads, and equal scores keep the date order. - Pagination in two shapes: page numbers with
has_nextandhas_prev, or a cursor withnext_cursor. ETagon the list responses and on a post detail,If-None-Matchanswered with304, andPUTandDELETEhonouringIf-Match, so a write is refused with412instead of silently overwriting a change the client never saw.- A post's
doiandorcidappear in the payload and in the JSON-LD block as resolvable identifiers, and a post withrefscarries itsreferencesarray, each entry's DOI, arXiv id and ORCID turned into a resolver link and the JSON-LD block carrying thecitationobjects beside them: the citation the web can hand to a reference manager. - Frontmatter keys the engine does not consume itself pass through in a
fieldsobject on a post detail, with nested tables, arrays and date-times intact. - A sliding-window rate limit per client address with
X-RateLimit-*headers and a429carryingRetry-After. - Personal access tokens with the
writeanddeletescopes forPOST,PUTandDELETE, for publishing from scripts and CI. Only the SHA-256 digest is stored, and the raw token is shown once. - One error envelope for every failure, with a machine-readable
errorcode and, where it helps, amessageand afield.
Admin under /admin/
- A first-run wizard founds the installation in place of the login: it creates the administrator account, takes the interface language and the colour scheme with a live preview, shows a password strength meter against the configured policy, and ends with the operator signed in. The first account is created in one write and under a lock, so two visitors claiming a fresh installation cannot both open an identity.
- Username and password login with the
adminandauthorroles, CSRF tokens on every state-changing form, and a strict Content-Security-Policy with a per-request nonce. Sessions are bound to the password they were issued under, so a password change retires every session issued before it, and an admin can reset another account's password from Settings, Users. - An optional second factor for any account: time-based one-time passwords with the enrolment QR drawn by the server itself, one-time recovery codes shown exactly once and stored only as digests, a replay floor that refuses a code a second time, and the same lockout guarding code guesses as password guesses. Nothing changes for an account until its owner finishes the setup.
- The interface is built on the author's website design system: a layered
stylesheet with
oklchneutrals and the Viridis, Plasma and Magma palettes from the exact matplotlib colour-map stops, self-hosted Ubuntu and Ubuntu Mono, a light/dark/system mode toggle, one shared icon sprite, Graphis, native dialogs for confirmations and prompts, and a sidebar that folds to an icon rail remembered on the device. On the post list the status and tag filters lead with a funnel and a tag glyph and the card's Edit action takes a pencil. The sheets and fonts are served under/admin/assets/and revalidated by a content ETag, so a visit fetches them once per change. The interface ships in English and Czech, both per-account choices, and the login screen follows the last one. - A dashboard with status counts, a tag cloud, search, status filters, bulk
publish, draft and delete, and the next scheduled posts with their publish
dates. The list shows one card per publication: the language versions are
merged by their
translationsfrontmatter, the card names the version in your interface language, and small language chips switch it to another version; a bulk selection acts on the whole publication, and the counters and the tag cloud count publications, not files. - A post editor with a Markdown source view and a visual view over the same
document, live preview, toolbar and keyboard shortcuts, drag-and-drop and
pasted image upload, slug generation, reading-time counters and autosave
with restore. The bibliography has its own card below the editor, as the
reference list sits below the body of a paper: it lists every entry the
post carries, edits the verbatim citation or the structured fields
(authors with ORCID, venue, year, volume, pages, DOI, arXiv, URL),
reorders and removes entries, inserts the
[[refs]]marker into the body, names its count in the heading and scrolls inside itself. A save writes the list back as[[refs]]tables with every other frontmatter key untouched and the identifiers checked on the way in: a DOI normalises from a doi.org URL, an ORCID checks its own digit. The live preview splices in the saved post's bibliography, so the author sees what the page will show. - Revision history per post, with download of any revision, a line-difference comparison against the current content and one-click restore, and an undo for the last delete.
- Settings: account (password, username, display name, fediverse handle, profile photo), users and roles, post templates, the media library, API tokens, webhooks with a test delivery and per-endpoint enable and remove that apply without a restart, backup and restore, the version panel with a checksum-verified in-place self-update, and the audit log switch.
- The surface reaches a phone: below 760 px a navigation sheet behind a hamburger button carries the sidebar links, the signed-in user and the logout button, and every control keeps a visible keyboard focus.
Command line
serve,status,doctor,check-update,export,import,publish-due,validateandversion, each with-h, and every operational failure reported with a non-zero exit code. A stray positional argument is a usage error.- A manual page,
man/volumen.1, documents every subcommand and flag, and the README links it beside the command list.
Operations
- Installing is: copy the binary, run
volumen serve, open/admin. With no--configthe server reads/etc/volumen/config.tomlif it exists, then~/.config/volumen/config.toml, and with neither it runs on per-user defaults whose state lives under~/.local/share/volumen(honouringXDG_DATA_HOME), so a plain start works without root and without writing any file first. The commented configuration template,config.toml.example, is committed at the repository root. - A fresh installation presents itself as Volumen: the default site title is
Volumenand the descriptionPowered by Volumen., in the API, the feeds and the admin. volumen doctorandvolumen validatecheck an installation and its content,volumen statusreports the installation's state, and a deployment with no accounts yet reports the first-run wizard as the next step (a warning, not a failure).GET /healthzreports readiness for a supervisor.volumen exportandvolumen importmove a whole deployment, or the admin Backup panel does, through the same archive: the posts, media and revisions under the content directory, plus the users, templates and tokens files.- The session secret is generated by the server: on first start it writes a
64-character key to
secret.keybeside the users file and reuses it across restarts, so an operator never edits a config to get sessions that survive.[admin].session_keyremains as an explicit override. - Structured JSON logging with
[server].log_format = "json", an append-only audit log, and outgoing webhooks that notify a front end when a post changes. - Every request carries a 16-character id, answered in
X-Request-Idand attached to each line the handlers write while serving it, and one access line per request records the method, the path, the status and the duration. [server].trusted_proxiesnames the addresses whoseX-Forwarded-Formay be believed; an empty list never reads the header.- Read, header, write and idle timeouts on the server, and a shutdown on
SIGINTorSIGTERMthat drains the requests in flight. NOTICE.mdin the repository reproduces the licence of every Go module the binary is compiled from and the terms of the artwork embedded in it.
Security
- Post bodies are rendered by scriptorium and then sanitised by bluemonday
against a strict allowlist, so a body is treated as untrusted even though
its author is authenticated;
<script>, event handlers, inline styles and unknown URL schemes do not survive, and links gainrel="noopener noreferrer". - Passwords are hashed with scrypt at fixed parameters and compared in constant time; a stored hash below the policy floor or above a 64 MiB memory bound is rejected rather than trusted.
- Sessions are HMAC-SHA256-signed cookies,
HttpOnlyandSameSite=Strict,Securewhen TLS terminates in front. Production refuses to start without a session key of at least 64 bytes. - The admin refuses a state-changing request a browser sends from another
origin before any handler runs, using the standard library's fetch-metadata
rule:
Sec-Fetch-Sitewhen the browser sends it, theOriginheader againstHostotherwise. The per-session CSRF token stays as the inner guard, because it also covers a same-site request from another port. - Login attempts are limited per address, and the failure message is generic, so neither the rate limit nor the response time reveals whether an account exists.
- Uploads are accepted only when the bytes carry a WebP or AVIF signature or the root element of an SVG, and the stored extension comes from those bytes rather than from the file name or the declared type.
- Media names, slugs, revision names and backup-archive members are confined
to their directories, and a restore writes through an
os.Root; an archive cannot plant a document where the public media route would serve it. - The self-update verifies the download against the release's
checksums.txtand refuses a version that is not newer. - The backend binds to loopback behind a TLS-terminating proxy, and
trust_proxytakes the client address from the lastX-Forwarded-Forentry, the one the proxy appended.