chore: prepare release v0.4.0
This commit is contained in:
+50
-34
@@ -3,7 +3,7 @@
|
||||
> Status: the core library, the public JSON API, and the server-rendered admin
|
||||
> (login, CSRF, post CRUD, Markdown editor + live preview) are implemented.
|
||||
|
||||
**volumen** is a small, file-based Markdown blog engine written in CRuby. It
|
||||
**volumen** is a small, file-based Markdown blog engine written in Python. It
|
||||
has no database: every post is a `.md` file on disk with a TOML frontmatter
|
||||
block. The engine reads those files, exposes them as a JSON API, and ships a
|
||||
server-rendered admin for editing them.
|
||||
@@ -12,35 +12,35 @@ server-rendered admin for editing them.
|
||||
|
||||
- **File-based** — posts are plain `.md` files, safe to commit to Git and edit
|
||||
by hand or through the admin.
|
||||
- **Single-admin** — one password, one content directory, one user.
|
||||
- **Self-contained** — runs as a single Ruby process; no Node build step is
|
||||
required to operate the engine or its admin.
|
||||
- **Dependency-light** — a small, well-chosen set of gems (Sinatra, kramdown,
|
||||
toml-rb, puma) rather than a full framework.
|
||||
- **Self-contained** — runs as a single Python process (Uvicorn) under FastAPI;
|
||||
no Node build step is required to operate the engine or its admin.
|
||||
- **Dependency-light** — a small, well-chosen set of PyPI packages (FastAPI,
|
||||
python-markdown, Jinja2, itsdangerous, python-multipart) rather than a
|
||||
full-stack framework. The standard library covers cryptography.
|
||||
- **Markdown-first** — the visual editor round-trips through the same renderer
|
||||
that powers the public API, so stored content is always Markdown.
|
||||
|
||||
## Components
|
||||
|
||||
```
|
||||
lib/volumen/
|
||||
version.rb # gem version
|
||||
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 (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
|
||||
src/volumen/
|
||||
__init__.py # VERSION + in-memory login-attempt rate limiter
|
||||
cli.py # argparse entry point: serve, hash-password, version
|
||||
config.py # loads config.toml via stdlib tomllib, deep-merges defaults
|
||||
frontmatter.py # parse/serialise the `+++ … +++` TOML block
|
||||
post.py # Post model: metadata + raw body + rendered HTML
|
||||
store.py # reads the content directory into Post objects (mtime-cached)
|
||||
markdown.py # python-markdown + pymdown-extensions wrapper, <figure>/<figcaption>
|
||||
feed_helpers.py # RSS / JSON Feed / sitemap generation
|
||||
users.py # users.toml + admin/author roles
|
||||
password.py # scrypt hashing/verification (hashlib + secrets.compare_digest)
|
||||
api_routes.py # /api/volumen/* (FastAPI APIRouter)
|
||||
admin_routes.py # /admin/* + /media/* (FastAPI APIRouter)
|
||||
server.py # create_app factory, middleware, dependency helpers
|
||||
web/
|
||||
views/ # ERB templates for the admin
|
||||
public/ # static assets (volumen-logo.svg, volumen-icon.svg)
|
||||
exe/volumen # CLI: serve, hash-password, version, help
|
||||
templates/ # Jinja2 templates for the admin
|
||||
static/ # static assets (volumen-logo.svg, volumen-icon.svg)
|
||||
exe/volumen # thin shim that calls volumen.cli:main
|
||||
```
|
||||
|
||||
## Request flow
|
||||
@@ -48,22 +48,38 @@ exe/volumen # CLI: serve, hash-password, version, help
|
||||
```mermaid
|
||||
graph TD
|
||||
A[HTTP request] --> B{Route}
|
||||
B -->|/api/volumen/*| C[api_routes.rb]
|
||||
B -->|/admin/*| D[admin_routes.rb + CSRF + CSP]
|
||||
C --> E[Store]
|
||||
D --> E
|
||||
B -->|/api/volumen/*| C[api_routes.py]
|
||||
B -->|/admin/*| D[admin_routes.py + CSRF + CSP]
|
||||
B -->|/media/*| E[admin_routes.py media_router]
|
||||
C --> F[Store]
|
||||
D --> F
|
||||
D --> U[Users]
|
||||
C --> F[FeedHelpers]
|
||||
C --> H[FeedHelpers]
|
||||
C --> G[Markdown renderer]
|
||||
E --> H[(content_dir/*.md)]
|
||||
E --> I[(content_dir/media/*)]
|
||||
U --> J[(users.toml)]
|
||||
F --> E
|
||||
G --> K[<figure> for titled images]
|
||||
F --> I[(content_dir/*.md)]
|
||||
F --> J[(content_dir/media/*)]
|
||||
U --> K[(users.toml)]
|
||||
H --> F
|
||||
G --> L[<figure> for titled images]
|
||||
D --> M[SessionMiddleware + CSPMiddleware]
|
||||
C --> M
|
||||
E --> M
|
||||
```
|
||||
|
||||
## Middleware stack
|
||||
|
||||
- `SessionMiddleware` (Starlette) — signed cookie session, signed with
|
||||
`itsdangerous` using the secret derived from `[admin].session_key`. Set to
|
||||
`HttpOnly` and `SameSite=Strict`; `Secure` is added by `SecureCookieMiddleware`
|
||||
when the request was forwarded over HTTPS.
|
||||
- `SecureCookieMiddleware` (custom) — inspects `X-Forwarded-Proto` (when
|
||||
`[server].trust_proxy = true`) and exposes `request.state.session_secure`
|
||||
for downstream code that needs to know whether the request is HTTPS.
|
||||
- `CSPMiddleware` (custom) — injects the `Content-Security-Policy` header on
|
||||
every `/admin/*` response.
|
||||
|
||||
## Public site integration
|
||||
|
||||
The public website (e.g. `petrbalvin.org`, a Vue 3 + Vite + Tailwind SPA)
|
||||
consumes volumen's JSON API. volumen does not render the public site itself; it
|
||||
is headless for the front-end and only renders its own admin.
|
||||
is headless for the front-end and only renders its own admin.
|
||||
Reference in New Issue
Block a user