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
-
## [0.3.0] — 2026-07-12
### Added
**Content & authoring**
- **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
in `config.toml`. The value is exposed as `fediverse_creator` on the site
and post 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.
- **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
`/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"`.
**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
auto-fixing lint issues.
- `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,
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
- **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 rejected (HTTP 415) unless the MIME type is one of
JPEG, PNG, GIF, WebP, AVIF, or SVG (`ALLOWED_UPLOAD_TYPES`).
- File uploads are restricted to `image/webp` and `image/avif`
(`ALLOWED_UPLOAD_TYPES`). Anything else is rejected with a 415 and a
conversion hint.
## [0.2.0] — 2026-06-25
@@ -190,3 +241,5 @@ admin — no database, no external services.
- Generic login error message to avoid username enumeration.
[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/
│ ├── version.rb
│ ├── 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)
│ ├── 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
│ ├── password.rb # scrypt hashing/verification (OpenSSL::KDF)
│ ├── users.rb # users.toml, admin/author roles
│ ├── server.rb # Sinatra app: public API + admin
│ ├── users.rb # users.toml, admin/author roles, per-user profile
│ ├── 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
│ └── web/
│ ├── 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
├── docs/ # architecture, configuration, api-reference, deployment, security
├── .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
├── Gemfile
├── CHANGELOG.md
+2 -2
View File
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
volumen (0.2.0)
volumen (0.3.0)
kramdown (~> 2.5)
kramdown-parser-gfm (~> 1.1)
puma (~> 8.0)
@@ -130,7 +130,7 @@ CHECKSUMS
toml-rb (4.2.0) sha256=10a48c91613e20cf63483a7a776767dfb3cd7d70e9327c0237443da601e13776
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
volumen (0.2.0)
volumen (0.3.0)
BUNDLED WITH
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
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
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
that powers the public API, so stored content is always Markdown.
## Components (planned layout)
## Components
```
lib/volumen/
@@ -28,27 +28,38 @@ lib/volumen/
config.rb # loads /etc/volumen/config.toml, applies CLI overrides
frontmatter.rb # parse/serialise the `+++ … +++` TOML block
post.rb # Post model: metadata + raw body + rendered HTML
store.rb # reads the content directory into Post objects
markdown.rb # kramdown wrapper (render + sanitise)
server.rb # Sinatra app: JSON API + admin routes
store.rb # reads the content directory into Post objects (mtime-cached)
markdown.rb # kramdown wrapper (render + <figure>/<figcaption> for titled images)
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/
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
```
## Request flow (planned)
## Request flow
```mermaid
graph TD
A[HTTP request] --> B{Route}
B -->|/api/volumen/*| C[JSON API]
B -->|/admin/*| D[Admin: session + CSRF]
B -->|/api/volumen/*| C[api_routes.rb]
B -->|/admin/*| D[admin_routes.rb + CSRF + CSP]
C --> E[Store]
D --> E
E --> F[(content_dir/*.md)]
D --> U[Users]
C --> F[FeedHelpers]
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
+32 -3
View File
@@ -51,6 +51,26 @@ than opening a public issue.
Requests without a valid token get `403`.
- `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
- 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
containment check, so `../` traversal cannot read files outside the media
directory.
- Upload content-type is **not** validated (the `accept="image/*"` attribute is
only a UI hint); this is acceptable under the trusted-author model.
- Uploads are **rejected (HTTP 413)** when the file exceeds **10 MB**
(`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
@@ -91,7 +118,9 @@ A small, audited set: `sinatra`, `kramdown` (+ `kramdown-parser-gfm`),
## 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.
- **No 2FA** and **no audit log**.
- **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
module Volumen
VERSION = "0.2.0"
VERSION = "0.3.0"
end