chore: prepare release v0.3.0
This commit is contained in:
+59
-6
@@ -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
|
||||||
|
(``) 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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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** (``) is rendered as a
|
||||||
|
`<figure>` with a `<figcaption>` and a small `i` info icon. Images without a
|
||||||
|
title render exactly as before:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
### 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
@@ -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
@@ -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,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
|
||||||
|
|||||||
Reference in New Issue
Block a user