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
|
||||
|
||||
-
|
||||
|
||||
## [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
|
||||
(``) 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
@@ -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
@@ -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
|
||||
|
||||
@@ -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** (``) 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
|
||||
|
||||
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
|
||||
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
@@ -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,5 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Volumen
|
||||
VERSION = "0.2.0"
|
||||
VERSION = "0.3.0"
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user