251 lines
15 KiB
Markdown
251 lines
15 KiB
Markdown
# Changelog
|
|||
|
|
|
||
|
|
All notable changes to **Volumen** are documented in this file.
|
||
|
|
|
||
|
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
||
|
|
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||
|
|
|
||
|
|
## [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.
|