11 KiB
11 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
0.3.0 — 2026-07-12
Added
Content & authoring
- Fediverse attribution: a
fediverse_creatorfrontmatter field (e.g.@user@mastodon.social) plus a site-wide[site].fediverse_creatordefault inconfig.toml. The value is exposed asfediverse_creatoron the site and post payloads, rendered as<dc:creator>in the RSS feed, asauthors[].namein 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. - Cover caption: a
cover_captionpost field rendered next to the cover image so authors can credit photos or add a short blurb. - Titled images as
<figure>: Markdown images with a title () are wrapped in a<figure>with a<figcaption>and a smalliinfo icon. Images without a title render exactly as before.
Public API
has_nextandhas_prevboolean flags in the paginated/api/volumen/postsresponse so clients can navigate pages without computing boundaries themselves.- Distinct
"draft"error code in/api/volumen/posts/:slugwhen the post exists but is a draft, replacing the generic"not_found".
Admin
- Modern admin UI: redesigned layout with a sticky top bar, breadcrumbs on every page, and a unified design language across login, post list, post form, and settings.
- Volumen icon: a dedicated SVG icon shown in the sidebar and on the
login page (served at
/admin/icon.svg). - Version in sidebar footer so admins always see which release is running.
- Per-user display name on the user record and admin header.
- Per-user fediverse handle editable from the settings page (independent
of the post-level
fediverse_creator). - Profile photo upload: each admin user can upload a WebP/AVIF avatar that is stored alongside posts in the content directory and shown in the settings page.
- Tabbed settings page separating account, profile, and admin sections.
- Restricted uploads: only WebP (
image/webp) and AVIF (image/avif) are accepted, with a 415 error message that points users tocwebp/avifencfor conversion. Reflects the engine's WebP/AVIF-only posture end-to-end. - Polished native inputs for
<input type="date|time|file">so they match the rest of the admin's design language.
Tooling
just lintrecipe for standalone RuboCop checks andjust fmtfor auto-fixing lint issues.config.toml.exampletemplate for local development;config.tomlis now gitignored.
Changed
- Memoize
published_postswithin a single request so repeated calls (posts list, tags, feeds, sitemap) reuse the cached result. Store#allcache now invalidates on individual file mtime changes, not just on the content directory mtime. Manual edits andgit pullare detected without a restart.Cache-Control: public, max-age=60header on all public API responses for browser and CDN caching.- Extract feed and sitemap generation into a dedicated
FeedHelpersmodule, reducingServerfrom ~500 to ~420 lines.
Fixed
- Admin post save/delete now correctly clears the in-memory posts cache, so the post list reflects edits without a manual reload.
- The post form's date field defaults to today when the underlying post has no date, preventing the picker from showing an invalid empty value.
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:,font-src 'self',connect-src 'self',form-action 'self',frame-ancestors 'none'. - File uploads are rejected (HTTP 413) when exceeding 10 MB
(
MAX_UPLOAD_BYTES). - File uploads are restricted to
image/webpandimage/avif(ALLOWED_UPLOAD_TYPES). Anything else is rejected with a 415 and a conversion hint.
0.2.0 — 2026-06-25
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.
<language>element in the RSS feed channel.- Periodic cleanup of stale
login_attemptsentries to prevent unbounded memory growth. - Slug format validation (
[a-z0-9._-]) preventing path traversal and XSS in templates. - Username validation in
change_usernamematching the create-user regex. - Escape-on-output for slug and username in admin template attributes.
- Memoization of
Store#allwith 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.
- CORS preflight (
OPTIONS /api/volumen/*) so browsers can reach the JSON API. Users#allmemoization 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_fileviaFile.realpath.
Added
- Scheduled publishing: a
publish_atfrontmatter 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.jsonserves a jsonfeed.org v1.1 feed withcontent_htmlanddate_publishedper item. - Fulltext search:
GET /api/volumen/posts?q=…filters posts by case-insensitive match in title and body.
Changed
- Split
lib/volumen/server.rbintoapi_routes.rbandadmin_routes.rbmodules, keeping the server class focused on configuration and shared helpers.
Fixed
admin.session_ttlwas defined but never read; it now controlsRack::Session::Cookieexpiration viaexpire_after.- Derived post excerpts now strip HTML tags before stripping markdown markup, avoiding raw HTML leaks in auto-generated excerpts.
- Draft posts no longer leak through the public detail endpoint (
/api/volumen/posts/:slug). Config::DEFAULTSinner hashes are now frozen anddeep_mergebuilds fresh copies, preventing mutation from silently poisoning laterConfig.loadcalls.- A warning is emitted when
admin.session_keyis configured but shorter than 64 bytes, instead of silently falling back to a random secret.
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
.mdfile with a TOML frontmatter block delimited by+++. No database. - Post fields:
title,slug,date,lang,author,tags,draft,excerpt,cover,all_langs, andtranslations(alang → slugmap). - 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, plusall_langsto 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), andsitemap.xml. postssupports pagination (page,limit, returningtotalandpage_size) and filtering bylangandtag.- 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 correctContent-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) andauthor(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 to9090),content_dir,users_file, andsitemetadata. - CLI:
serve,hash-password,version,help, with--config,--content,--host, and--portoverrides.
Deployment
install.shinstalls 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.
justtask set (install,run,dev,build,test,uninstall) and Forgejo Actions CI (RuboCop + tests on Ruby 3.3/3.4, gem build on release).CONTRIBUTING.mdand 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
HttpOnlyandSameSite=Strict, and gain theSecureattribute automatically on HTTPS requests (detected viarequest.ssl?/X-Forwarded-Proto), so the cookie never travels over plain HTTP in production. - Configurable, stable
session_keyso sessions survive restarts. - Generic login error message to avoid username enumeration.