Files
volumen/CHANGELOG.md
T
petrbalvin d1295c6633 docs: document all development changes in changelog
Add entries for pagination flags, draft error code, just lint/fmt,
config.toml.example, published_posts memoization, Store mtime
invalidation, Cache-Control, FeedHelpers extraction, CSP header,
upload size limit, and MIME whitelist.
2026-06-26 20:49:48 +02:00

8.5 KiB
Raw Blame History

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

  • 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.
  • 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).

[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_attempts entries to prevent unbounded memory growth.
  • 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.
  • 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.

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.

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.
  • 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::DEFAULTS inner hashes are now frozen and deep_merge builds fresh copies, preventing mutation from silently poisoning later Config.load calls.
  • A warning is emitted when admin.session_key is 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 .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.