diff --git a/CHANGELOG.md b/CHANGELOG.md index 25c4d79..b8cee1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `` in the RSS feed, as `authors[].name` - in the JSON feed, and can be consumed by a front-end to emit - `` on rendered pages. Per-post values + in `config.toml`. The value is exposed as `fediverse_creator` on the site + and post payloads, rendered as `` in the RSS feed, as + `authors[].name` in the JSON feed, and can be consumed by a front-end to + emit `` 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 `
`:** Markdown images with a title + (`![alt](url "caption")`) are wrapped in a `
` with a `
` + 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 `` 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cff11f5..7391096 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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;
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 diff --git a/Gemfile.lock b/Gemfile.lock index aace83e..1f51b09 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/README.md b/README.md index b5684c0..015dcd8 100644 --- a/README.md +++ b/README.md @@ -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 +`
` with a `
` 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 diff --git a/docs/architecture.md b/docs/architecture.md index d18b1bf..75c3849 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 +
/
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[
for titled images] ``` ## Public site integration diff --git a/docs/security.md b/docs/security.md index ffb081d..f57509b 100644 --- a/docs/security.md +++ b/docs/security.md @@ -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 `