2026-06-21 13:15:06 +02:00
|
|
|
|
# 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).
|
|
|
|
|
|
|
2026-06-25 21:37:27 +02:00
|
|
|
|
## [development]
|
|
|
|
|
|
|
2026-06-26 17:40:48 +02:00
|
|
|
|
### Added
|
|
|
|
|
|
|
|
|
|
|
|
- **Fediverse attribution:** a `fediverse_creator` frontmatter field (e.g.
|
|
|
|
|
|
`@user@mastodon.social`) plus a site-wide `[site].fediverse_creator` default
|
|
|
|
|
|
in `config.toml`. The value is exposed as `fediverse_creator` on post and
|
|
|
|
|
|
site payloads, rendered as `<dc:creator>` in the RSS feed, as `authors[].name`
|
|
|
|
|
|
in the JSON feed, and can be consumed by a front-end to emit
|
|
|
|
|
|
`<meta name="fediverse:creator">` on rendered pages. Per-post values
|
|
|
|
|
|
override the site default.
|
2026-06-26 20:49:48 +02:00
|
|
|
|
- `has_next` and `has_prev` boolean flags in the paginated
|
|
|
|
|
|
`/api/volumen/posts` response so clients can navigate pages without
|
|
|
|
|
|
computing boundaries themselves.
|
|
|
|
|
|
- Distinct `"draft"` error code in `/api/volumen/posts/:slug` when the post
|
|
|
|
|
|
exists but is a draft, replacing the generic `"not_found"`.
|
|
|
|
|
|
- `just lint` recipe for standalone RuboCop checks and `just fmt` for
|
|
|
|
|
|
auto-fixing lint issues.
|
|
|
|
|
|
- `config.toml.example` template for local development; `config.toml` is
|
|
|
|
|
|
now gitignored.
|
|
|
|
|
|
|
|
|
|
|
|
### Changed
|
|
|
|
|
|
|
|
|
|
|
|
- Memoize `published_posts` within a single request so repeated calls
|
|
|
|
|
|
(posts list, tags, feeds, sitemap) reuse the cached result.
|
|
|
|
|
|
- `Store#all` cache now invalidates on individual file mtime changes, not
|
|
|
|
|
|
just on the content directory mtime. Manual edits and `git pull` are
|
|
|
|
|
|
detected without a restart.
|
|
|
|
|
|
- `Cache-Control: public, max-age=60` header on all public API responses
|
|
|
|
|
|
for browser and CDN caching.
|
|
|
|
|
|
- Extract feed and sitemap generation into a dedicated `FeedHelpers` module,
|
|
|
|
|
|
reducing `Server` from ~500 to ~420 lines.
|
|
|
|
|
|
|
|
|
|
|
|
### Security
|
|
|
|
|
|
|
|
|
|
|
|
- **Content-Security-Policy** header on all `/admin/*` responses:
|
|
|
|
|
|
`default-src 'self'`, `script-src 'self' 'unsafe-inline'`,
|
|
|
|
|
|
`style-src 'self' 'unsafe-inline'`, `img-src 'self' data:`,
|
|
|
|
|
|
`frame-ancestors 'none'`.
|
|
|
|
|
|
- File uploads are rejected (HTTP 413) when exceeding **10 MB**
|
|
|
|
|
|
(`MAX_UPLOAD_BYTES`).
|
|
|
|
|
|
- File uploads are rejected (HTTP 415) unless the MIME type is one of
|
|
|
|
|
|
JPEG, PNG, GIF, WebP, AVIF, or SVG (`ALLOWED_UPLOAD_TYPES`).
|
2026-06-26 17:40:48 +02:00
|
|
|
|
|
2026-06-25 22:10:43 +02:00
|
|
|
|
## [0.2.0] — 2026-06-25
|
|
|
|
|
|
|
2026-06-25 21:37:27 +02:00
|
|
|
|
### Added
|
|
|
|
|
|
|
|
|
|
|
|
- `<pubDate>` in RSS feed items, derived from the post date.
|
|
|
|
|
|
- `<lastmod>` in sitemap entries for SEO.
|
|
|
|
|
|
- In-memory rate limiting on the login endpoint: 10 attempts per 60 seconds per IP.
|
2026-06-25 21:41:58 +02:00
|
|
|
|
- `<language>` element in the RSS feed channel.
|
|
|
|
|
|
- Periodic cleanup of stale `login_attempts` entries to prevent unbounded memory growth.
|
2026-06-25 21:51:32 +02:00
|
|
|
|
- Slug format validation (`[a-z0-9._-]`) preventing path traversal and XSS in templates.
|
|
|
|
|
|
- Username validation in `change_username` matching the create-user regex.
|
|
|
|
|
|
- Escape-on-output for slug and username in admin template attributes.
|
|
|
|
|
|
- Memoization of `Store#all` with mtime-based invalidation, eliminating N×read+parse
|
|
|
|
|
|
per request.
|
|
|
|
|
|
- Atomic file writes (tmp + rename) for posts and `users.toml`.
|
|
|
|
|
|
- Post language directory routing: non-default-language posts now land in
|
|
|
|
|
|
`content_dir/<lang>/` instead of overwriting root-level files.
|
|
|
|
|
|
- CLI test coverage (version, help, unknown command, option parsing, config bootstrap).
|
|
|
|
|
|
- Test coverage for RSS feed and sitemap endpoints.
|
2026-06-25 21:55:33 +02:00
|
|
|
|
- CORS preflight (`OPTIONS /api/volumen/*`) so browsers can reach the JSON API.
|
|
|
|
|
|
- `Users#all` memoization with mtime-based invalidation, matching Store#all.
|
|
|
|
|
|
- Session invalidation: deleted users can no longer use existing session cookies.
|
|
|
|
|
|
- CSRF protection on `POST /admin/preview`.
|
|
|
|
|
|
- Symlink-safe containment in `Store#media_file` via `File.realpath`.
|
2026-06-25 21:37:27 +02:00
|
|
|
|
|
2026-06-25 22:04:44 +02:00
|
|
|
|
### Added
|
|
|
|
|
|
|
|
|
|
|
|
- **Scheduled publishing:** a `publish_at` frontmatter field hides posts from the
|
|
|
|
|
|
public API and feeds until their publication date arrives. The admin form
|
|
|
|
|
|
includes a Publish at date picker.
|
|
|
|
|
|
- **JSON Feed:** `GET /api/volumen/feed.json` serves a jsonfeed.org v1.1 feed
|
|
|
|
|
|
with `content_html` and `date_published` per item.
|
|
|
|
|
|
- **Fulltext search:** `GET /api/volumen/posts?q=…` filters posts by
|
|
|
|
|
|
case-insensitive match in title and body.
|
|
|
|
|
|
|
2026-06-25 21:37:27 +02:00
|
|
|
|
### Changed
|
|
|
|
|
|
|
|
|
|
|
|
- Split `lib/volumen/server.rb` into `api_routes.rb` and `admin_routes.rb` modules,
|
|
|
|
|
|
keeping the server class focused on configuration and shared helpers.
|
|
|
|
|
|
|
|
|
|
|
|
### Fixed
|
|
|
|
|
|
|
|
|
|
|
|
- `admin.session_ttl` was defined but never read; it now controls
|
|
|
|
|
|
`Rack::Session::Cookie` expiration via `expire_after`.
|
2026-06-25 21:41:58 +02:00
|
|
|
|
- Derived post excerpts now strip HTML tags before stripping markdown markup,
|
|
|
|
|
|
avoiding raw HTML leaks in auto-generated excerpts.
|
2026-06-25 21:51:32 +02:00
|
|
|
|
- Draft posts no longer leak through the public detail endpoint (`/api/volumen/posts/:slug`).
|
|
|
|
|
|
- `Config::DEFAULTS` inner hashes are now frozen and `deep_merge` builds fresh
|
|
|
|
|
|
copies, preventing mutation from silently poisoning later `Config.load` calls.
|
2026-06-25 21:55:33 +02:00
|
|
|
|
- A warning is emitted when `admin.session_key` is configured but shorter than
|
|
|
|
|
|
64 bytes, instead of silently falling back to a random secret.
|
2026-06-25 21:37:27 +02:00
|
|
|
|
|
2026-06-21 13:15:06 +02:00
|
|
|
|
## [0.1.0] — 2026-06-20
|
|
|
|
|
|
|
|
|
|
|
|
First public release: a small, file-based Markdown blog engine in CRuby. Posts
|
|
|
|
|
|
are plain files, the engine exposes them as a JSON API and ships a self-hosted
|
|
|
|
|
|
admin — no database, no external services.
|
|
|
|
|
|
|
|
|
|
|
|
### Added
|
|
|
|
|
|
|
|
|
|
|
|
**Content & storage**
|
|
|
|
|
|
|
|
|
|
|
|
- File-based store: each post is a `.md` file with a TOML frontmatter block
|
|
|
|
|
|
delimited by `+++`. No database.
|
|
|
|
|
|
- Post fields: `title`, `slug`, `date`, `lang`, `author`, `tags`, `draft`,
|
|
|
|
|
|
`excerpt`, `cover`, `all_langs`, and `translations` (a `lang → slug` map).
|
|
|
|
|
|
- Drafts (`draft = true`) are hidden from the public API.
|
|
|
|
|
|
- Excerpt falls back to the first paragraph (~200 chars) when none is set.
|
|
|
|
|
|
- Estimated reading time (~200 words/min) computed per post.
|
|
|
|
|
|
- Multilingual posts: per-language slugs via `translations`, plus `all_langs`
|
|
|
|
|
|
to surface a single post under every language.
|
|
|
|
|
|
|
|
|
|
|
|
**Markdown rendering**
|
|
|
|
|
|
|
|
|
|
|
|
- GFM rendering via kramdown: tables, fenced code blocks, task lists, and
|
|
|
|
|
|
strikethrough.
|
|
|
|
|
|
- Automatic heading IDs for in-page anchors.
|
|
|
|
|
|
- Raw HTML in post bodies is passed through (authors are the trusted admin).
|
|
|
|
|
|
|
|
|
|
|
|
**Public JSON API (`/api/volumen`)**
|
|
|
|
|
|
|
|
|
|
|
|
- Read-only endpoints: `site`, `posts`, `posts/{slug}`, `tags`, `feed.xml`
|
|
|
|
|
|
(RSS), and `sitemap.xml`.
|
|
|
|
|
|
- `posts` supports pagination (`page`, `limit`, returning `total` and
|
|
|
|
|
|
`page_size`) and filtering by `lang` and `tag`.
|
|
|
|
|
|
- Permissive CORS so a separate front end can consume the API.
|
|
|
|
|
|
|
|
|
|
|
|
**Admin (`/admin`)**
|
|
|
|
|
|
|
|
|
|
|
|
- Server-rendered admin with a sidebar layout and full post CRUD.
|
|
|
|
|
|
- Dual-mode editor: Markdown source (primary) and a visual WYSIWYG, switchable
|
|
|
|
|
|
per post, with a toggleable live preview persisted across sessions.
|
|
|
|
|
|
- Image upload from the editor toolbar into `content_dir/media`; any image
|
|
|
|
|
|
format is accepted (WebP and AVIF included) and served with the correct
|
|
|
|
|
|
`Content-Type`.
|
|
|
|
|
|
- Cover image field with upload, URL, clear, and preview.
|
|
|
|
|
|
|
|
|
|
|
|
**Multi-user & roles**
|
|
|
|
|
|
|
|
|
|
|
|
- Multiple admin users stored in `users.toml`, managed from a settings page.
|
|
|
|
|
|
- Two roles: `admin` (full access incl. user management) and `author` (posts
|
|
|
|
|
|
only).
|
|
|
|
|
|
- Change password and username; last-admin and self-deletion safeguards.
|
|
|
|
|
|
|
|
|
|
|
|
**Configuration & CLI**
|
|
|
|
|
|
|
|
|
|
|
|
- TOML configuration, auto-generated with a commented template on first run.
|
|
|
|
|
|
- Configurable `host`/`port` (defaults to `9090`), `content_dir`,
|
|
|
|
|
|
`users_file`, and `site` metadata.
|
|
|
|
|
|
- CLI: `serve`, `hash-password`, `version`, `help`, with `--config`,
|
|
|
|
|
|
`--content`, `--host`, and `--port` overrides.
|
|
|
|
|
|
|
|
|
|
|
|
**Deployment**
|
|
|
|
|
|
|
|
|
|
|
|
- `install.sh` installs the Ruby toolchain from distro packages on Fedora and
|
|
|
|
|
|
openEuler (and prints guidance on other distros), creates the service user
|
|
|
|
|
|
and directories, and installs a systemd unit.
|
|
|
|
|
|
- Deployment docs for systemd and FreeBSD (`rc.d`), plus an nginx reverse-proxy
|
|
|
|
|
|
setup that runs the admin same-origin, with brute-force rate-limiting and an
|
|
|
|
|
|
optional IP allowlist on the login endpoint.
|
|
|
|
|
|
|
|
|
|
|
|
**Project & tooling**
|
|
|
|
|
|
|
|
|
|
|
|
- Documentation set: architecture, configuration, API reference, deployment,
|
|
|
|
|
|
and security.
|
|
|
|
|
|
- `just` task set (`install`, `run`, `dev`, `build`, `test`, `uninstall`) and
|
|
|
|
|
|
Forgejo Actions CI (RuboCop + tests on Ruby 3.3/3.4, gem build on release).
|
|
|
|
|
|
- `CONTRIBUTING.md` and a minitest suite; clean RuboCop.
|
|
|
|
|
|
|
|
|
|
|
|
### Security
|
|
|
|
|
|
|
|
|
|
|
|
- Passwords hashed with scrypt and verified in constant time.
|
|
|
|
|
|
- Per-form CSRF tokens on every state-changing admin request.
|
|
|
|
|
|
- Session cookies are `HttpOnly` and `SameSite=Strict`, and gain the `Secure`
|
|
|
|
|
|
attribute automatically on HTTPS requests (detected via `request.ssl?` /
|
|
|
|
|
|
`X-Forwarded-Proto`), so the cookie never travels over plain HTTP in
|
|
|
|
|
|
production.
|
|
|
|
|
|
- Configurable, stable `session_key` so sessions survive restarts.
|
|
|
|
|
|
- Generic login error message to avoid username enumeration.
|
|
|
|
|
|
|
|
|
|
|
|
[0.1.0]: https://codeberg.org/petrbalvin/volumen/releases/tag/v0.1.0
|