chore: prepare release v0.4.0
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# volumen — file-based Markdown blog engine
|
||||
|
||||
**volumen** is a small, dependency-light blog engine written in **Ruby**. It
|
||||
**volumen** is a small, dependency-light blog engine written in **Python**. It
|
||||
serves your Markdown posts as a JSON API and includes a built-in, server-
|
||||
rendered admin with both a Markdown source editor and a visual editor — so any
|
||||
front-end (Vue, React, Svelte, plain HTML) can consume your content without
|
||||
@@ -10,26 +10,33 @@ It is designed to be:
|
||||
|
||||
- **File-based** — every post is a plain `.md` file with TOML frontmatter, safe
|
||||
to commit to Git and edit in your favourite editor.
|
||||
- **Self-contained** — runs as a single Ruby process; no Node build step is
|
||||
required to operate the engine or its admin.
|
||||
- **Self-contained** — runs as a single Python process (Uvicorn); no Node
|
||||
build step is required to operate the engine or its admin.
|
||||
- **Multi-user with roles** — username + password login; **admin** (full
|
||||
access, manages users) and **author** (posts and own account) roles.
|
||||
- **Multi-site friendly** — one instance per blog, configurable per instance.
|
||||
- **Markdown-first** — the visual editor round-trips through the same renderer
|
||||
that powers the public API, so what you store is always Markdown.
|
||||
- **IPv6-first** — the default bind address is `::` (IPv6 with automatic IPv4
|
||||
fallback); bind to `::1` when running behind nginx so the admin port is not
|
||||
exposed to the network.
|
||||
|
||||
---
|
||||
|
||||
## Stack
|
||||
|
||||
| Concern | Choice |
|
||||
|----------------|------------------------------------------|
|
||||
| Language | CRuby (≥ 3.2) |
|
||||
| Web framework | [Sinatra](https://sinatrarb.com) |
|
||||
| Markdown | [kramdown](https://kramdown.gettalong.org) |
|
||||
| Frontmatter | TOML via [`toml-rb`](https://github.com/emancu/toml-rb) |
|
||||
| App server | [Puma](https://puma.io) |
|
||||
| Packaging | a Ruby gem with an `exe/volumen` CLI |
|
||||
| Concern | Choice |
|
||||
|----------------|------------------------------------------------------------------------|
|
||||
| Language | Python 3.14+ |
|
||||
| Web framework | [FastAPI](https://fastapi.tiangolo.com/) |
|
||||
| App server | [Uvicorn](https://www.uvicorn.org/) (via `fastapi[standard]`) |
|
||||
| Markdown | [python-markdown](https://python-markdown.github.io/) + [pymdown-extensions](https://facelessuser.github.io/pymdown-extensions/) |
|
||||
| Frontmatter | TOML via stdlib [`tomllib`](https://docs.python.org/3/library/tomllib.html) + [`tomli-w`](https://github.com/camillescott/tomli-w) |
|
||||
| Templating | [Jinja2](https://jinja.palletsprojects.com/) |
|
||||
| Sessions | Starlette `SessionMiddleware` (signed cookies via [`itsdangerous`](https://itsdangerous.palletsprojects.com/)) |
|
||||
| Packaging | Wheel + sdist via [`setuptools`](https://setuptools.pypa.io/); [`uv`](https://docs.astral.sh/uv/) for dependency management |
|
||||
| Lint / format | [Ruff](https://docs.astral.sh/ruff/) |
|
||||
| Tests | [pytest](https://docs.pytest.org/) + [httpx](https://www.python-httpx.org/) |
|
||||
|
||||
---
|
||||
|
||||
@@ -40,19 +47,22 @@ It is designed to be:
|
||||
- Multi-language posts with a translations map
|
||||
- Clean JSON API at `/api/volumen/`:
|
||||
- `GET /site` — site metadata
|
||||
- `GET /posts` — paginated list with `lang`, `tag`, `page`, `limit` filters
|
||||
- `GET /posts` — paginated list with `lang`, `tag`, `q`, `page`, `limit`
|
||||
filters
|
||||
- `GET /posts/{slug}` — single post (raw Markdown + rendered HTML)
|
||||
- `GET /tags` — tag cloud with counts
|
||||
- `GET /feed.xml` — RSS 2.0 feed
|
||||
- `GET /feed.json` — JSON Feed 1.1
|
||||
- `GET /sitemap.xml` — sitemap
|
||||
- Server-rendered admin at `/admin/`:
|
||||
- Username + password login (scrypt-hashed via Ruby's `OpenSSL::KDF`)
|
||||
- Username + password login (scrypt-hashed via `hashlib.scrypt`,
|
||||
verified in constant time with `secrets.compare_digest`)
|
||||
- Settings page: change password, change username, manage users and roles
|
||||
(admin only)
|
||||
- Session cookies (HttpOnly, SameSite=Strict) + CSRF tokens
|
||||
- List, create, edit, delete posts
|
||||
- **Markdown source** editor and a **visual** editor with live preview, both
|
||||
storing Markdown
|
||||
- **Markdown source** editor and a **visual** editor with live preview,
|
||||
both storing Markdown
|
||||
- "Reload from disk" to pick up out-of-band edits
|
||||
- Permissive CORS for the public API, same-origin only for the admin
|
||||
|
||||
@@ -64,16 +74,44 @@ It is designed to be:
|
||||
|
||||
## Quick start
|
||||
|
||||
### Production / first install
|
||||
|
||||
```bash
|
||||
# 1. Install dependencies (Ruby ≥ 3.2)
|
||||
# 1. Install the volumen CLI (once, on the machine):
|
||||
uv tool install volumen # or: pipx install volumen
|
||||
|
||||
# 2. Bootstrap (once, typically as root — generates config, prompts for
|
||||
# an admin password, and installs a hardened systemd unit):
|
||||
sudo volumen init
|
||||
sudo systemctl status volumen
|
||||
|
||||
# 3. Reverse-proxy with nginx and you're done — see docs/deployment.md.
|
||||
```
|
||||
|
||||
`uv tool install` (or `pipx install`) drops a self-contained `volumen`
|
||||
executable on `PATH`; `volumen init` is the operational bootstrap that
|
||||
renders the config, creates the data dirs, generates a session key, and
|
||||
(with `--systemd`, on by default under root) installs a hardened unit
|
||||
file. See `volumen init --help` and `docs/deployment.md` for the full
|
||||
flow.
|
||||
|
||||
### Development (this checkout)
|
||||
|
||||
```bash
|
||||
# 1. Install dependencies (Python 3.14+ and uv required)
|
||||
just install
|
||||
|
||||
# 2. Run
|
||||
just run
|
||||
```
|
||||
|
||||
`just install` runs `uv sync` (creates `.venv/` and installs the locked
|
||||
dependency set from `uv.lock`). `just run` launches the `volumen` console
|
||||
script from the venv with the bundled sample posts.
|
||||
|
||||
The admin will live at <http://localhost:9090/admin/> and the API at
|
||||
<http://localhost:9090/api/volumen/posts>.
|
||||
<http://localhost:9090/api/volumen/posts>. The dev admin password is `admin`
|
||||
(the hash lives in `./config.toml`).
|
||||
|
||||
---
|
||||
|
||||
@@ -89,7 +127,7 @@ date = 2026-01-15 # first-class TOML date
|
||||
lang = "en" # language code
|
||||
author = "Petr Balvín" # optional
|
||||
fediverse_creator = "@petrbalvin@mastodon.social" # optional, for Mastodon attribution
|
||||
tags = ["ruby", "web"] # optional
|
||||
tags = ["python", "web"] # optional
|
||||
draft = false # optional
|
||||
excerpt = "Short summary" # optional, auto-derived from body if missing
|
||||
all_langs = false # optional, show this post in every language
|
||||
@@ -100,7 +138,7 @@ cs = "muj-prispevek"
|
||||
|
||||
# Heading
|
||||
|
||||
Body in **Markdown**, rendered by kramdown.
|
||||
Body in **Markdown**, rendered by python-markdown.
|
||||
```
|
||||
|
||||
Posts are organised on disk as either:
|
||||
@@ -156,20 +194,28 @@ per-post value takes precedence.
|
||||
becoming `false`).
|
||||
- **No whitespace pitfalls** — indentation is not significant.
|
||||
|
||||
The engine parses it with the Python 3.11+ standard library `tomllib`; no
|
||||
extra TOML dependency is needed at runtime.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
A single TOML file, auto-generated at `/etc/volumen/config.toml` on first
|
||||
start. See [`docs/configuration.md`](docs/configuration.md) for the full
|
||||
A single TOML file, auto-generated at `/etc/volumen/config.toml` when you
|
||||
run `volumen init` (or, for local development, by `volumen serve` on first
|
||||
start). See [`docs/configuration.md`](docs/configuration.md) for the full
|
||||
schema.
|
||||
|
||||
```toml
|
||||
[server]
|
||||
host = "0.0.0.0"
|
||||
host = "::" # IPv6 + IPv4 fallback; use "::1" behind nginx
|
||||
port = 9090
|
||||
env = "production"
|
||||
trust_proxy = true
|
||||
cookie_secure = true # required in production behind HTTPS
|
||||
|
||||
content_dir = "/var/lib/volumen/posts"
|
||||
users_file = "/var/lib/volumen/users.toml"
|
||||
|
||||
[site]
|
||||
title = "My Blog"
|
||||
@@ -179,17 +225,26 @@ language = "en"
|
||||
author = "Anonymous"
|
||||
|
||||
[admin]
|
||||
password_hash = ""
|
||||
session_key = ""
|
||||
password_hash = "" # populated by `volumen init`
|
||||
session_key = "" # populated by `volumen init`
|
||||
session_ttl = 86400
|
||||
min_password_length = 10
|
||||
cookie_secure = false # set to true in production behind HTTPS
|
||||
```
|
||||
|
||||
`volumen init` writes the bootstrap admin hash and a fresh session key for
|
||||
you. To rotate the admin password later:
|
||||
|
||||
```bash
|
||||
sudo volumen hash-password # returns a scrypt$… string; paste into [admin].password_hash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Public API
|
||||
|
||||
The API is at `/api/volumen/`. CORS is open (`*`) for read endpoints; admin routes
|
||||
are same-origin only.
|
||||
The API is at `/api/volumen/`. CORS is open (`*`) for read endpoints; admin
|
||||
routes are same-origin only.
|
||||
|
||||
### `GET /api/volumen/posts`
|
||||
|
||||
@@ -197,6 +252,7 @@ are same-origin only.
|
||||
|-------|--------|---------|----------------------|
|
||||
| lang | string | — | Filter by language |
|
||||
| tag | string | — | Filter by tag |
|
||||
| q | string | — | Search in title/body |
|
||||
| page | int | 1 | Page number |
|
||||
| limit | int | 20 | Posts per page |
|
||||
|
||||
@@ -205,6 +261,8 @@ are same-origin only.
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"total": 42,
|
||||
"has_next": true,
|
||||
"has_prev": false,
|
||||
"posts": [
|
||||
{
|
||||
"slug": "welcome",
|
||||
@@ -212,7 +270,7 @@ are same-origin only.
|
||||
"excerpt": "A short introduction…",
|
||||
"date": "2026-01-15",
|
||||
"lang": "en",
|
||||
"tags": ["intro", "ruby"],
|
||||
"tags": ["intro", "python"],
|
||||
"author": "Petr Balvín",
|
||||
"translations": { "cs": "vitame-vas" },
|
||||
"url": "/api/volumen/posts/welcome"
|
||||
@@ -231,7 +289,7 @@ Returns the full post including the raw Markdown body and the rendered HTML.
|
||||
"title": "Welcome to volumen",
|
||||
"date": "2026-01-15",
|
||||
"lang": "en",
|
||||
"tags": ["intro", "ruby"],
|
||||
"tags": ["intro", "python"],
|
||||
"excerpt": "…",
|
||||
"body": "# Welcome to **volumen**\n\n…",
|
||||
"html": "<h1>Welcome to <strong>volumen</strong></h1>…",
|
||||
@@ -245,13 +303,22 @@ Returns the full post including the raw Markdown body and the rendered HTML.
|
||||
|
||||
```bash
|
||||
just # list recipes
|
||||
just install # bundle install
|
||||
just run # run the dev server (exe/volumen serve)
|
||||
just build # rubocop (zero warnings) + build the gem → pkg/volumen.gem
|
||||
just test # bundle exec rake test (minitest)
|
||||
just uninstall # remove build artifacts (pkg/, *.gem)
|
||||
just install # uv sync (creates .venv/ from uv.lock)
|
||||
just run # run the dev server (volumen serve)
|
||||
just dev # run against ./posts and ./config.toml on :9090
|
||||
just build # ruff check + ruff format --check (zero warnings)
|
||||
just test # uv run pytest
|
||||
just fmt # ruff format + ruff check --fix
|
||||
just uninstall # remove build artifacts (dist/, __pycache__, .pytest_cache, .ruff_cache)
|
||||
```
|
||||
|
||||
The console script lives at `src/volumen/cli.py` and is registered as
|
||||
`volumen` via the `[project.scripts]` entry in `pyproject.toml`; `uv run`
|
||||
shims it onto `PATH` inside `.venv/`. In production the CLI is installed
|
||||
globally via `uv tool install volumen` (or `pipx install volumen`) and the
|
||||
operational bootstrap is done with `sudo volumen init` —
|
||||
see [`docs/deployment.md`](docs/deployment.md).
|
||||
|
||||
---
|
||||
|
||||
## Public site integration
|
||||
@@ -280,4 +347,4 @@ commit conventions, the pull request flow, and how releases are cut.
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
Copyright © 2026 [Petr Balvín](https://petrbalvin.org)
|
||||
Copyright © 2026 [Petr Balvín](https://petrbalvin.org)
|
||||
Reference in New Issue
Block a user