Files
volumen/CHANGELOG.md
T
petrbalvin f8ed33df83
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Initial commit
Assisted-by: GLM 5.3
2026-09-29 10:03:32 +02:00

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]

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, cover with alt text and caption, series with series_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 mermaid code block renders server-side to an inline SVG, with flowcharts (including the historical graph spelling) 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 doi and orcid in 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 bare 10.…/… 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 refs array 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 translations map, with all_langs for a post that belongs to every language.
  • Scheduled publishing: volumen publish-due for 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 to revision_limit versions; deleting a post moves it into that archive, so it stays undoable.
  • Post templates, an import of a .md file, 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 TOML key = value lines for any editor input (series, doi, orcid, author, lang, cover, excerpt, …), stored in templates.toml under [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, tag and q filters, 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 q search 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_next and has_prev, or a cursor with next_cursor.
  • ETag on the list responses and on a post detail, If-None-Match answered with 304, and PUT and DELETE honouring If-Match, so a write is refused with 412 instead of silently overwriting a change the client never saw.
  • A post's doi and orcid appear in the payload and in the JSON-LD block as resolvable identifiers, and a post with refs carries its references array, each entry's DOI, arXiv id and ORCID turned into a resolver link and the JSON-LD block carrying the citation objects beside them: the citation the web can hand to a reference manager.
  • Frontmatter keys the engine does not consume itself pass through in a fields object 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 a 429 carrying Retry-After.
  • Personal access tokens with the write and delete scopes for POST, PUT and DELETE, 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 error code and, where it helps, a message and a field.

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 admin and author roles, 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 oklch neutrals 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 translations frontmatter, 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, validate and version, 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 --config the server reads /etc/volumen/config.toml if it exists, then ~/.config/volumen/config.toml, and with neither it runs on per-user defaults whose state lives under ~/.local/share/volumen (honouring XDG_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 Volumen and the description Powered by Volumen., in the API, the feeds and the admin.
  • volumen doctor and volumen validate check an installation and its content, volumen status reports 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 /healthz reports readiness for a supervisor.
  • volumen export and volumen import move 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.key beside the users file and reuses it across restarts, so an operator never edits a config to get sessions that survive. [admin].session_key remains 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-Id and 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_proxies names the addresses whose X-Forwarded-For may be believed; an empty list never reads the header.
  • Read, header, write and idle timeouts on the server, and a shutdown on SIGINT or SIGTERM that drains the requests in flight.
  • NOTICE.md in 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 gain rel="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, HttpOnly and SameSite=Strict, Secure when 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-Site when the browser sends it, the Origin header against Host otherwise. 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.txt and refuses a version that is not newer.
  • The backend binds to loopback behind a TLS-terminating proxy, and trust_proxy takes the client address from the last X-Forwarded-For entry, the one the proxy appended.