chore: prepare release v0.3.0

This commit is contained in:
2026-07-12 14:03:14 +02:00
parent 1dfbf67921
commit 6f62d3e3f3
7 changed files with 144 additions and 28 deletions
+59 -6
View File
@@ -9,18 +9,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
-
## [0.3.0] — 2026-07-12
### Added
**Content & authoring**
- **Fediverse attribution:** a `fediverse_creator` frontmatter field (e.g. - **Fediverse attribution:** a `fediverse_creator` frontmatter field (e.g.
`@user@mastodon.social`) plus a site-wide `[site].fediverse_creator` default `@user@mastodon.social`) plus a site-wide `[site].fediverse_creator` default
in `config.toml`. The value is exposed as `fediverse_creator` on post and in `config.toml`. The value is exposed as `fediverse_creator` on the site
site payloads, rendered as `<dc:creator>` in the RSS feed, as `authors[].name` and post payloads, rendered as `<dc:creator>` in the RSS feed, as
in the JSON feed, and can be consumed by a front-end to emit `authors[].name` in the JSON feed, and can be consumed by a front-end to
`<meta name="fediverse:creator">` on rendered pages. Per-post values emit `<meta name="fediverse:creator">` on rendered pages. Per-post values
override the site default. override the site default.
- **Cover caption:** a `cover_caption` post 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
(`![alt](url "caption")`) are wrapped in a `<figure>` with a `<figcaption>`
and a small `i` info icon. Images without a title render exactly as before.
**Public API**
- `has_next` and `has_prev` boolean flags in the paginated - `has_next` and `has_prev` boolean flags in the paginated
`/api/volumen/posts` response so clients can navigate pages without `/api/volumen/posts` response so clients can navigate pages without
computing boundaries themselves. computing boundaries themselves.
- Distinct `"draft"` error code in `/api/volumen/posts/:slug` when the post - Distinct `"draft"` error code in `/api/volumen/posts/:slug` when the post
exists but is a draft, replacing the generic `"not_found"`. 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 to `cwebp` /
`avifenc` for 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 lint` recipe for standalone RuboCop checks and `just fmt` for - `just lint` recipe for standalone RuboCop checks and `just fmt` for
auto-fixing lint issues. auto-fixing lint issues.
- `config.toml.example` template for local development; `config.toml` is - `config.toml.example` template for local development; `config.toml` is
@@ -38,16 +80,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Extract feed and sitemap generation into a dedicated `FeedHelpers` module, - Extract feed and sitemap generation into a dedicated `FeedHelpers` module,
reducing `Server` from ~500 to ~420 lines. reducing `Server` from ~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 ### Security
- **Content-Security-Policy** header on all `/admin/*` responses: - **Content-Security-Policy** header on all `/admin/*` responses:
`default-src 'self'`, `script-src 'self' 'unsafe-inline'`, `default-src 'self'`, `script-src 'self' 'unsafe-inline'`,
`style-src 'self' 'unsafe-inline'`, `img-src 'self' data:`, `style-src 'self' 'unsafe-inline'`, `img-src 'self' data:`,
`font-src 'self'`, `connect-src 'self'`, `form-action 'self'`,
`frame-ancestors 'none'`. `frame-ancestors 'none'`.
- File uploads are rejected (HTTP 413) when exceeding **10 MB** - File uploads are rejected (HTTP 413) when exceeding **10 MB**
(`MAX_UPLOAD_BYTES`). (`MAX_UPLOAD_BYTES`).
- File uploads are rejected (HTTP 415) unless the MIME type is one of - File uploads are restricted to `image/webp` and `image/avif`
JPEG, PNG, GIF, WebP, AVIF, or SVG (`ALLOWED_UPLOAD_TYPES`). (`ALLOWED_UPLOAD_TYPES`). Anything else is rejected with a 415 and a
conversion hint.
## [0.2.0] — 2026-06-25 ## [0.2.0] — 2026-06-25
@@ -190,3 +241,5 @@ admin — no database, no external services.
- Generic login error message to avoid username enumeration. - Generic login error message to avoid username enumeration.
[0.1.0]: https://codeberg.org/petrbalvin/volumen/releases/tag/v0.1.0 [0.1.0]: https://codeberg.org/petrbalvin/volumen/releases/tag/v0.1.0
[0.2.0]: https://codeberg.org/petrbalvin/volumen/releases/tag/v0.2.0
[0.3.0]: https://codeberg.org/petrbalvin/volumen/releases/tag/v0.3.0
+9 -6
View File
@@ -142,21 +142,24 @@ volumen/
│ └── volumen/ │ └── volumen/
│ ├── version.rb │ ├── version.rb
│ ├── frontmatter.rb # parse/serialise the +++ TOML frontmatter block │ ├── frontmatter.rb # parse/serialise the +++ TOML frontmatter block
│ ├── markdown.rb # kramdown (GFM) → HTML │ ├── markdown.rb # kramdown (GFM) → HTML; <figure> for titled images
│ ├── post.rb # Post model (metadata + body + rendered HTML) │ ├── post.rb # Post model (metadata + body + rendered HTML)
│ ├── store.rb # read/write posts and media on disk │ ├── store.rb # read/write posts and media on disk (mtime-cached)
│ ├── config.rb # load/merge config.toml, generate template │ ├── config.rb # load/merge config.toml, generate template
│ ├── password.rb # scrypt hashing/verification (OpenSSL::KDF) │ ├── password.rb # scrypt hashing/verification (OpenSSL::KDF)
│ ├── users.rb # users.toml, admin/author roles │ ├── users.rb # users.toml, admin/author roles, per-user profile
│ ├── server.rb # Sinatra app: public API + admin │ ├── feed_helpers.rb # RSS / JSON Feed / sitemap generation
│ ├── api_routes.rb # /api/volumen/* (Sinatra routes)
│ ├── admin_routes.rb # /admin/* (Sinatra routes)
│ ├── server.rb # Sinatra app: composes the two route modules
│ ├── cli.rb # command dispatcher │ ├── cli.rb # command dispatcher
│ └── web/ │ └── web/
│ ├── views/ # ERB templates (layout, login, list, form, settings) │ ├── views/ # ERB templates (layout, login, list, form, settings)
│ └── public/ # static assets (volumen-logo.svg) │ └── public/ # static assets (volumen-logo.svg, volumen-icon.svg)
├── test/ # minitest suite ├── test/ # minitest suite
├── docs/ # architecture, configuration, api-reference, deployment, security ├── docs/ # architecture, configuration, api-reference, deployment, security
├── .forgejo/workflows/ # test.yml, release.yml (Forgejo Actions) ├── .forgejo/workflows/ # test.yml, release.yml (Forgejo Actions)
├── justfile # install, run, dev, build, test, uninstall ├── justfile # install, run, dev, lint, fmt, build, test, uninstall
├── volumen.gemspec ├── volumen.gemspec
├── Gemfile ├── Gemfile
├── CHANGELOG.md ├── CHANGELOG.md
+2 -2
View File
@@ -1,7 +1,7 @@
PATH PATH
remote: . remote: .
specs: specs:
volumen (0.2.0) volumen (0.3.0)
kramdown (~> 2.5) kramdown (~> 2.5)
kramdown-parser-gfm (~> 1.1) kramdown-parser-gfm (~> 1.1)
puma (~> 8.0) puma (~> 8.0)
@@ -130,7 +130,7 @@ CHECKSUMS
toml-rb (4.2.0) sha256=10a48c91613e20cf63483a7a776767dfb3cd7d70e9327c0237443da601e13776 toml-rb (4.2.0) sha256=10a48c91613e20cf63483a7a776767dfb3cd7d70e9327c0237443da601e13776
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
volumen (0.2.0) volumen (0.3.0)
BUNDLED WITH BUNDLED WITH
4.0.10 4.0.10
+20
View File
@@ -114,6 +114,26 @@ front-end can offer a language switcher.
Set `all_langs = true` on a post to show it in **every** language (useful for Set `all_langs = true` on a post to show it in **every** language (useful for
posts that have no per-language translations). posts that have no per-language translations).
### Cover caption
A `cover_caption` frontmatter field renders a short caption next to the cover
image — useful for photo credits or a one-line blurb:
```toml
cover = "media/cover.webp"
cover_caption = "Photo by Petr Balvín"
```
### Titled images as figures
A Markdown image with a **title** (`![alt](url "caption")`) is rendered as a
`<figure>` with a `<figcaption>` and a small `i` info icon. Images without a
title render exactly as before:
```markdown
![A sunset over the hills](media/sunset.webp "Sunset from the ridge above the village")
```
### Fediverse attribution ### Fediverse attribution
Set `fediverse_creator = "@user@instance.tld"` on a post to attach a Mastodon Set `fediverse_creator = "@user@instance.tld"` on a post to attach a Mastodon
+21 -10
View File
@@ -20,7 +20,7 @@ server-rendered admin for editing them.
- **Markdown-first** — the visual editor round-trips through the same renderer - **Markdown-first** — the visual editor round-trips through the same renderer
that powers the public API, so stored content is always Markdown. that powers the public API, so stored content is always Markdown.
## Components (planned layout) ## Components
``` ```
lib/volumen/ lib/volumen/
@@ -28,27 +28,38 @@ lib/volumen/
config.rb # loads /etc/volumen/config.toml, applies CLI overrides config.rb # loads /etc/volumen/config.toml, applies CLI overrides
frontmatter.rb # parse/serialise the `+++ … +++` TOML block frontmatter.rb # parse/serialise the `+++ … +++` TOML block
post.rb # Post model: metadata + raw body + rendered HTML post.rb # Post model: metadata + raw body + rendered HTML
store.rb # reads the content directory into Post objects store.rb # reads the content directory into Post objects (mtime-cached)
markdown.rb # kramdown wrapper (render + sanitise) markdown.rb # kramdown wrapper (render + <figure>/<figcaption> for titled images)
server.rb # Sinatra app: JSON API + admin routes feed_helpers.rb # RSS / JSON Feed / sitemap generation
users.rb # users.toml + admin/author roles
password.rb # scrypt hashing/verification (OpenSSL::KDF)
api_routes.rb # /api/volumen/* (Sinatra routes)
admin_routes.rb # /admin/* (Sinatra routes)
server.rb # Sinatra app: composes the two route modules, shared helpers
cli.rb # command dispatcher
web/ web/
views/ # ERB templates for the admin views/ # ERB templates for the admin
public/ # admin CSS + the visual-editor JS island public/ # static assets (volumen-logo.svg, volumen-icon.svg)
exe/volumen # CLI: serve, hash-password, version, help exe/volumen # CLI: serve, hash-password, version, help
``` ```
## Request flow (planned) ## Request flow
```mermaid ```mermaid
graph TD graph TD
A[HTTP request] --> B{Route} A[HTTP request] --> B{Route}
B -->|/api/volumen/*| C[JSON API] B -->|/api/volumen/*| C[api_routes.rb]
B -->|/admin/*| D[Admin: session + CSRF] B -->|/admin/*| D[admin_routes.rb + CSRF + CSP]
C --> E[Store] C --> E[Store]
D --> E D --> E
E --> F[(content_dir/*.md)] D --> U[Users]
C --> F[FeedHelpers]
C --> G[Markdown renderer] C --> G[Markdown renderer]
D --> G E --> H[(content_dir/*.md)]
E --> I[(content_dir/media/*)]
U --> J[(users.toml)]
F --> E
G --> K[<figure> for titled images]
``` ```
## Public site integration ## Public site integration
+32 -3
View File
@@ -51,6 +51,26 @@ than opening a public issue.
Requests without a valid token get `403`. Requests without a valid token get `403`.
- `SameSite=Strict` cookies provide a second, independent layer. - `SameSite=Strict` cookies provide a second, independent layer.
## Content Security Policy (admin)
All `/admin/*` responses send a strict **Content-Security-Policy** header:
```
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'
```
The inline allowances are required by the server-rendered admin (ERB templates
emit small inline `<script>` blocks and inline `style=""` attributes);
`frame-ancestors 'none'` prevents clickjacking via iframe embedding. The public
JSON API does not set a CSP — it returns no HTML, so none is needed.
## CORS and Host handling ## CORS and Host handling
- The public read API sends `Access-Control-Allow-Origin: *` (read-only, no - The public read API sends `Access-Control-Allow-Origin: *` (read-only, no
@@ -68,8 +88,15 @@ than opening a public issue.
- `GET /media/*` resolves names with `File.basename` and an explicit - `GET /media/*` resolves names with `File.basename` and an explicit
containment check, so `../` traversal cannot read files outside the media containment check, so `../` traversal cannot read files outside the media
directory. directory.
- Upload content-type is **not** validated (the `accept="image/*"` attribute is - Uploads are **rejected (HTTP 413)** when the file exceeds **10 MB**
only a UI hint); this is acceptable under the trusted-author model. (`MAX_UPLOAD_BYTES`).
- Upload MIME type is **whitelisted** to `image/webp` and `image/avif`
(`ALLOWED_UPLOAD_TYPES`); anything else returns **HTTP 415** with a
conversion hint pointing at `cwebp` / `avifenc`. This matches the
WebP/AVIF-only posture the engine serves content in and rejects binaries
(PHP, executable, …) outright.
- Per-user profile photos go through the same upload validation and storage
pipeline as post media.
## Secrets and files ## Secrets and files
@@ -91,7 +118,9 @@ A small, audited set: `sinatra`, `kramdown` (+ `kramdown-parser-gfm`),
## Known limitations / non-goals ## Known limitations / non-goals
- **No rate limiting or lockout** on `/admin/login`. Put nginx `limit_req` in - **No rate limiting or lockout** on `/admin/login` at the application layer
(the engine does have an in-memory rate limiter, but nginx is the durable
line of defence — see `docs/deployment.md`). Put nginx `limit_req` in
front of it, or use fail2ban, to slow brute-force attempts. front of it, or use fail2ban, to slow brute-force attempts.
- **No 2FA** and **no audit log**. - **No 2FA** and **no audit log**.
- **Raw HTML in posts is trusted** (see the trust model). There is no built-in - **Raw HTML in posts is trusted** (see the trust model). There is no built-in
+1 -1
View File
@@ -1,5 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
module Volumen module Volumen
VERSION = "0.2.0" VERSION = "0.3.0"
end end