• v1.0.0 f8ed33df83

    v1.0.0
    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
    Stable

    petrbalvin released this 2026-09-29 08:03:48 +00:00 | 0 commits to main since this release

    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.
    Downloads