Auto-generated from the live docs registry. Give this file to an agent to learn the API.
Molodetz is a quiet blog roll. Notes about the work, finishing it, and judgement. This documentation describes how the site works, how to use the API and how admin keeps the whole thing running.
Read Writing on the roll for the content, and API and authentication for integrations.
Posts are markdown and live in SQLite. Only administrators write. A post has a title, a topic (roll or standard), a status (draft or published) and an optional placeholder marker.
The server renders GFM: tables, strikethrough, hard line breaks. HTML in the text is escaped. Links may only be http, https, mailto and tel. Write emoji as :shortcode:, for example :fire:.
A bare image URL becomes an image, @name becomes a link to the profile.
The first notes were carried over from the old Molodetz page and carry the Placeholder label. retoor replaces them with his own stories. Untick the box while editing once a post is real.
A slug is the title plus the last twelve characters of the uid. Editing does not change the slug. If a title is ever rewritten in the database, the old slug still resolves through its uid tail and redirects with 301 to the canonical one.
The form at /join stores a join request: name, contact, an optional link to your work and a message. Administrators get a notification and see the request under Admin, Join requests.
There is no automatic access. retoor reads every request and gets in touch himself.
Molodetz has no ads, no cross-app tracking, no payments and no social login.
Every route has four faces: HTML, JSON, documentation and (where selected) an assistant tool. Ask for JSON with Accept: application/json.
Resolution order:
session cookie (64 hex characters) after logging in at /auth/login.X-API-KEY header with your API key.Authorization: Bearer <key>.Authorization: Basic with name or email and password.There is no JWT and no OAuth.
curl -H 'Accept: application/json' https://molodetz.nl/roll
Errors have the shape {"error": {"status": 404, "message": "..."}}. Validation errors return 422 with {"error": "validation", "fields": [...], "messages": [...]}.
Membership starts from a join request plus an admin-issued invite. Admins call POST /admin/joins/{uid}/invite (returns claim_url and expires_at in data) and POST /admin/joins/{uid}/invite/revoke. The public claim is GET and POST /invite/{token} with username, email, password, password_confirm and terms. Claim links are single use and every dead link answers 404 with the same message.
GET /admin/gallery reports live flyer and meme counts plus catalogued source files missing from disk. POST /admin/gallery/resync re-reads the sources and retires removed entries. See Galleries and content for how publishing works.
The Dutch paths from before (/rol, /standaard, /mensen, /binnen, /voorwaarden) answer with a 301 to their English replacement. Query strings are kept.
The flyers and the memes are curated galleries. Every work is a source file plus a catalogue entry with its caption. Nothing is uploaded through the site: new works arrive with a deploy, and the database reconciles at boot.
Flyers live at /flyers. New flyers are 1080 by 1350 (portrait). The first flyer in the catalogue is the featured one on the home page.
Memes live at /memes. New memes are 1080 by 1080 (square). Some older memes are 1280 by 720 (landscape) from the previous generation; they stay as they are.
An administrator can also press Resync under Admin, Gallery after a deploy. Resync re-reads the sources, refreshes thumbnails whose content changed, reports missing source files and retires removed entries. It never invents catalogue entries.
Public pages are cached for seconds to minutes (landing, settings, sitemap each have their own short TTL), so a fresh deploy can take a moment to show everywhere. The exact TTLs are operator detail; see the operator runbook under Admin.
Old Dutch paths (/rol, /mensen, and friends) redirect with 301 to their English replacements; see API and authentication.
Public read routes: the roll, the standard, posts, galleries and people.
GET /Home · auth public (guest) · negotiable
Latest posts and the featured flyer.
curl -X GET -H 'Accept: application/json' 'https://example.com/'
{
"flyer_count": 4,
"meme_count": 22,
"posts": []
}GET /rollThe roll · auth public (guest) · negotiable
Published notes in the roll topic, newest first, cursor paginated.
| Name | Type | Location | Required | Example |
|---|---|---|---|---|
before | string | query | no | None |
curl -X GET -H 'Accept: application/json' 'https://example.com/roll'
{
"next_cursor": null,
"posts": [],
"title": "Roll",
"topic": "roll"
}GET /standardThe standard · auth public (guest) · negotiable
Published posts in the standard topic.
curl -X GET -H 'Accept: application/json' 'https://example.com/standard'
{
"posts": [],
"title": "Standard",
"topic": "standard"
}GET /posts/{slug}Post · auth public (guest) · negotiable
A published post by slug or uid. Old slugs redirect with 301 to the canonical slug.
| Name | Type | Location | Required | Example |
|---|---|---|---|---|
slug | string | path | yes | a-pass-is-not-an-ambition |
curl -X GET -H 'Accept: application/json' 'https://example.com/posts/a-pass-is-not-an-ambition'
{
"path": {
"slug": "a-pass-is-not-an-ambition"
}
}{
"author": {
"username": "retoor"
},
"post": {
"title": "A pass is not an ambition.",
"topic": "roll"
}
}GET /flyersFlyers · auth public (guest) · negotiable
Flyer gallery.
curl -X GET -H 'Accept: application/json' 'https://example.com/flyers'
{
"items": [],
"kind": "flyer",
"title": "Flyers"
}GET /memesMemes · auth public (guest) · negotiable
Meme gallery.
curl -X GET -H 'Accept: application/json' 'https://example.com/memes'
{
"items": [],
"kind": "meme",
"title": "Memes"
}GET /peoplePeople · auth public (guest) · negotiable
People on the roll.
curl -X GET -H 'Accept: application/json' 'https://example.com/people'
{
"people": [
{
"post_count": 5,
"username": "retoor"
}
]
}GET /people/{username}Person · auth public (guest) · negotiable
Profile with posts.
| Name | Type | Location | Required | Example |
|---|---|---|---|---|
username | string | path | yes | retoor |
curl -X GET -H 'Accept: application/json' 'https://example.com/people/retoor'
{
"path": {
"username": "retoor"
}
}{
"person": {
"username": "retoor"
},
"posts": []
}GET /robots.txtrobots.txt · auth public (guest) · none
curl -X GET -H 'Accept: application/json' 'https://example.com/robots.txt'
null
GET /sitemap.xmlSitemap · auth public (guest) · none
curl -X GET -H 'Accept: application/json' 'https://example.com/sitemap.xml'
null
GET /avatar/svg/{seed}Avatar · auth public (guest) · none
Deterministic SVG avatar.
curl -X GET -H 'Accept: application/json' 'https://example.com/avatar/svg/{seed}'null
GET /healthHealth · auth public (guest) · json
Liveness and version.
curl -X GET -H 'Accept: application/json' 'https://example.com/health'
{
"status": "ok",
"version": "1.0.0"
}GET /presence/rosterPresence · auth public (guest) · json
Online set (uids).
curl -X GET -H 'Accept: application/json' 'https://example.com/presence/roster'
{
"online": [],
"tracked": 0
}Sign up to write along. Stored for administrators, never public.
GET /joinJoin form · auth public (guest) · negotiable
curl -X GET -H 'Accept: application/json' 'https://example.com/join'
{
"submitted": false
}POST /joinI'm in · auth public (guest) · negotiable
Stores a join request: name, contact and an optional repo link.
| Name | Type | Location | Required | Example |
|---|---|---|---|---|
name | string | body | yes | Ada |
contact | string | body | yes | ada@example.com |
repo_url | string | body | no | https://git.example.com/ada |
message | string | body | no | I write about finishing things. |
curl -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' -d '{"name": "Probe", "contact": "probe@example.com"}' 'https://example.com/join'{
"contact": "probe@example.com",
"name": "Probe"
}{
"data": {
"uid": "0190..."
},
"ok": true,
"redirect": "/join?ok=1"
}GET /invite/{token}Invite claim form · auth public (guest) · negotiable
Shows the claim form for a live invite; unknown, used and expired links answer 404 with the same message.
| Name | Type | Location | Required | Example |
|---|---|---|---|---|
token | string | path | yes | unknown-token |
curl -X GET -H 'Accept: application/json' 'https://example.com/invite/unknown-token'
{
"path": {
"token": "unknown-token"
}
}{
"email": "ada@example.com",
"email_locked": true,
"username": "ada",
"valid": true
}POST /invite/{token}Claim invite · auth public (guest) · negotiable
Creates the Member account, logs in and marks the join request accepted. Single use.
| Name | Type | Location | Required | Example |
|---|---|---|---|---|
token | string | path | yes | unknown-token |
username | string | body | yes | ada |
email | string | body | yes | ada@example.com |
password | string | body | yes | correct horse battery staple |
password_confirm | string | body | yes | correct horse battery staple |
terms | boolean | body | yes | True |
curl -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' -d '{"username": "Probe", "email": "probe@example.com", "password": "probe-pass-1", "password_confirm": "probe-pass-1", "terms": true}' 'https://example.com/invite/unknown-token'{
"email": "probe@example.com",
"password": "probe-pass-1",
"password_confirm": "probe-pass-1",
"terms": true,
"username": "Probe"
}{
"data": {
"username": "ada"
},
"ok": true,
"redirect": "/"
}Session login, API key, notifications. No JWT, no OAuth.
GET /auth/loginLogin page · auth public (guest) · negotiable
curl -X GET -H 'Accept: application/json' 'https://example.com/auth/login'
{
"errors": [],
"next": "/admin"
}POST /auth/loginLog in · auth public (guest) · negotiable
Sets an httponly session cookie (7 days, 30 with remember me).
| Name | Type | Location | Required | Example |
|---|---|---|---|---|
username | string | body | yes | {{username}} |
password | password | body | yes | None |
remember | boolean | body | no | None |
curl -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' -d '{"username": "retoor"}' 'https://example.com/auth/login'{
"body": {
"username": "retoor"
}
}{
"data": null,
"ok": true,
"redirect": "/admin"
}POST /auth/logoutLog out · auth public (guest) · negotiable
curl -X POST -H 'Accept: application/json' 'https://example.com/auth/logout'
{
"data": null,
"ok": true,
"redirect": "/"
}GET /profile/api-keyView API key · auth member (Member) · negotiable
curl -X GET -H 'Accept: application/json' -H 'X-API-KEY: YOUR_API_KEY' 'https://example.com/profile/api-key'
{
"api_key": "YOUR_API_KEY"
}POST /profile/api-key/regenerateRenew API key · auth member (Member) · negotiable
curl -X POST -H 'Accept: application/json' -H 'X-API-KEY: YOUR_API_KEY' 'https://example.com/profile/api-key/regenerate'
{
"data": {
"api_key": "..."
},
"ok": true,
"redirect": "/profile/api-key"
}POST /profile/avatar/regenerateRenew avatar · auth member (Member) · negotiable
curl -X POST -H 'Accept: application/json' -H 'X-API-KEY: YOUR_API_KEY' 'https://example.com/profile/avatar/regenerate'
{
"ok": true
}GET /notificationsNotifications · auth member (Member) · negotiable
curl -X GET -H 'Accept: application/json' -H 'X-API-KEY: YOUR_API_KEY' 'https://example.com/notifications'
{
"notifications": [],
"unread": 0
}POST /notifications/readMark all read · auth member (Member) · negotiable
curl -X POST -H 'Accept: application/json' -H 'X-API-KEY: YOUR_API_KEY' 'https://example.com/notifications/read'
{
"ok": true,
"redirect": "/notifications"
}GET /termsTerms · auth public (guest) · negotiable
curl -X GET -H 'Accept: application/json' 'https://example.com/terms'
{
"title": "Terms"
}POST /terms/acceptAccept terms · auth member (Member) · negotiable
curl -X POST -H 'Accept: application/json' -H 'X-API-KEY: YOUR_API_KEY' 'https://example.com/terms/accept'
{
"ok": true,
"redirect": "/"
}GET /privacyPrivacy · auth public (guest) · negotiable
curl -X GET -H 'Accept: application/json' 'https://example.com/privacy'
{
"title": "Privacy"
}